両建てグリッドEAをゼロから作る(第7回・番外編:④挟み直し型)

この連載は研究用です。 読み物として楽しんでもらえれば。番外編なので読まなくてよいw

前回のおさらい

第6回の③段階ヘッジ型は、ナンピンしながら5本目と7本目で反対の玉を足す型でした。上限の10本に届いたサイクルが翌日に止まり、1年5か月そのまま残りました。

番外編の3つ目は ④挟み直し型 です。これまでの型と違って、ロットを増やしません。

 

 

 

第7回:挟んで、負けた側を少しずつ削る

項目設定
始め方今の価格の 2.5ドル上に買いの逆指値、2.5ドル下に売りの逆指値(どちらも 0.10ロット)
片方だけ約定そのまま走って、サイクルの損益が +50 USD(0.01ロット換算で +5ドル)になったら全部決済
両方約定勝っている側が建値から 5ドル走ったら、勝った側を決済。その利益から 10 USD を残して払える分だけ、負けた側を 0.01ロット単位で一緒に決済する
負けた側だけ残る反対向きの逆指値を、負けた玉と同じロットで、価格から 5ドル先に置く。価格が負けた玉に有利な方へ動いたら、逆指値も付いていく
置き直した玉約定して、勝っている側が5ドル走ったら、上と同じように決済して削る
挟み直し型の動き

ロットは 0.10 にしています。負けた側を 0.01ロットずつ削るので、0.01ロットでは削る余地がないからです。ほかの回と比べやすいように、金額には 0.01ロット換算(10で割った値)を併記します。

 

 

 

結果:6回中5回はプラス、1回が最後まで終わらなかった

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

項目第6回(段階ヘッジ型)第7回(挟み直し型)0.10ロット0.01ロット換算
1サイクル(始めてから全部決済まで)55回6回
うちプラスで終わったサイクル54回(合計 +283 USD)5回(合計 +254 USD)+25 USD
決済した回数138回(勝率 64%)482回(勝率 98%)
損益合計−10,055 USD−4,071 USD−407 USD
最大DD(含み損込み)−23,087 USD−15,499 USD−1,550 USD
含み損込みの一番低いところ(初期証拠金から)−22,808 USD(2026-01-29)−4,559 USD(2026-06-15)−456 USD
同時に持った玉最大12本・0.12ロット最大2本・0.20ロット0.02ロット
試行回数 N2829
DSR0.002(N=100 換算 0.000)0.000(N=100 換算 0.000)
資産の推移

今回の図は、これまでと形が逆です。確定した残高(黒)は階段状に増えて、一時は 60,940 USD まで上がりました。含み損込み(灰色)は、ゆっくり下がり続けています。2本の線の差が、ずっと持ち続けた負け玉の含み損です。テスト終了で強制決済すると、残高は 45,929 USD に落ちました。

 

 

 

何が起きたか:1月6日の売り0.05ロット

最初の5回は、どれも +50 USD 前後で終わりました。6回目のサイクルは 2025-01-06 15:47 に始まります。

時点起きたこと
1月6日 15:47〜16:17売りが約定して、すぐ買いも約定。売りが5ドル走って利食い(+50.50)、買いを 0.03ロット削る(−32.46)
16:43〜16:51残った買い 0.07ロットの下に売りを置き直し、約定。今度は買いが5ドル走って決済、売りを 0.02ロット削る
16:51 以降売り 0.05ロット(建値 2,623.57)が残る。その上5ドルに買いの逆指値

ここからGoldは上がり続けました。残った売りの上に置いた買いは、約定して5ドル走るたびに、毎回 +25 USD 前後で利食いされます。全期間で 467回、1回も負けずに合計 +12,106 USD です。黒い線が増え続けたのはこのためです。

ところが、売りは一度も削れませんでした。0.01ロット削るには、買いの利益(約 +25 USD)から 10 USD を残して、売り 0.01ロット分の含み損を払わなければいけません。売りの含み損が 0.01ロットあたり 15 USD を超えると(価格が建値から15ドル上がると)、もう1回も削れません。それが1月6日のうちに起きました。

買いの利食いは、売りの含み損が増える分を後ろから追いかけていただけです。5ドル走った分しか取らず、次の逆指値はまた5ドル上に置くので、上げ幅のすべては取りきれません。その差とスワップが、灰色の線をゆっくり下げていきました。

そして 2026-01-29 01:39、Goldが急騰した高値の 5,598.99 で、最後の買いが約定しました。直後にGoldは下げ、買いは5ドル走れません。売りも建値(2,623.57)から5ドル下まで戻ることはありません。売りと買いが 0.05ロットずつ、ロックされた状態のまま、テスト終了まで5か月止まりました。

強制決済した玉損益うちスワップ
売り 0.05(2,623.57)−6,920 USD+383 USD
買い 0.05(5,598.99)−7,962 USD−512 USD
このサイクルの合計(途中の利食い込み)−4,325 USD

(スワップの向きと大きさは業者や時期で変わるので、ここは割り引いて見てください。)

途中の2025年5〜8月と11月に黒い線が平らなのも、同じ理由です。高値近くで約定した買いが、下げて5ドル走れないまま、次の上げまで待っていました。

 

 

 

第6回と比べると

0.01ロット換算の損益は −407 USD、含み損込みの一番低いところは初期証拠金から −456 USD で、これまでの回よりずっと浅く済んでいます。ロットを増やさない型なので、残った負け玉は最初のロットの半分(0.01ロット換算で 0.005ロット)でした。

ただ、形は同じです。2025年1月の上げの入り口で始まったサイクルが1本、最後まで終わらなかった。損の大きさを決めたのは、そのとき残った玉の量でした。

 

 

 

第7回のまとめ

  • 挟み直し型は、勝った側の利益で負けた側を少しずつ削る。ロットは増えない
  • 今回の期間では、2025年1月6日に残った売り0.05ロットが、初日のうちに削れない大きさの含み損になり、最後まで残った
  • その売りを追いかける買いは467回すべて勝ち、確定した残高は 60,940 USD まで増えたが、含み損込みの資産は下がり続けた
  • 最後は高値で入った買いと売りがロックされ、5か月止まった
  • 勝率98%でも、残った1本の玉が損益を決める

 

 

 

次回

番外編の最後は、⑥ゾーン・リカバリー型です。2本の線の間を行き来するたびに、ロットを大きくして反対に建てていきます。

 

 

 

試行回数について

今回は N=29 です。第6回(N=28)の続きで数えています。

 

 

 

ソースコード

研究用です。リアル口座では使わないでください。ログ出力の hg_log.mqh は第1回と同じです。ファイル名の番号(HG10)は作った順の通し番号で、回の番号とはずれています。

HG10_Squeeze.mq5

//+------------------------------------------------------------------+
//| HG10_Squeeze.mq5                                                  |
//| Hedged Grid series, chapter 9 (extra): type 4, bracket squeeze    |
//| Start : buy stop at ask + InpHalfWidth and sell stop at           |
//|         bid - InpHalfWidth, both InpLot.                          |
//| Win   : close everything when the cycle result (closed profit of  |
//|         this cycle + floating profit incl. swap) reaches          |
//|         InpCycleTarget (a single fill that runs InpTakeProfit).   |
//| Hedged: when both sides are open and price runs InpTakeProfit     |
//|         beyond the winner's open price, close the winner and as   |
//|         much of the loser (0.01 steps) as keeps the two together  |
//|         at or above InpSqueezeGain.                               |
//| Squeeze: with only the loser left, keep an opposite stop of the   |
//|         same volume InpReBracket away from price, trailing it as  |
//|         price moves in the loser's favour. Lots never increase.   |
//| 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.10;   // Lot size (0.01 steps are needed for partial closes)
input double InpHalfWidth    = 2.5;    // Initial stops: distance from price (USD)
input double InpTakeProfit   = 5.0;    // Run beyond the winner's open price that triggers a close (USD)
input double InpReBracket    = 5.0;    // Distance of the re-bracket stop from price (USD)
input double InpCycleTarget  = 50.0;   // Close all when the cycle result reaches this (account currency)
input double InpSqueezeGain  = 10.0;   // Winner + closed part of the loser must reach this (account currency)
input double InpTrailMin     = 0.5;    // Move the re-bracket stop only when it improves by this (USD)
input long   InpMagic        = 260810; // Magic number
input string InpTag          = "ch09"; // Log file tag

CTrade trade;
double bal0 = 0;
bool   cycleOn = false, squeezed = false;

int OnInit()
{
   trade.SetExpertMagicNumber(InpMagic);
   trade.SetTypeFillingBySymbol(_Symbol);
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   HGLog_Deinit(InpTag);
}

struct Leg { ulong ticket; double vol, price, pnl; };

// Our open positions: at most one buy and one sell (lots never add up)
int Scan(Leg &buy, Leg &sell, double &floating)
{
   buy.ticket = 0; sell.ticket = 0; floating = 0;
   int n = 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++;
      double pnl = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
      floating += pnl;
      Leg l;
      l.ticket = t; l.vol = PositionGetDouble(POSITION_VOLUME);
      l.price = PositionGetDouble(POSITION_PRICE_OPEN); l.pnl = pnl;
      if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) buy = l; else sell = l;
   }
   return n;
}

// Our pending stops: returns the count, and the last one found
int Pending(ulong &ticket, double &price, double &vol, bool &isBuy)
{
   int n = 0; ticket = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      ulong t = OrderGetTicket(i);
      if(t == 0 || OrderGetString(ORDER_SYMBOL) != _Symbol
         || OrderGetInteger(ORDER_MAGIC) != InpMagic) continue;
      n++;
      ticket = t; price = OrderGetDouble(ORDER_PRICE_OPEN);
      vol = OrderGetDouble(ORDER_VOLUME_CURRENT);
      isBuy = OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_BUY_STOP;
   }
   return n;
}

void DeletePending()
{
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      ulong t = OrderGetTicket(i);
      if(t > 0 && OrderGetString(ORDER_SYMBOL) == _Symbol
         && OrderGetInteger(ORDER_MAGIC) == InpMagic) trade.OrderDelete(t);
   }
}

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);
   }
}

// Close the winner and the largest part of the loser that keeps the pair at or above InpSqueezeGain
void Squeeze(const Leg &win, const Leg &lose, const MqlTick &tick)
{
   double perStep = -lose.pnl / lose.vol * 0.01;          // loss of 0.01 lot of the loser
   int steps = 0;
   if(perStep > 0) steps = (int)MathFloor((win.pnl - InpSqueezeGain) / perStep);
   int maxSteps = (int)MathRound(lose.vol / 0.01);
   steps = MathMax(0, MathMin(steps, maxSteps));
   trade.PositionClose(win.ticket);
   if(steps >= maxSteps) trade.PositionClose(lose.ticket);
   else if(steps > 0) trade.PositionClosePartial(lose.ticket, NormalizeDouble(steps * 0.01, 2));
   PrintFormat("HG10 squeeze win=%.2f loser %.2f lot pnl=%.2f closed %d steps sp=%.2f",
               win.pnl, lose.vol, lose.pnl, steps, tick.ask - tick.bid);
   squeezed = true;
}

void OnTick()
{
   HGLog_OnTick();
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick)) return;

   Leg buy, sell;
   double floating;
   int n = Scan(buy, sell, floating);
   ulong pt; double pp, pv; bool pBuy;
   int np = Pending(pt, pp, pv, pBuy);

   if(cycleOn && n > 0 && AccountInfoDouble(ACCOUNT_BALANCE) - bal0 + floating >= InpCycleTarget)
   {
      CloseAll(); DeletePending(); cycleOn = false;
      return;
   }

   if(n == 0)
   {
      if(cycleOn && np > 0 && !squeezed) return;          // waiting for the first fill
      // New cycle (also when a squeeze closed the loser completely)
      DeletePending();
      bal0 = AccountInfoDouble(ACCOUNT_BALANCE); squeezed = false;
      string c = StringFormat("HG10 bracket sp=%.2f", tick.ask - tick.bid);
      bool ok1 = trade.BuyStop(InpLot, NormalizeDouble(tick.ask + InpHalfWidth, _Digits), _Symbol, 0, 0, ORDER_TIME_GTC, 0, c);
      bool ok2 = trade.SellStop(InpLot, NormalizeDouble(tick.bid - InpHalfWidth, _Digits), _Symbol, 0, 0, ORDER_TIME_GTC, 0, c);
      cycleOn = ok1 && ok2;
      if(!cycleOn) DeletePending();
      return;
   }

   if(buy.ticket > 0 && sell.ticket > 0)
   {
      if(np > 0) DeletePending();
      if(tick.bid >= buy.price + InpTakeProfit) Squeeze(buy, sell, tick);
      else if(tick.ask <= sell.price - InpTakeProfit) Squeeze(sell, buy, tick);
      return;
   }

   // One position left
   if(!squeezed) return;                                   // first fill: the other initial stop stays
   bool loserIsBuy = buy.ticket > 0;
   double vol = loserIsBuy ? buy.vol : sell.vol;
   string c = StringFormat("HG10 rebracket sp=%.2f", tick.ask - tick.bid);
   if(np > 0 && (MathAbs(pv - vol) > 1e-8 || pBuy == loserIsBuy)) { DeletePending(); np = 0; }
   if(loserIsBuy)
   {
      double want = NormalizeDouble(tick.bid - InpReBracket, _Digits);
      if(np == 0) trade.SellStop(vol, want, _Symbol, 0, 0, ORDER_TIME_GTC, 0, c);
      else if(want > pp + InpTrailMin) trade.OrderModify(pt, want, 0, 0, ORDER_TIME_GTC, 0);
   }
   else
   {
      double want = NormalizeDouble(tick.ask + InpReBracket, _Digits);
      if(np == 0) trade.BuyStop(vol, want, _Symbol, 0, 0, ORDER_TIME_GTC, 0, c);
      else if(want < pp - InpTrailMin) trade.OrderModify(pt, want, 0, 0, ORDER_TIME_GTC, 0);
   }
}

コメント

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