両建てグリッドEAをゼロから作る(第2回:損切りのかわりに両建てロック)

この連載は研究用です。 読み物として楽しんでもらえれば。あと、今回もあまり面白くないですw今回は部品づくり。

前回のおさらい

第1回(ADXで1本だけ入り、±5ドルで決済)は、1トレードあたりコスト分きっちり負けました。エントリーの方向に優位性がないので、単発のポジションをどう決済しても平均はマイナスのままです。

いよいよ両建てに入ります。損切りするかわりに、反対方向に同じ量を建てて損益を固定(ロック)し、相場が戻るのを待ちます。第0回の6つの型でいうと、②ロック型です。 

第2回:ロックして、建値に戻るのを待つ

項目設定
エントリー第1回と同じ(ADX)
利食い最初のポジションに +5.00ドル(第1回と同じ)
損切りなし
ロック最初のポジションから 5.00ドル逆行したら、反対方向に同じ0.01ロットを建てる
ロックの解除最初のポジションの建値まで価格が戻ったら、2本ともまとめて決済

ロックした時点で、価格の損益は「−5ドル+スプレッド」で固定されます。そこから建値に戻れば、最初のポジションはトントン、ロックしたほうは −5ドル。結局、第1回で5ドル損切りしたのと同じ結果になるはずです。戻らなければ、2本を持ったまま待ち続けます。

ロックの2つの結末

ここまでは、計算すれば分かること。それでもわざわざ回すのには、理由が2つありまして。

1つ目は、ロックが剥がし型の最初の部品だからです。「5ドル逆行したら、反対に同じ量を建てる」というこのルールは、第3回以降のEAにもそのまま入ります。このあと部品を1つずつ足していくので、足すたびに何が変わったのかを見分けられるように、まずロックだけを入れた形を見ておきます。ほどき方(建値に戻ったら2本とも決済)は、いちばん素朴なものにしました。ここは第3回で別のやり方に置きかえます。

2つ目は、計算では分からないことがあるからです。建値に戻らなかったら、どうなるのか。戻るのを待っているあいだに、何が起きるのか。これは実際に回してみないと分かりません。 

結果:最初の2週間で止まった

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

項目値
1サイクル(エントリーから決済まで)25回
うち利食い12回(平均 +4.91 USD)
うちロック13回
ロックして建値に戻ったサイクルの損益12回とも −5.3〜−7.4 USD(平均 約 −6.0)
最後のロック2025-01-13 22:51(売り 2,657.37 / ロックの買い 2,662.37)
その後2026-06-30 のテスト終了まで、一度も建値に戻らず
損益合計−312.51 USD
最大DD(含み損込み)−428 USD(初期証拠金の0.9%)
同時に持った玉最大2本・0.02ロット
試行回数 N18
DSR0.000
資産の推移

確定した残高(黒)は、2025年1月の半ばから最終日まで真横です。そのあいだ一度も決済が起きていません。ロックの売りを建てたのはGoldが2,657ドルのとき。その後、Goldは4,000ドル台まで一方的に上がり、売りの建値へ戻る日は来ませんでした。EAは1年5か月、何もできずに固まりました。 最終日の段差は、テスト終了で2本が強制的に閉じられた分です。

含み損込みの線(灰色)は、そのあいだずっと右下がりです。理由は2つ先の節で見ます。 

予想どおりだったところ

建値に戻れたロック12回は、どれも −5.3〜−7.4ドル で終わりました。

  • 狙いどおり、ほぼ「5ドル損切り」と同じ
  • そのうえで、ロックを建てるときと外すときにスプレッドを余計に払うので、第1回の損切り(平均 −5.29)より少し悪い
  • 値が飛んだときはロックの位置も滑る(1回はスプレッドが3.73ドルまで開いた瞬間にロックしていました)

ロックは損切りを先送りしているだけで、損は消えません。 しかもコストが増えます。 

予想していなかったところ:待つだけでお金が減る

いや、予想はしてたけど、意外に影響デカいなと思いまして笑。固まっていた1年5か月、含み損込みの線はなだらかに下がり続けました。原因はスワップです。

固まった2本のスワップ

釈迦説(釈迦に説法)ですが、売りと買いを同じ量持っていても、スワップは相殺されません。このブローカーのGoldは、買いで払う額が売りで受け取る額よりずっと大きいので、ロックしているだけで1日あたり0.5ドルほど減っていきます。

損益が「固定される」と言っても、それは価格の損益だけです。時間がたつほど、スワップで少しずつ減っていきます。

(ところどころ下に飛び出しているヒゲは、早朝などスプレッドが一時的に開いた瞬間の含み損です。2本持っていると、スプレッドが開いた分だけ含み損が膨らみます。) 

第2回のまとめ

  • ロックして建値に戻るのを待つのは、損切りと同じ結果になり、コストは損切りより多い
  • 戻らなければ永遠に固まり、そのあいだスワップを払い続ける
  • 「待てばいつか戻る」は、強いトレンドの前では成り立たない
  • ロックはこのあとの回のEAにもそのまま入るので、「戻らなければ固まる」「待つあいだスワップで減る」は、この先もついて回る 

次回

固まったロックをどうほどくか。次回は、価格が抜けていった方向へ玉を追加し、全体の損益がプラスになったらまとめて決済する仕組みを足します。トレンドが出ればロックがほどけるはず。ただし玉の数は増えます。 

試行回数について

今回は N=18 です。第1回(N=16)のあとに、記事にはしていませんが「単発のトレーリング」(利が乗ったら決済ラインを引き上げる)を1回試していて、その分も数えています(結果は第1回と同じくコスト分の負けでした)。

記事にする前に、同じEAを別の初期証拠金で回したことがあります。ロットが固定なのでトレードは1本残らず同じになり、試行としては同じ1回と数えています(ロジックは一切変えなくても、証拠金を変えたなら試行回数は+1となります)。 

ソースコード

研究用です。これも使う人はいないと思うけど載せておきます。ログ出力の hg_log.mqh は第1回と同じです。ファイル名の番号(HG03)は作った順の通し番号で、回の番号とはずれています。

HG03_Lock.mq5

//+------------------------------------------------------------------+
//| HG03_Lock.mq5                                                     |
//| Hedged Grid series, chapter 2: hedge lock instead of stop loss.   |
//| Entry : same as HG01 (ADX on M1 and M30, DI direction agrees)     |
//| Exit  : take profit on the first position; no stop loss.          |
//|         If price moves InpLockDistance against it, open the same  |
//|         size in the opposite direction (lock). When price comes   |
//|         back to the first position's entry price, close both.     |
//| 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 the first position (USD)
input double InpLockDistance = 5.0;    // Adverse move that triggers the lock (USD)
input long   InpMagic        = 260803; // Magic number
input string InpTag          = "ch03"; // 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;
}

// Collect our tickets. The first position is the one with a take profit; the lock has none.
int Collect(ulong &first, ulong &lock)
{
   first = 0; lock = 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++;
      if(PositionGetDouble(POSITION_TP) > 0) first = t; else lock = t;
   }
   return n;
}

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

   ulong first, lock;
   int n = Collect(first, lock);
   if(n > 0)
   {
      if(first == 0)
      {
         // Should not happen (lock left alone); close it so the EA never gets stuck
         if(lock > 0) trade.PositionClose(lock);
         return;
      }
      PositionSelectByTicket(first);
      bool   isBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
      double open  = PositionGetDouble(POSITION_PRICE_OPEN);
      if(lock == 0)
      {
         // Not locked yet: lock when price moved InpLockDistance against the first position
         if(isBuy && tick.bid <= open - InpLockDistance)
            trade.Sell(InpLot, _Symbol, tick.bid, 0, 0, StringFormat("HG03 lock sell sp=%.2f", tick.ask - tick.bid));
         if(!isBuy && tick.ask >= open + InpLockDistance)
            trade.Buy(InpLot, _Symbol, tick.ask, 0, 0, StringFormat("HG03 lock buy sp=%.2f", tick.ask - tick.bid));
      }
      else
      {
         // Locked: wait until the first position is back at its entry price, then close both
         if((isBuy && tick.bid >= open) || (!isBuy && tick.ask <= open))
         {
            trade.PositionClose(first);
            trade.PositionClose(lock);
         }
      }
      return;
   }

   // 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),
                     StringFormat("HG03 buy sp=%.2f", tick.ask - tick.bid));
   else
      ok = trade.Sell(InpLot, _Symbol, tick.bid, 0,
                      NormalizeDouble(tick.bid - InpTakeProfit, _Digits),
                      StringFormat("HG03 sell sp=%.2f", tick.ask - tick.bid));
   if(ok) lastEntryBar = bar;
}

コメント

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