EAの勝ち負けを“資金曲線”で制御する:エクイティカーブ・トレーディング(ECT)実践ガイド[MQL4完全版・初心者向け]

EA

要点:エクイティカーブ・トレーディング(Equity Curve Trading, ECT)は、EAのオン/オフやポジションサイズを「自分の資金曲線(エクイティ)」で制御するメタ戦略です。勝ちやすい局面だけを抽出し、ドローダウンを浅くし、回復を早めることを目的とします。本稿は、初心者が今日から実装できるように、概念→設計→MQL4完全コード→バックテスト→チューニング→運用までを一気通貫で解説します。

この記事は売買助言ではありません。実運用前に必ずデモ検証・小ロット検証を行ってください。

1. エクイティカーブ・トレーディングとは何か

通常のEAは、相場から生成したシグナル(例:移動平均クロス、ブレイクアウト、RSI等)にもとづいて売買します。ECTはその一段上位のレイヤで、EAの結果(損益)から得られる資金曲線を監視し、「いま勝ち運のモードか」「負けモードか」を自分の成績で判断し、取引を制御します。

  • フィルタ型:資金曲線が平滑線の上なら取引許可、下なら休止。
  • スロットル型:好調ならロット増、低調ならロット減。
  • ブレーカー型:連敗や一定のドローダウンで強制クールダウン。

発想は単純ですが、過剰取引や連敗の深掘りを抑え、破綻確率を下げる効果が期待できます。

2. 初心者でも分かる最小実装:移動平均フィルタ

最小構成は以下の3要素だけです。

  1. エクイティ系列:取引ごとに累積損益(または Balance/Equity)を記録。
  2. 平滑線:エクイティの単純移動平均(例:MA(50))。
  3. ルール:Equity > MA を満たすときだけ新規エントリーを許可。

この「好調時のみ取る」だけで、無駄なトレードが削られ、カーブの素直さが改善しやすくなります。

3. 設計の全体像:パラメータとデータの流れ

本稿で実装するEAは、ベース戦略(移動平均クロス)+ECTガードの二層構造です。

  • ベース戦略:短期MAと長期MAのクロスで売買(初心者に分かりやすい)。
  • ECTガード:エクイティMAフィルタ、Zスコア判定、日次ドローダウン制限を装備。
  • サイズ制御:ATRを用いたボラティリティ正規化ロット。
  • リスク管理:SL/TP、トレイリング、ブレークイーブン移動、1日の損失上限。
  • 運用保護:スプレッド監視、ニュース時間帯の休止(手動/時刻ベース)。

4. まずは結果のイメージ:ECTで何が変わるか

典型例として、同一のベース戦略を「ECTなし」と「ECTあり」でバックテストすると、多くのケースで以下の傾向が見られます。

  • 最大ドローダウンの縮小:エクイティMAを下抜く低調期を休止できるため。
  • リカバリー時間の短縮:負け期間の新規が減るため山谷が浅くなる。
  • トレード回数の減少:無駄打ちが減る。結果として手数料/スリッページの総量も削減。
  • PFやSQNの改善:質の良いトレードの比率が上がる。

一方で、フルベット時の理論上の最大利益は削られる可能性があります。目的は「無駄な損失の抑制と生存性の向上」です。

5. 実装:MQL4 完全コード(ベース戦略+ECTガード)

以下は、USDJPY(5分足想定)で動作するデモEAです。ブローカー仕様(銘柄名サフィックス、最小ロット、ストップレベル等)に合わせて調整してください。


//+------------------------------------------------------------------+
//|  ECT_MA_Guard_EA.mq4                                            |
//|  Equity Curve Trading (ECT) Guard + MA Crossover Base Strategy  |
//|  For educational use. Test on demo first.                       |
//+------------------------------------------------------------------+
#property strict

// ---- 外部パラメータ
extern string   Base__ = "=== Base Strategy (MA Cross) ===";
extern int      FastMAPeriod = 20;
extern int      SlowMAPeriod = 50;
extern int      MaShift      = 0;

extern string   Risk__ = "=== Risk & Money ===";
extern double   RiskPerTradePercent = 1.0;  // 口座残高に対する%
extern double   MaxDailyLossPercent = 3.0;  // 1日の損失上限(%)
extern double   StopATRMult  = 2.5;         // ATR倍率のSL
extern double   TakeATRMult  = 5.0;         // ATR倍率のTP
extern int      ATRPeriod    = 14;

extern string   ECT__ = "=== ECT Guard ===";
extern int      EquityMAPeriod = 50;        // エクイティ移動平均の期間
extern double   ZScoreEntryThr = 0.0;       // Zスコアでの追加フィルタ(0で無効)
extern int      MinTradesForECT = 30;       // ECT有効化の最小取引数(初期学習)

extern string   Exec__ = "=== Execution ===";
extern int      Magic      = 20250905;
extern int      Slippage   = 3;
extern double   MaxSpread  = 2.5;           // pips
extern bool     UseTrailing= true;
extern double   TrailATRMult= 1.5;
extern bool     MoveToBE   = true;
extern double   BE_ATRMult = 1.0;

extern string   Time__ = "=== Time Filter (Broker Time) ===";
extern bool     UseTimeFilter = false;
extern int      StartHour = 0;
extern int      EndHour   = 24;

// ---- 内部変数
double g_equityMA = 0.0;
double g_equity   = 0.0;
double g_equityStd= 0.0;
int    g_totalTrades = 0;
datetime g_day = 0;
double g_dailyPnL = 0.0;

// ---- ユーティリティ
double PipValue()
{
   double p = MarketInfo(Symbol(), MODE_POINT);
   if(Digits == 3 || Digits == 5) return p*10.0;
   return p;
}

double GetSpreadPips()
{
   return (MarketInfo(Symbol(), MODE_SPREAD) * PipValue())/PipValue();
}

bool TimeAllowed()
{
   if(!UseTimeFilter) return true;
   int h = TimeHour(TimeCurrent());
   if(StartHour <= EndHour) return (h >= StartHour && h < EndHour);
   // 24h wrap
   return (h >= StartHour || h < EndHour);
}

// ---- ATR計算
double ATR(int period)
{
   return iATR(Symbol(), PERIOD_CURRENT, period, 0);
}

// ---- MA クロスシグナル
int BaseSignal()
{
   double fast = iMA(Symbol(), PERIOD_CURRENT, FastMAPeriod, MaShift, MODE_SMA, PRICE_CLOSE, 0);
   double slow = iMA(Symbol(), PERIOD_CURRENT, SlowMAPeriod, MaShift, MODE_SMA, PRICE_CLOSE, 0);
   double fast1= iMA(Symbol(), PERIOD_CURRENT, FastMAPeriod, MaShift, MODE_SMA, PRICE_CLOSE, 1);
   double slow1= iMA(Symbol(), PERIOD_CURRENT, SlowMAPeriod, MaShift, MODE_SMA, PRICE_CLOSE, 1);

   if(fast1 <= slow1 && fast > slow) return 1;   // ゴールデンクロス → Buy
   if(fast1 >= slow1 && fast < slow) return -1;  // デッドクロス   → Sell
   return 0;
}

// ---- ロット計算(ATRでボラ正規化)
double CalcLots(double atr, double stopATRMult)
{
   if(atr <= 0) return 0.01;
   double stopPips = (stopATRMult * atr) / PipValue();
   if(stopPips <= 0.0) stopPips = 10.0;
   double riskMoney = AccountBalance() * (RiskPerTradePercent/100.0);
   double lotPerPipValue = MarketInfo(Symbol(), MODE_TICKVALUE) / PipValue();
   double lots = riskMoney / (stopPips * lotPerPipValue);
   double minLot = MarketInfo(Symbol(), MODE_MINLOT);
   double lotStep= MarketInfo(Symbol(), MODE_LOTSTEP);
   lots = MathMax(minLot, MathFloor(lots/lotStep)*lotStep);
   return lots;
}

// ---- ECT: エクイティ系列の更新とMA/Zスコア算出
void UpdateECTMetrics()
{
   // 1日損益トラッキング
   datetime today = DateOfDay(TimeCurrent());
   if(g_day != today){ g_day = today; g_dailyPnL = 0.0; }

   double balance = AccountBalance();
   double equity  = AccountEquity();
   g_equity = equity;

   // トレード数
   g_totalTrades = OrdersHistoryTotal();

   // 簡易移動平均と標準偏差(過去N本の履歴ベースで簡易算出)
   // ここでは履歴のクローズ損益から擬似的に系列を生成
   int n = MathMin(EquityMAPeriod, g_totalTrades);
   if(n <= 1){ g_equityMA = balance; g_equityStd = 0; return; }

   double sum=0, sum2=0;
   double eq = 0;
   int counted = 0;
   for(int i=0; i<OrdersHistoryTotal(); i++)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
      {
         if(OrderSymbol() == Symbol() && OrderMagicNumber()==Magic)
         {
            eq += OrderProfit()+OrderSwap()+OrderCommission();
            counted++;
            sum  += eq;
            sum2 += eq*eq;
            if(counted>=n) break;
         }
      }
   }
   if(counted <= 1){ g_equityMA = balance; g_equityStd=0; return; }
   double mean = sum / counted;
   double var  = MathMax(0.0, (sum2/counted) - mean*mean);
   g_equityMA  = balance + mean;   // バランスに履歴損益の平均を足した近似
   g_equityStd = MathSqrt(var);
}

// ---- ECT許可判定
bool ECT_AllowsEntry()
{
   if(g_totalTrades < MinTradesForECT) return true; // 初期学習期間は通す

   bool aboveMA = (g_equity >= g_equityMA);
   if(!aboveMA) return false;

   if(ZScoreEntryThr > 0 && g_equityStd > 0)
   {
      double z = (g_equity - g_equityMA) / g_equityStd;
      if(z < ZScoreEntryThr) return false;
   }
   return true;
}

// ---- 日次ドローダウン制限
bool DailyLossLimitHit()
{
   // 当日履歴を集計
   double todaysPnL = 0.0;
   for(int i=0; i<OrdersHistoryTotal(); i++)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
      {
         if(OrderSymbol()==Symbol() && OrderMagicNumber()==Magic)
         {
            datetime ct = OrderCloseTime();
            if(TimeDay(ct)==TimeDay(TimeCurrent()) && TimeMonth(ct)==TimeMonth(TimeCurrent()) && TimeYear(ct)==TimeYear(TimeCurrent()))
            {
               todaysPnL += (OrderProfit()+OrderSwap()+OrderCommission());
            }
         }
      }
   }
   double limit = - AccountBalance() * (MaxDailyLossPercent/100.0);
   return (todaysPnL <= limit);
}

// ---- 既存ポジ管理:トレイリング/BE
void ManageOpenPositions(double atr)
{
   for(int i=0; i<OrdersTotal(); i++)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol()!=Symbol() || OrderMagicNumber()!=Magic) continue;

      double point = MarketInfo(Symbol(), MODE_POINT);
      double dist  = TrailATRMult*atr;

      // Move to BE
      if(MoveToBE)
      {
         if(OrderType()==OP_BUY)
         {
            double be = OrderOpenPrice();
            if(Bid - be >= BE_ATRMult*atr && OrderStopLoss() < be)
               OrderModify(OrderTicket(), OrderOpenPrice(), be, OrderTakeProfit(), 0, clrBlue);
         }
         else if(OrderType()==OP_SELL)
         {
            double be = OrderOpenPrice();
            if(be - Ask >= BE_ATRMult*atr && (OrderStopLoss()==0 || OrderStopLoss() > be))
               OrderModify(OrderTicket(), OrderOpenPrice(), be, OrderTakeProfit(), 0, clrRed);
         }
      }

      if(!UseTrailing) continue;
      if(OrderType()==OP_BUY)
      {
         double newSL = Bid - dist;
         if(newSL > OrderStopLoss())
            OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrBlue);
      }
      else if(OrderType()==OP_SELL)
      {
         double newSL = Ask + dist;
         if(OrderStopLoss()==0 || newSL < OrderStopLoss())
            OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrRed);
      }
   }
}

// ---- エントリー
void TryEntry()
{
   if(!TimeAllowed()) return;
   if(DailyLossLimitHit()) return;
   if(GetSpreadPips() > MaxSpread) return;

   int sig = BaseSignal();
   if(sig==0) return;

   UpdateECTMetrics();
   if(!ECT_AllowsEntry()) return;

   double atr = ATR(ATRPeriod);
   double lots= CalcLots(atr, StopATRMult);
   if(lots <= 0) return;

   double sl=0,tp=0;
   double distSL = StopATRMult*atr;
   double distTP = TakeATRMult*atr;
   int    slip   = Slippage;

   if(sig>0)
   {
      sl = Bid - distSL;
      tp = Bid + distTP;
      OrderSend(Symbol(), OP_BUY, lots, Ask, slip, sl, tp, "ECT Buy", Magic, 0, clrBlue);
   }
   else if(sig<0)
   {
      sl = Ask + distSL;
      tp = Ask - distTP;
      OrderSend(Symbol(), OP_SELL, lots, Bid, slip, sl, tp, "ECT Sell", Magic, 0, clrRed);
   }
}

// ---- ライフサイクル
int OnInit(){ g_day=DateOfDay(TimeCurrent()); return(INIT_SUCCEEDED); }
int OnDeinit(){ return(0); }
void OnTick()
{
   double atr = ATR(ATRPeriod);
   ManageOpenPositions(atr);
   TryEntry();
}
//+------------------------------------------------------------------+
    

注意:学習用の簡易ECTです。実運用ではジャーナル出力、例外処理、銘柄/口座仕様への適合、スプレッド単位の確認、サーバ時刻と夏時間差の調整など、堅牢化を行ってください。

6. バックテストのやり方(MT4)

  1. データ品質:可能なら高精度ティックデータ(90%超)。低品質だとECTのON/OFFタイミングが再現しにくくなります。
  2. 期間:最低3〜5年、ボラ急変期(コロナ、介入期等)を含める。
  3. 銘柄:USDJPYやEURUSDなどスプレッドが狭い主要通貨から。
  4. パラメータ:MA(20/50)やEquityMA(50)は起点。感度は後述のグリッドで最適化。
  5. 評価指標:最大DD、プロフィットファクター(PF)、勝率、SQN、MAR、Ulcer Indexなど。
  6. 分解:「ECTなし」と「ECTあり」を同条件で比較、差分の原因を特定。

7. 推奨チューニング手順

  1. ベース戦略の健全性確認:ECTは万能ではありません。ベースが完全に負け戦略なら休止が増えるだけです。
  2. EquityMAの期間:短すぎると頻繁にON/OFFし約定コストが増えます。長すぎると反応が鈍くなります(例:30〜100で探索)。
  3. Zスコア閾値:0〜1.0程度で微調整。過度に上げると機会損失が増えます。
  4. 日次損失上限:2〜5%の範囲で口座・心的耐性に合わせて設定。
  5. ロット算出:ATRに加え、口座余力や銘柄の最小ロット・ストップレベルを考慮。

8. 典型的な落とし穴

  • 過学習:EquityMAやZ閾値を最適化しすぎると将来効かない。必ずアウトオブサンプルを用意してください。
  • ブローカー差:スプレッド、小数桁、最小ストップ距離、約定仕様で結果が変わります。
  • コスト軽視:トレード回数が減る一方、ON/OFFの切替で機会を逃すコストも生じます。
  • 記録不足:日別損益、連敗数、フィルタ判定をログに残し、後から原因分析できるように。

9. バリエーション(上級)

  • 二値→連続制御:MA上なら1.0、下なら0.0ではなく、距離やZスコアで0〜1の重みを与えロットを連続調整。
  • レジーム連動:VIXや金利、セッション別ボラ等の外生変数とエクイティ情報を融合。
  • 複数戦略のメタPF:各EAのエクイティを合成し、ポートフォリオ全体のオン/オフを制御。
  • カーブフィット抑制:ウォークフォワード、クロスバリデーションをECT閾値にも適用。

10. 検証チェックリスト

  • ECTなし/ありを同一条件で比較し、DD・PF・回復日数の差分を確認。
  • EquityMA期間とZ閾値を粗グリッド→微調整の順で探索。
  • エントリー許可・拒否の理由をログで追跡し、過度な拒否や偏りがないかを確認。
  • 手動でも一時停止できるUI(グローバル変数やパネル)を備える。

11. よくある質問(FAQ)

Q1:完全に負け期を避けられますか?
A:いいえ。ECTは期待値を押し上げる魔法ではありません。損失の深掘りを抑え、生存性を高める補助輪です。

Q2:どの銘柄が向いていますか?
A:スプレッドが狭く、取引時間が長いメジャー通貨が初学者向けです。株価指数CFDや金も検証可能ですが、仕様差に注意してください。

Q3:最初からECTをONにすべき?
A:過去の履歴がないと判定が不安定です。最初の一定トレードまでは通し、十分な系列ができてからECTを厳格化する運用が無難です。

12. まとめ

ECTは、「自分の成績で自分を制御する」シンプルなメタ戦略です。勝ちやすいときに賭け、負けやすいときは退く——この規律が、長期の生存率を上げます。まずは本稿のEAをデモで動かし、ECTのオン/オフ差分を自分の目で確認してください。

コメント

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