両建てグリッドEAをゼロから作る(第1回:単発エントリーで土台を作る)

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

第1回:まずは単発エントリーから

第0回で書いたとおり、この連載では⑤剥がし型を部品1つずつ組み立てます。最初は両建ても、ナンピンもまだ何も足しません。土台として、

  • エントリーは ADX(トレンドの強さを測る指標)で決める
  • 1回に持つポジションは 1つだけ
  • 利食い・損切りはどちらも 5.00ドル(価格差。0.01ロットだと1回 ±5ドル)

という一番シンプルな形で回します。負け確ですな。次の回から機能を足したとき、「足した分でどれだけ変わったか」を比べるための基準になります。なのですっ飛ばして第2回から読んでいいよ笑。

エントリーのルール

項目設定
1分足 ADX(14)22以上
30分足 ADX(14)22以上
向き両方の足で +DI > −DI なら買い、−DI > +DI なら売り(向きがそろわなければ見送り)
判定確定した足で判定。新規エントリーは1分足1本につき最大1回
決済利食い +5.00ドル/損切り −5.00ドル(価格差)

ほかのテクニカルとの比較は、だいぶ後ろの回でやります。

資産の推移

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

項目値
トレード数22,933回
勝率41.5%
平均利益/平均損失+5.18 / −5.29 USD
1トレードあたり平均−0.94 USD(95%信頼区間 −1.01〜−0.87)
プロフィットファクター0.70
損益合計−21,570 USD
最大DD(含み損込み)−21,590 USD(初期証拠金の43%)
同時に持った玉最大1本・0.01ロット
試行回数 N16
DSR0.000(N=100 換算でも 0.000)
資産の推移

右肩下がりの一本道です。まあ想定通り。1本しか持たないので含み損はほとんど出ず、確定した残高と含み損込みの線がほぼ重なっています。DDの深さは、そのまま負けの積み上げです。

2026年に入ってから下げが急になっています。理由は次の次の節で見ます。 

時間帯で避ければ勝てるか

年×時間帯別の損益

年と時間帯で分けても、全部のマスがマイナスです(2025年の14時だけほぼゼロ)。「この時間だけ避ければ勝てる」という場所はありません。 

なぜ負けるのか:負けはほぼスプレッド

利食いと損切りが同じ5ドルなのに、勝率は41.5%と半分を切っています。買った瞬間にスプレッド分の含み損から始まるので、利食いまでの距離が損切りまでの距離より長くなるからです。

では、負けのうちどれだけがスプレッドなのか。エントリーした瞬間のスプレッドを注文コメントに記録して数えました。

1トレードあたりの損益の内訳
  • 1トレードの平均 −0.94 のうち、スプレッド 0.79 と手数料 0.06 で 0.85 が説明できます
  • 残りの −0.09 が、エントリーの方向が当たったか外れたかの分です。ほぼゼロ、つまりADXのエントリーに優位性はないということです

四半期ごとに分けると、スプレッドが広い時期ほど負けが大きくなっています。

四半期トレード数Goldの平均価格平均スプレッド1トレード平均損益保有時間(中央値)
2025 Q19842,8730.47−0.8136分
2025 Q22,7463,2730.53−0.4810分
2025 Q31,3093,5090.38−0.4524分
2025 Q44,0904,1600.63−0.765分
2026 Q18,5374,8751.11−1.341.3分
2026 Q25,2674,4980.68−0.824分
全体22,9330.79−0.943.8分

(単位はUSD。0.01ロットなので価格差1.00ドル=1 USD。手数料はどの時期も0.06)

2026 Q1 はGoldが5,000ドル近くまで上がり、スプレッドも1ドルを超えました。利食い幅5ドルの2割以上がスプレッドで消えています。 しかも値動きが激しすぎて、保有時間の中央値は1.3分しかありません。5ドルの幅が今のGoldには狭すぎて、ほぼ「スプレッドを払ってコインを投げる」状態です。トレード数もこの四半期だけで全体の4割近くあり、資産曲線の急な下げはここから来ています。

第1回のまとめ

  • ADXだけのエントリー+固定5ドルの利食い・損切りは、コストの分だけ素直に負ける
  • エントリーそのものには、勝ち負けを分けるほどの力はない
  • この先は「エントリーで勝つ」のではなく、ポジションの持ち方で何とかできるかを試していくことになります 

次回

次回は損切りをやめて、逆行したら同じ量の反対玉を建てるロックにします。損切りしないで固定しておけば、あとで取り返せるのか?

試行回数とDSRについて

今回の試行回数は N=16 です。第0回に書いたとおり、連載を始める前の下調べで15回バックテストを回しているので、その続きから数えています。

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

DSRは「これだけ試したなら、たまたまこのくらいの成績は出る」という分を割り引いた評価で、0.95以上なら採用候補です。今回は成績がマイナスなので、割り引くまでもなく 0.000 です。 

ソースコード

研究用です。使う人おらんやろけど、載せておきます。

HG01_SingleEntry.mq5

//+------------------------------------------------------------------+
//| HG01_SingleEntry.mq5                                              |
//| Hedged Grid series, chapter 1: one position at a time.            |
//| Entry : ADX on M1 and M30 both above threshold, DI direction agrees|
//| Exit  : fixed take profit / stop loss (price distance in USD)      |
//| 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 (price distance, USD)
input double InpStopLoss     = 5.0;    // Stop loss (price distance, USD)
input long   InpMagic        = 260801; // Magic number
input string InpTag          = "ch01"; // 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;
}

bool HasPosition()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
      if(PositionGetTicket(i) > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol
         && PositionGetInteger(POSITION_MAGIC) == InpMagic) return true;
   return false;
}

void OnTick()
{
   HGLog_OnTick();
   if(HasPosition()) 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;

   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick)) return;
   bool ok;
   if(side == 1)
      ok = trade.Buy(InpLot, _Symbol, tick.ask,
                     NormalizeDouble(tick.ask - InpStopLoss, _Digits),
                     NormalizeDouble(tick.ask + InpTakeProfit, _Digits), StringFormat("HG01 buy sp=%.2f", tick.ask - tick.bid));
   else
      ok = trade.Sell(InpLot, _Symbol, tick.bid,
                      NormalizeDouble(tick.bid + InpStopLoss, _Digits),
                      NormalizeDouble(tick.bid - InpTakeProfit, _Digits), StringFormat("HG01 sell sp=%.2f", tick.ask - tick.bid));
   if(ok) lastEntryBar = bar;
}

hg_log.mqh(検証用のログ出力。売買には関係しません)

//+------------------------------------------------------------------+
//| hg_log.mqh  - backtest logger for the Hedged Grid blog series     |
//| Writes two CSV files to Terminal\Common\Files at OnDeinit:        |
//|   HG_<tag>_daily.csv : date,balance,equity,min_equity,max_pos,max_lots
//|   HG_<tag>_deals.csv : every deal in the history                   |
//| Logging only. It never changes trading decisions.                 |
//+------------------------------------------------------------------+
#property strict

string   hgDaily[];
datetime hgDay = 0;
double   hgMinEq = 0, hgMaxLots = 0;
int      hgMaxPos = 0;

void HGLog_FlushDay()
{
   if(hgDay == 0) return;
   int n = ArraySize(hgDaily);
   ArrayResize(hgDaily, n + 1);
   hgDaily[n] = StringFormat("%s,%.2f,%.2f,%.2f,%d,%.2f",
                             TimeToString(hgDay, TIME_DATE),
                             AccountInfoDouble(ACCOUNT_BALANCE),
                             AccountInfoDouble(ACCOUNT_EQUITY),
                             hgMinEq, hgMaxPos, hgMaxLots);
}

void HGLog_OnTick()
{
   datetime d = iTime(_Symbol, PERIOD_D1, 0);
   double eq = AccountInfoDouble(ACCOUNT_EQUITY);
   if(d != hgDay)
   {
      HGLog_FlushDay();
      hgDay = d; hgMinEq = eq; hgMaxPos = 0; hgMaxLots = 0;
   }
   if(eq < hgMinEq) hgMinEq = eq;
   int n = PositionsTotal();
   double lots = 0;
   for(int i = 0; i < n; i++)
      if(PositionGetTicket(i) > 0) lots += PositionGetDouble(POSITION_VOLUME);
   if(n > hgMaxPos) hgMaxPos = n;
   if(lots > hgMaxLots) hgMaxLots = lots;
}

void HGLog_Deinit(const string tag)
{
   HGLog_FlushDay();
   int h = FileOpen("HG_" + tag + "_daily.csv", FILE_WRITE | FILE_ANSI | FILE_COMMON | FILE_TXT);
   if(h != INVALID_HANDLE)
   {
      FileWriteString(h, "date,balance,equity,min_equity,max_pos,max_lots\r\n");
      for(int i = 0; i < ArraySize(hgDaily); i++) FileWriteString(h, hgDaily[i] + "\r\n");
      FileClose(h);
   }
   HistorySelect(0, TimeCurrent() + 86400);
   h = FileOpen("HG_" + tag + "_deals.csv", FILE_WRITE | FILE_ANSI | FILE_COMMON | FILE_TXT);
   if(h == INVALID_HANDLE) return;
   FileWriteString(h, "time,position,type,entry,volume,price,profit,commission,swap,comment\r\n");
   int total = HistoryDealsTotal();
   for(int i = 0; i < total; i++)
   {
      ulong t = HistoryDealGetTicket(i);
      long type = HistoryDealGetInteger(t, DEAL_TYPE);
      if(type != DEAL_TYPE_BUY && type != DEAL_TYPE_SELL) continue;
      FileWriteString(h, StringFormat("%s,%I64d,%s,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%s\r\n",
         TimeToString((datetime)HistoryDealGetInteger(t, DEAL_TIME), TIME_DATE | TIME_SECONDS),
         HistoryDealGetInteger(t, DEAL_POSITION_ID),
         type == DEAL_TYPE_BUY ? "buy" : "sell",
         (int)HistoryDealGetInteger(t, DEAL_ENTRY),
         HistoryDealGetDouble(t, DEAL_VOLUME),
         HistoryDealGetDouble(t, DEAL_PRICE),
         HistoryDealGetDouble(t, DEAL_PROFIT),
         HistoryDealGetDouble(t, DEAL_COMMISSION),
         HistoryDealGetDouble(t, DEAL_SWAP),
         HistoryDealGetString(t, DEAL_COMMENT)));
   }
   FileClose(h);
}

コメント

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