両建てグリッドEAをゼロから作る(第3回:抜けた方向へ追加して、まとめて決済)

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

前回のおさらい

第2回は、逆行したら反対方向に同じ量を建てて(両建てロック)、建値に戻るのを待ちました。戻ったときの結果は「5ドル損切り+余計なコスト」。戻らなかったときは、1年5か月固まったままスワップを払い続けました。

固まったロックをほどくには、何かしら動きを足す必要があります。 

第3回:抜けた方向に玉を足し、全体がプラスになったら全部決済

項目設定
エントリー・利食い・ロック第2回と同じ(ADXでエントリー、+5ドルで利食い、5ドル逆行したらロック)
追加ロックしたあと、持っている玉の一番外側の建値から5ドル抜けたら、その方向に0.01ロットを1本足す(上に抜けたら買い、下に抜けたら売り)
まとめて決済全部の玉の含み損益(スワップ込み)の合計が +5ドル になったら、全部決済
上限20本まで

たとえば、買いでエントリーして5ドル下がったら売りでロック。そこからさらに5ドル下がったら売りを1本追加、もう5ドル下がったらまた1本……。売りが増えるほど、下げが続けば全体はプラスに近づき、どこかで +5ドルに届いて全部決済できます。

追加とまとめて決済の2つの結末

トレンドが出ればロックがほどける仕組みです。弱点は右図のように、上下に往復されたときです。 

結果:122勝1敗

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

項目値
1サイクル(エントリーから全部決済まで)123回
うちプラスで終わったサイクル122回(99%、合計 +624 USD)
マイナスで終わったサイクル1回(−3,903 USD。テスト終了まで決済できず)
損益合計−3,279 USD
最大DD(含み損込み)−6,414 USD(初期証拠金の12.8%)
同時に持った玉最大20本・0.20ロット
試行回数 N19
DSR0.002(N=100 換算 0.000)
資産の推移

確定した残高(黒)は2025年4月まで順調に右肩上がりで、そこからはまったく動きません。含み損込みの線(灰色)は、4月下旬からじわじわ、Goldが上げ足を速めた9月以降は大きく沈んでいき、2026年1月末には 44,209 USD まで下がりました。最終日の段差は、テスト終了で全部の玉が強制的に閉じられた分です。 

122勝1敗の1敗

上限20本に達したサイクル

2025年4月24〜25日のGoldは、3,320ドル前後でもみ合ったあと、25日の朝に3,365ドルまで上げ、昼から3,270ドルまで下げました。

  • 上に抜けるたびに買いを足し、下に抜けるたびに売りを足す
  • どちらかに抜け切る前に反対側へ戻されるので、全体の含み損益はプラスにならない
  • 1日半で上限の20本(買い9本・売り11本)に到達して、それ以上何もできなくなった

これが、いわゆる往復ビンタです。抜けた方向に足す仕組みは、一方向のトレンドには強いのですが、幅の広いレンジを行ったり来たりされると、玉が増えるだけになります。

20本で止まったあと、Goldは4,000ドル台まで上がりました。売りが2本多いので、上がるほど含み損が増えます。スワップも毎日つきます。売り11本は受け取り(+668 USD)でしたが、買い9本の支払い(−2,673 USD)のほうがずっと大きく、差し引き14か月で −2,005 USD。最後はテスト終了の時点で強制的に決済して −3,903 USD でした。

ほかにも2回、20本に達したサイクルがありました(2月と4月上旬)。どちらも、その後トレンドが出て運よくプラスで決済できましたが、2月のほうは決済まで48日かかっています。

(ついでに言うと、25日の日本時間朝7時ごろ(Goldの取引が毎日再開する直後)に追加した買いの1本は、スプレッドが20ドル開いた瞬間に約定していました。再開直後はスプレッドが大きく開くので、こういうところで玉を足すのも損のもとです。) 

第3回のまとめ

  • 抜けた方向に足してまとめて決済すると、勝率は99%まで上がる。資産曲線も、途中まではきれいな右肩上がり
  • そのかわり、負けるときは50回分の利益を一度に吹き飛ばす
  • 1回の負けの中身は、往復ビンタで玉が上限まで積み上がり、そのまま固まってスワップを払い続けたもの

Xで見かける「チャートに矢印がずらっと並んで、資産曲線がきれい」なスクショは、たぶんこの黒い線の前半の部分です。線が多いほどインパクトはありますが、だいたいうまく行っていないときです。 

次回

剥がし型には、この「積み上がった玉」を処理するための仕組みがもう1つあります。新しく大きめのロットで玉を建て(サブ玉)、その稼ぎで一番負けている玉を1本ずつ消していく剥がし(ピール)です。次回はこれを足して、固まった20本がほどけるかを見ます。 

試行回数について

今回は N=19 です。 

ソースコード

研究用です。これまた一応載せておく。ログ出力の hg_log.mqh は第1回と同じです。ファイル名の番号(HG04)は作った順の通し番号で、回の番号とはずれています。

HG04_Basket.mq5

//+------------------------------------------------------------------+
//| HG04_Basket.mq5                                                   |
//| Hedged Grid series, chapter 3: lock, then add in the breakout     |
//| direction and close the whole basket at a profit target.          |
//| Entry : same as HG01 (ADX on M1 and M30, DI direction agrees)     |
//| Single: take profit on the first position; lock (same size,       |
//|         opposite side) after InpLockDistance against it            |
//| Basket: when price goes InpAddDistance beyond the highest (lowest) |
//|         open price, add one buy (sell). Close everything when the  |
//|         floating profit of all positions reaches InpBasketTarget.  |
//| 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;   // 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;    // 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;     // Position cap
input long   InpMagic        = 260804; // Magic number
input string InpTag          = "ch04"; // Log file tag

CTrade   trade;
int      hAdxM1 = INVALID_HANDLE, hAdxHTF = INVALID_HANDLE;
datetime lastEntryBar = 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.SetExpertMagicNumber(InpMagic);
   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;
}

// Scan our positions: count, floating profit (profit + swap), outermost open prices
int Scan(double &floating, double &hiOpen, double &loOpen, ulong &single)
{
   int n = 0;
   floating = 0; hiOpen = -DBL_MAX; loOpen = DBL_MAX; single = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong t = PositionGetTicket(i);
      if(t == 0 || PositionGetString(POSITION_SYMBOL) != _Symbol
         || PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
      n++;
      single = t;
      floating += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
      double op = PositionGetDouble(POSITION_PRICE_OPEN);
      hiOpen = MathMax(hiOpen, op);
      loOpen = MathMin(loOpen, op);
   }
   return n;
}

void CloseAll()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong t = PositionGetTicket(i);
      if(t > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol
         && PositionGetInteger(POSITION_MAGIC) == InpMagic) trade.PositionClose(t);
   }
}

void OnTick()
{
   HGLog_OnTick();
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick)) return;
   string sp = StringFormat("sp=%.2f", tick.ask - tick.bid);

   double floating, hiOpen, loOpen;
   ulong single;
   int n = Scan(floating, hiOpen, loOpen, single);

   if(n == 1)
   {
      // Single position: take profit is on the order; lock after an adverse move
      PositionSelectByTicket(single);
      bool   isBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
      double open  = PositionGetDouble(POSITION_PRICE_OPEN);
      if(isBuy && tick.bid <= open - InpLockDistance)
      {
         trade.PositionModify(single, 0, 0);   // basket rules take over
         trade.Sell(InpLot, _Symbol, tick.bid, 0, 0, "HG04 lock sell " + sp);
      }
      if(!isBuy && tick.ask >= open + InpLockDistance)
      {
         trade.PositionModify(single, 0, 0);
         trade.Buy(InpLot, _Symbol, tick.ask, 0, 0, "HG04 lock buy " + sp);
      }
      return;
   }
   if(n >= 2)
   {
      if(floating >= InpBasketTarget) { CloseAll(); return; }
      if(n >= InpMaxPositions) return;
      if(tick.ask >= hiOpen + InpAddDistance)
         trade.Buy(InpLot, _Symbol, tick.ask, 0, 0, "HG04 add buy " + sp);
      else if(tick.bid <= loOpen - InpAddDistance)
         trade.Sell(InpLot, _Symbol, tick.bid, 0, 0, "HG04 add sell " + sp);
      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;

   bool ok;
   if(side == 1)
      ok = trade.Buy(InpLot, _Symbol, tick.ask, 0,
                     NormalizeDouble(tick.ask + InpTakeProfit, _Digits), "HG04 buy " + sp);
   else
      ok = trade.Sell(InpLot, _Symbol, tick.bid, 0,
                      NormalizeDouble(tick.bid - InpTakeProfit, _Digits), "HG04 sell " + sp);
   if(ok) lastEntryBar = bar;
}

コメント

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