この連載は研究用です。 読み物として楽しんでもらえれば。
前回のおさらい
第3回のEAは、抜けた方向へ玉を足し、全体がプラスになったらまとめて決済しました。122勝1敗まで勝率は上がりましたが、その1敗は、往復ビンタで上限の20本まで積み上がり、そのまま固まったものでした。
剥がし型には、この「積み上がった玉」を処理するための仕組みがもう1つあります。第0回で見た⑤剥がし型の名前のもとになった「剥がし」です。今回はこれを足して、剥がし型を完成させます。
第4回:サブ玉を建てて、その稼ぎでメイン玉を1本ずつ消す
これまでの玉をまとめて メイン玉、負けを回収するために新しく足す玉を サブ玉 と呼びます。
| 項目 | 設定 |
|---|---|
| メイン玉 | 第3回と同じ(ADXでエントリー、+5ドルで利食い、5ドル逆行でロック、5ドル抜けで追加、上限20本) |
| サブ玉を始める条件 | メイン玉が 8本以上になったとき |
| サブ玉の中心 | メイン玉の建値の一番高いところと一番安いところの、ちょうど真ん中 |
| サブ玉を建てる | 中心から 5ドル抜けたら、その方向に 0.02ロット(メイン玉の2倍)。さらに5ドル進むごとに1本追加、上限5本 |
| 剥がす | 「メイン玉で一番負けている1本」と「サブ玉全部」の損益の合計が +1ドル以上になったら、そのメイン玉の1本とサブ玉を全部決済。中心を引き直して続ける |
| まとめて決済 | メイン玉とサブ玉を合わせた損益が +5ドル以上で全部決済(第3回と同じ) |

ねらいは左図です。メイン玉がどちらかに抜けたとき、サブ玉が同じ方向に大きめのロットで乗って稼ぎます。その稼ぎで、メイン玉の一番ひどい含み損を1本だけ消す。これを繰り返せば、積み上がったメイン玉が少しずつ減っていくはずです。
(ここでの値は、この連載で決めたものです。サブ玉を何本目から始めるか、ロットをどう決めるかなどは作り方しだいで、別の値は第12回で回します。)
結果:損は3倍、DDも3倍
条件は第0回の表のとおりです(XAUUSD、2025-01-01〜2026-07-01、リアルティック、メイン玉は0.01ロット、初期証拠金 50,000 USD)。
| 項目 | 第3回 | 第4回 |
|---|---|---|
| 1サイクル(エントリーから全部決済まで) | 123回 | 48回 |
| うちプラスで終わったサイクル | 122回(99%) | 47回(98%、合計 +240 USD) |
| 損益合計 | −3,279 USD | −10,009 USD |
| 最大DD(含み損込み) | −6,414 USD(12.8%) | −19,061 USD(38%) |
| 同時に持った玉 | 最大20本・0.20ロット | 最大24本・0.28ロット |
| 試行回数 N | 19 | 20 |
| DSR | 0.002 | 0.001(N=100 換算 0.000) |

確定した残高(黒)は、2025年2月で止まっています。そこから1年4か月、一度も決済がありません。含み損込みの線(灰色)は第3回より早く、深く沈み、2026年1月末には 31,179 USD まで下がりました。
剥がしを足したら、損もDDも第3回の約3倍になりました。
剥がしは、ちゃんと動いていた
最初の2か月は、ねらいどおりに動いています。剥がしは4回ありました。
| 日時(サーバー時間) | 消した玉 | 剥がしの損益 |
|---|---|---|
| 2025-01-21 17:01 | メイン玉1本 + サブ玉3本 | +1.10 USD |
| 2025-01-30 17:43 | メイン玉1本 + サブ玉4本 | +1.25 USD |
| 2025-02-03 16:50 | メイン玉1本 + サブ玉3本 | +1.74 USD |
| 2025-02-28 15:55 | メイン玉1本 + サブ玉4本 | +1.02 USD |
たとえば2月28日は、メイン玉の中で一番負けていた買いの1本(スワップ込み −110.28 USD)を、サブ玉の売り4本の利益(合計 +111.30 USD)で消しています。
何が起きたか:サブ玉は「損切りのない大きめの賭け」
問題は、その2月28日の直後です。

- 2月11〜20日:上下に振られて、メイン玉が上限の20本まで積み上がる(第3回と同じ往復ビンタ)
- 2月28日:Goldが2,850ドル台まで下げたところで剥がしが成功。メイン玉が19本になる
- 剥がした直後に中心を引き直すと、価格はすでに5ドル下に抜けていたので、サブ玉の売りがすぐに建つ。メイン玉も1本足されて、また20本
- 同じ日のうちにサブ玉の売りが4本(0.08ロット)そろう。安値は2,833ドル
ところが、Goldはここで底を打ちました。そこから上がり続け、テスト終了時には4,000ドル台です。
- サブ玉には損切りがない。剥がしもまとめ決済も「プラスになったら」しか動かない
- メイン玉は上限の20本で、もう追加もできない
- 0.02ロットの売りが残っているので、上がれば上がるほどメイン玉の2倍の速さで損が増える
2月28日の17時を最後に、EAは1年4か月、1本も建てていません。 テスト終了で強制決済したときの内訳は、サブ玉の売り4本だけで −8,760 USD、メイン玉の20本は差し引き −1,488 USD でした(メイン玉はスワップが −3,169 USD)。
剥がしは、メイン玉の含み損を「消す」仕組みではなく、一方向に大きめに賭けて、当たったらメイン玉を1本消す仕組みです。外れたときの損は、メイン玉より大きなロットで積み上がります。
第4回のまとめ
- 剥がしは、トレンドが続く間は、負けているメイン玉を1本ずつきれいに消してくれる
- ただしサブ玉には損切りがないので、外れるとメイン玉より大きなロットで含み損が増える
- 今回の期間では、第3回より早く・大きく負けた。回収の仕組みを足しても、損がなくなるわけではない
ここまでで、第0回の⑤剥がし型の部品はすべてそろいました。剥がし型は、どう作ってもサブ玉に損切りがない(入れると剥がしの意味がなくなる)のがポイントです。
次回
次回からは番外編です。第0回で紹介した残りの型を、同じ条件で1つずつ回します。最初は、いちばん素直な ①同時両建て型 です。
試行回数について
今回は N=20 です。記事にする前に、同じEAを別の初期証拠金で回したことがあります。ロットが固定なのでトレードは1本残らず同じになり、試行としては同じ1回と数えています。
ソースコード
まーまだ使う人はいないとはず。ログ出力の hg_log.mqh は第1回と同じです。メイン玉とサブ玉はマジックナンバーを分けています(サブ玉はメイン玉の +1)。ファイル名の番号(HG06)は作った順の通し番号で、回の番号とはずれています。
HG06_Recovery.mq5
//+------------------------------------------------------------------+
//| HG06_Recovery.mq5 |
//| Hedged Grid series, chapter 4: sub group that removes the |
//| worst losing position one at a time. |
//| Main : same as HG04 (entry, lock, breakout adds, |
//| basket close of everything at InpBasketTarget) |
//| Sub : once the main group has InpSubStart positions, take the |
//| middle of its highest and lowest open prices. When price |
//| moves InpSubDistance away from it, open InpSubLot in that |
//| direction, and add one more every InpSubDistance. |
//| Peel : when (worst main position + all sub positions) >= |
//| InpPeelTarget, close them all, then re-center. |
//| Research code for the fxwolf2.com blog. Not for live trading. |
//+------------------------------------------------------------------+
#property copyright "fxwolf2.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
#include "hg_log.mqh"
input double InpLot = 0.01; // Main lot size
input int InpAdxPeriodM1 = 14; // ADX period (M1)
input double InpAdxLevelM1 = 22.0; // ADX threshold (M1)
input int InpAdxPeriodHTF = 14; // ADX period (higher timeframe)
input double InpAdxLevelHTF = 22.0; // ADX threshold (higher timeframe)
input ENUM_TIMEFRAMES InpHTF = PERIOD_M30; // Higher timeframe
input double InpTakeProfit = 5.0; // Take profit of a single position (USD)
input double InpLockDistance = 5.0; // Adverse move that triggers the lock (USD)
input double InpAddDistance = 5.0; // Main breakout distance beyond the outermost open price (USD)
input double InpBasketTarget = 5.0; // Close all when floating profit reaches this (USD)
input int InpMaxPositions = 20; // Main position cap
input int InpSubStart = 8; // Main positions needed before the sub group starts
input double InpSubLot = 0.02; // Sub lot size
input double InpSubDistance = 5.0; // Sub breakout distance from the center / between sub adds (USD)
input int InpSubMax = 5; // Sub position cap
input double InpPeelTarget = 1.0; // Peel when worst main + all sub reaches this (USD)
input long InpMagic = 260806; // Magic number (main); sub uses InpMagic + 1
input string InpTag = "ch06"; // Log file tag
CTrade trade;
int hAdxM1 = INVALID_HANDLE, hAdxHTF = INVALID_HANDLE;
datetime lastEntryBar = 0;
bool subMode = false; // Sub group is active for the current cycle
double subCenter = 0;
int OnInit()
{
hAdxM1 = iADX(_Symbol, PERIOD_M1, InpAdxPeriodM1);
hAdxHTF = iADX(_Symbol, InpHTF, InpAdxPeriodHTF);
if(hAdxM1 == INVALID_HANDLE || hAdxHTF == INVALID_HANDLE) return INIT_FAILED;
trade.SetTypeFillingBySymbol(_Symbol);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
HGLog_Deinit(InpTag);
}
// Direction from one ADX handle on the last closed bar: 1=buy, -1=sell, 0=none
int AdxSide(const int handle, const double level)
{
double adx[1], plus[1], minus[1];
if(CopyBuffer(handle, 0, 1, 1, adx) != 1) return 0;
if(CopyBuffer(handle, 1, 1, 1, plus) != 1) return 0;
if(CopyBuffer(handle, 2, 1, 1, minus) != 1) return 0;
if(adx[0] < level) return 0;
if(plus[0] > minus[0]) return 1;
if(minus[0] > plus[0]) return -1;
return 0;
}
bool Ours(const ulong t, const long magic)
{
return t > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol
&& PositionGetInteger(POSITION_MAGIC) == magic;
}
double Net(const ulong t)
{
PositionSelectByTicket(t);
return PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
}
// Scan one group: count, floating profit, outermost open prices, last ticket,
// worst position (lowest net) and the open price of the latest position
int Scan(const long magic, double &floating, double &hiOpen, double &loOpen,
ulong &last, ulong &worst, double &lastOpen)
{
int n = 0;
double worstNet = DBL_MAX;
datetime lastTime = 0;
floating = 0; hiOpen = -DBL_MAX; loOpen = DBL_MAX; last = 0; worst = 0; lastOpen = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong t = PositionGetTicket(i);
if(!Ours(t, magic)) continue;
n++;
last = t;
double net = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
floating += net;
if(net < worstNet) { worstNet = net; worst = t; }
double op = PositionGetDouble(POSITION_PRICE_OPEN);
hiOpen = MathMax(hiOpen, op);
loOpen = MathMin(loOpen, op);
datetime ot = (datetime)PositionGetInteger(POSITION_TIME);
if(ot >= lastTime) { lastTime = ot; lastOpen = op; }
}
return n;
}
void CloseGroup(const long magic)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong t = PositionGetTicket(i);
if(Ours(t, magic)) trade.PositionClose(t);
}
}
int SubSide()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong t = PositionGetTicket(i);
if(Ours(t, InpMagic + 1))
return PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? 1 : -1;
}
return 0;
}
void Open(const long magic, const int side, const double lot, const double tp,
const string what, const MqlTick &tick)
{
trade.SetExpertMagicNumber(magic);
string c = StringFormat("HG06 %s %s sp=%.2f", what, side == 1 ? "buy" : "sell", tick.ask - tick.bid);
if(side == 1) trade.Buy(lot, _Symbol, tick.ask, 0, tp, c);
else trade.Sell(lot, _Symbol, tick.bid, 0, tp, c);
}
void OnTick()
{
HGLog_OnTick();
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick)) return;
double f1, hi1, lo1, lastOpen1, f2, hi2, lo2, lastOpen2;
ulong last1, worst1, last2, worst2;
int n1 = Scan(InpMagic, f1, hi1, lo1, last1, worst1, lastOpen1);
int n2 = Scan(InpMagic + 1, f2, hi2, lo2, last2, worst2, lastOpen2);
if(n1 == 0 && n2 > 0) { CloseGroup(InpMagic + 1); return; } // sub never stays alone
if(n1 == 0) subMode = false;
if(n1 == 1 && n2 == 0)
{
// Single position: take profit is on the order; lock after an adverse move
PositionSelectByTicket(last1);
bool isBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
double open = PositionGetDouble(POSITION_PRICE_OPEN);
if(isBuy && tick.bid <= open - InpLockDistance)
{
trade.PositionModify(last1, 0, 0);
Open(InpMagic, -1, InpLot, 0, "lock", tick);
}
if(!isBuy && tick.ask >= open + InpLockDistance)
{
trade.PositionModify(last1, 0, 0);
Open(InpMagic, 1, InpLot, 0, "lock", tick);
}
return;
}
if(n1 >= 2 || n2 > 0)
{
// Basket: everything (main + sub) at the target
if(f1 + f2 >= InpBasketTarget)
{
CloseGroup(InpMagic + 1); CloseGroup(InpMagic);
subMode = false;
return;
}
// Peel: worst main position paid for by the sub group
if(n2 > 0 && worst1 > 0 && Net(worst1) + f2 >= InpPeelTarget)
{
CloseGroup(InpMagic + 1);
trade.PositionClose(worst1);
n1 = Scan(InpMagic, f1, hi1, lo1, last1, worst1, lastOpen1);
subCenter = n1 > 0 ? (hi1 + lo1) / 2 : 0;
return;
}
// Sub group starts once the main group is large enough, and stays on until the cycle ends
if(!subMode && n1 >= InpSubStart) { subMode = true; subCenter = (hi1 + lo1) / 2; }
if(subMode)
{
if(n2 == 0)
{
if(tick.ask >= subCenter + InpSubDistance) Open(InpMagic + 1, 1, InpSubLot, 0, "sub", tick);
else if(tick.bid <= subCenter - InpSubDistance) Open(InpMagic + 1, -1, InpSubLot, 0, "sub", tick);
}
else if(n2 < InpSubMax)
{
int s = SubSide();
if(s == 1 && tick.ask >= lastOpen2 + InpSubDistance) Open(InpMagic + 1, 1, InpSubLot, 0, "sub add", tick);
else if(s == -1 && tick.bid <= lastOpen2 - InpSubDistance) Open(InpMagic + 1, -1, InpSubLot, 0, "sub add", tick);
}
}
// Main breakout adds, as in HG04
if(n1 >= 1 && n1 < InpMaxPositions)
{
if(tick.ask >= hi1 + InpAddDistance) Open(InpMagic, 1, InpLot, 0, "add", tick);
else if(tick.bid <= lo1 - InpAddDistance) Open(InpMagic, -1, InpLot, 0, "add", tick);
}
return;
}
// Flat: at most one new entry per M1 bar
datetime bar = iTime(_Symbol, PERIOD_M1, 0);
if(bar == lastEntryBar) return;
int side = AdxSide(hAdxM1, InpAdxLevelM1);
if(side == 0 || AdxSide(hAdxHTF, InpAdxLevelHTF) != side) return;
double tp = side == 1 ? NormalizeDouble(tick.ask + InpTakeProfit, _Digits)
: NormalizeDouble(tick.bid - InpTakeProfit, _Digits);
Open(InpMagic, side, InpLot, tp, "entry", tick);
lastEntryBar = bar;
}

コメント