両建てグリッドEAをゼロから作る(第9回:損切り+再エントリーと比べる)

この連載は研究用です。 読み物として楽しんでもらえれば。

前回のおさらい

番外編の4つの型(第5〜8回)は、どれも同じ形で負けました。小さなサイクルを何十回も +5ドル前後で終えたあと、終わらないサイクルが1本来て、それが損益のほとんどを決めました。本編の剥がし型(第3・4回)も同じです。

回型損益合計終わらなかったサイクルの始まり
第4回⑤剥がし型−10,009 USD2025-02-11
第5回①同時両建て型−11,973 USD2025-02-03
第6回③段階ヘッジ型−10,055 USD2025-01-20
第7回④挟み直し型(0.01ロット換算)−407 USD2025-01-06
第8回⑥ゾーン・リカバリー型−50,032 USD(口座がなくなった)2025-01-20

それなら、終わらないサイクルを 自分で終わらせて しまえばどうなるか。今回はこれを試します。 

第9回:−50ドルで全部やめて、また始める

EAは第4回の剥がし型(HG06)に、ルールを1つだけ足したものです。

項目設定
ほかの設定第4回とまったく同じ(ADXでエントリー、+5ドルで利食い、5ドル逆行でロック、5ドル抜けで追加、上限20本、サブ玉、剥がし、まとめて +5ドルで決済)
足したルールそのサイクルの玉の含み損益(メイン玉+サブ玉、スワップ込み)が −50ドルまで下がったら、全部決済する
そのあと何もなかったように、次のエントリーを待つ(いつもどおり ADX で入る)

−50ドルは、0.01ロットの玉が10本ぶん、5ドルずつ逆行したくらいの損です。第4回で終わらなかったサイクルの損(約 −10,000 USD)の200分の1で打ち切る、ということになります。

比べる相手は、第4回と同じ設定を、今回あらためて回し直したものです。テスターは今のスワップ値を過去にも当てはめるので、第4回の表とは少しずれます(損益合計 −10,009 → −9,976 USD)。 

結果:固まらなくなったが、損は2倍になった

条件は第0回の表のとおりです(XAUUSD、2025-01-01〜2026-07-01、リアルティック、メイン玉は0.01ロット、初期証拠金 50,000 USD)。

項目第4回(損切りなし)第9回(−50ドルで損切り+再エントリー)
1サイクル(エントリーから全部決済まで)48回5,304回
うちプラスで終わったサイクル47回(合計 +241 USD)4,422回(合計 +23,945 USD)
損切りしたサイクル—882回(合計 −46,023 USD)
損益合計−9,976 USD−22,079 USD
最大DD(含み損込み)−19,038 USD−22,177 USD
含み損込みの一番低いところ31,203 USD(2026-01-29)27,903 USD(2026-06-30)
同時に持った玉最大25本・0.30ロット最大12本・0.14ロット
試行回数 N2031
DSR0.001(N=100 換算 0.000)0.000(N=100 換算 0.000)
含み損込みの資産の推移

損切りを入れると、サイクルは固まらなくなりました。第4回は2025年2月28日のあと新しい玉を1本も建てられませんでしたが、第9回は最後の日まで建て続け、サイクルは5,304回まで増えました。玉の数も最大12本で止まっています。

それでも、損益合計は第4回の2倍以上です。図の金色の線は、ほとんど上がらないまま、下がり続けました。 

何が起きたか:1回の損切りが、勝ち10回分を消す

サイクルの勝ちと負けを並べると、こうなります。

回数1回あたり
プラスで終わったサイクル4,422回(83.4%)+5.41 USD
損切りしたサイクル882回(16.6%)−52.18 USD

1回の損切りは、勝ち約10回ぶんです。トントンにするには、勝率が 90.6% 必要でした。実際は 83.4% です。

損切りの882回のうち、825回は −50〜−55 USD で止まっています。残りの57回(合計 −3,994 USD)は、週末をまたいだり、価格が一気に動いたりして、−50 を通り過ぎてから決済されたものです。いちばん大きいのは、2026年6月12日の金曜に始まって、週明け15日の朝に −238 USD で決済されたサイクルでした。 

損はどこへ行ったか

もう1つ、別の見方をします。第9回では、玉を 18,377本 建てました。1本建てるたびに、買値と売値の差(スプレッド)を1回払っています。

項目金額
スプレッド(建てたときの差 × ロット、合計)約 −20,500 USD
手数料−1,111 USD
スワップ−477 USD
コストの合計約 −22,100 USD
損益合計−22,079 USD

損益合計は、ほぼコストの合計と同じです。言いかえると、値動きそのものからは、勝ちも負けもほとんど出ていません。損切りして入り直すたびに払ったスプレッドが、そのまま積み上がっただけです。

とくに2026年1〜3月は、Goldが大きく上下して、エントリーと損切りが一気に増えました。この3か月で建てた玉は8,261本、スプレッドの中央値は1.12ドルに広がり、スプレッドだけで約12,800 USD を払っています。図の金色の線が2026年2月に急に落ちているのは、このためです。

第4回の損は、固まったサイクル1本(−10,217 USD、うちスワップ −2,570 USD)でした。第9回は、それを882回の小さな損切りと、18,377回分のスプレッドに置きかえただけでした。 

5ドル動いたあと、戻ってくるのか

両建てグリッドは、どの型も「逆に動いた価格は、いずれ戻ってくる」ことを当てにしています。ロックして待つのも、玉を足すのも、剥がすのも、戻ってくれば勝ち、戻ってこなければ固まる、という作りです。

そこで最後に、Goldの1分足で、それを直接数えてみました。同じ向きに5ドルずつ m 回動いたあと、先に5ドル戻るか、先にもう5ドル進むかです。比べる相手として、同じ期間の1分ごとの値動きを並べ替えて「前の動きを覚えていない」値動きを作り、同じように数えました。

5ドル動いたあと、先に5ドル戻る確率
1回2回3回4回5回6回7回8回
実際のGold0.5030.5160.5260.5350.5420.5270.5300.495
並べ替えたもの0.5040.5050.4940.5090.5030.4890.4930.483

実際のGoldは、同じ向きに3〜5回動いたあとだけ、並べ替えたものより少し戻りやすくなっています。戻る力はゼロではありません。

ただ、スプレッドを払うと話が変わります。往復のコストを 0.35ドル(この連載を始める前の調査で使った値)とすると、5ドル戻るほうに賭けてトントンになる確率は 0.535 です(点線)。これを超えたのは5回のときの 0.542 だけで、それも95%の幅(縦線)は点線をまたいでいます。今回のテストで実際に払ったスプレッドは中央値で 0.7ドル前後だったので、トントンの線は 0.57 まで上がり、どの回数でも届きません。

(この数え方は、パラメータを選んだり EA を変えたりしていない、ただの集計です。なので試行回数 N には数えていません。) 

第9回のまとめ

  • 終わらないサイクルを −50ドルで損切りすると、サイクルは固まらなくなった
  • ただし損切りは勝ち約10回ぶんで、トントンに必要な勝率 90.6% に届かず、損益合計は第4回の2倍以上(−22,079 USD)になった
  • 損益合計は、ほぼスプレッドと手数料とスワップの合計だった。固まったサイクル1本の損が、882回の損切りと18,377回分のスプレッドに置きかわっただけだった
  • Goldの値動きは、5ドル動いたあと少しだけ戻りやすいが、その差はスプレッドとほぼ同じ大きさだった 

次回

次回は、ここまで素朴に作ってきた部品を全部、少しずつ賢くした 完成版 を回します。タイトルは「完成版を回す」です。

試行回数について

今回は N=31 です。第8回(N=30)の続きで数えています。比べた第4回の回し直しは、第4回と同じ試行なので数えていません。 

ソースコード

研究用です。リアル口座では使わないでください。ログ出力の hg_log.mqh は第1回と同じです。第4回の HG06_Recovery.mq5 に、損切りの数行(InpCycleStop)を足しただけです。ファイル名の番号(HG12)は作った順の通し番号で、回の番号とはずれています。

HG12_StopReentry.mq5

//+------------------------------------------------------------------+
//| HG12_StopReentry.mq5                                              |
//| Hedged Grid series, chapter 11: HG06 (chapter 4) plus one rule.   |
//| Stop  : when the open positions (main + sub) lose InpCycleStop    |
//|         in total, close them all. The next cycle starts as usual. |
//| Everything else is exactly HG06.                                  |
//| 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 double InpCycleStop    = 50.0;   // Close all when floating profit falls to minus this (USD)
input long   InpMagic        = 260812; // Magic number (main); sub uses InpMagic + 1
input string InpTag          = "ch11"; // 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("HG12 %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;
      }
      // Stop: give up the cycle
      if(f1 + f2 <= -InpCycleStop)
      {
         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;
}

コメント

タイトルとURLをコピーしました