构建K线图趋势约束模型(第九部分):多策略EA(第一部分)(基础篇)
📘

构建K线图趋势约束模型(第九部分):多策略EA(第一部分)(基础篇)

第 1/3 篇

「多策略EA的框架起点」

这一节是系列第九部分的开篇,定位在把七种已知策略塞进同一个EA骨架里跑,而不是新写一套信号逻辑。原文列了总览、魔法数字引入、趋势跟踪落地、回测与结论五个块,说明工程重点是‘多策略共存’而非‘单策略优化’。 魔法数字(magic number)在这里是关键隔离手段:不同子策略的下单用不同 magic 值标记,便于MT5后台按策略统计胜率与回撤,也避免平仓时误杀其他策略的持仓。对外汇与贵金属来说,多策略同账户运行会放大隔夜利息与滑点风险,实盘前必须用策略测试器分 magic 跑一遍。 作者给出的基础数据点:该文发布于2025年6月16日,原文交互量820次、评论0条,说明这套多策略框架当时还未经过社区充分质疑,直接照搬需自行验证。

日线定方向,小周期找切入点

做趋势约束型 EA,第一性原则是把仓位方向锁在 D1 日 K 线的主流情绪上。日线收阳(close > open)视为多头日,收阴(close < open)视为空头日,这是上层约束;真正下单信号则下沉到 M1 这类小周期,等 RSI 撞到极端值再触发,初版逻辑刻意不做复杂堆叠,方便手动跟代码。 回头看已发布的约束片段,判断函数非常直白:用 iOpen 和 iClose 抓当前日线的开收价,比大小即得 bool 标志。下面这段就是日线多空判定的最小可用核,建议直接开 MT5 新建脚本粘进去跑一遍验证。

MQL5 / C++
class="type">void OnTick()
{
  class=class="str">"cmt">// Determine current daily trend(bullish )
  class="type">class="kw">double daily_open = iOpen(_Symbol, PERIOD_D1, class="num">0);
  class="type">class="kw">double daily_close = iClose(_Symbol, PERIOD_D1, class="num">0);
  class="type">bool is_bullish = daily_close > daily_open;
}
class="type">void OnTick()
{
  class=class="str">"cmt">// Determine current daily trend(bearish)
  class="type">class="kw">double daily_open = iOpen(_Symbol, PERIOD_D1, class="num">0);
  class="type">class="kw">double daily_close = iClose(_Symbol, PERIOD_D1, class="num">0);
  class="type">bool is_bearish = daily_close < daily_open;
}
if (is_bullish)
{
  class=class="str">"cmt">// Logic for bullish trend
  Print("The daily trend is bullish.");
  class=class="str">"cmt">// You can add your trading logic here, for example:
  class=class="str">"cmt">// if (OrderSelect(...)) { class=class="str">"cmt">/* Place buy order */ }
}
if (is_bearish)
{
  class=class="str">"cmt">// Logic for bearish trend
  Print("The daily trend is bearish.");
  class=class="str">"cmt">// You can add your trading logic here, for example:
  class=class="str">"cmt">// if (OrderSelect(...)) { class=class="str">"cmt">/* Place sell order */ }
}
class="type">long position_type = PositionGetInteger(POSITION_TYPE);
逐行拆一下:两个 OnTick 分别算日线多/空,daily_open 取 D1 第 0 根 K 线开盘价,daily_close 取同根收盘价;is_bullish 与 is_bearish 就是约束开关。下方 if 块里先 Print 打日志占位,注释行提示你可接 OrderSelect 下单。注意 position_type 已改成 long,这是修掉编译警告的关键——原代码用 int 在 78 行 28 列报类型不匹配,换 long 即消。 外汇与贵金属杠杆高、滑点跳空频繁,日线约束只过滤方向,不保证小周期信号必赢。下一阶段会把几位“市场之父”的著名策略拆进模块,并引入 magic number 做单号隔离,届时现有 RSI+约束框架的局限也会一并摊开。

MQL5 / C++
class="type">void OnTick()
{
  class=class="str">"cmt">// Determine current daily trend(bullish )
  class="type">class="kw">double daily_open = iOpen(_Symbol, PERIOD_D1, class="num">0);
  class="type">class="kw">double daily_close = iClose(_Symbol, PERIOD_D1, class="num">0);
  class="type">bool is_bullish = daily_close > daily_open;
}
class="type">void OnTick()
{
  class=class="str">"cmt">// Determine current daily trend(bearish)
  class="type">class="kw">double daily_open = iOpen(_Symbol, PERIOD_D1, class="num">0);
  class="type">class="kw">double daily_close = iClose(_Symbol, PERIOD_D1, class="num">0);
  class="type">bool is_bearish = daily_close < daily_open;
}
if (is_bullish)
{
  class=class="str">"cmt">// Logic for bullish trend
  Print("The daily trend is bullish.");
  class=class="str">"cmt">// You can add your trading logic here, for example:
  class=class="str">"cmt">// if (OrderSelect(...)) { class=class="str">"cmt">/* Place buy order */ }
}
if (is_bearish)
{
  class=class="str">"cmt">// Logic for bearish trend
  Print("The daily trend is bearish.");
  class=class="str">"cmt">// You can add your trading logic here, for example:
  class=class="str">"cmt">// if (OrderSelect(...)) { class=class="str">"cmt">/* Place sell order */ }
}
class="type">long position_type = PositionGetInteger(POSITION_TYPE);

◍ 把参数和句柄先钉死

EA 跑起来之前,先把外部可调参数摊开:超买 70、超卖 30 是 RSI 的经典边界,手数 0.1 属于轻仓试错,止损 100 点、止盈 200 点构成 1:2 的风险回报比,追踪止损 50 点用于在趋势延续时锁利。外汇与贵金属波动剧烈,这类固定点数止损在跳空时可能直接穿损,实盘前须在 MT5 策略测试器按品种点值重算。 初始化阶段只做一件事:用 iRSI 给当前品种、当前周期、收盘价算出的 RSI 建句柄。句柄若返回 INVALID_HANDLE,说明指标创建失败,OnInit 直接返回 INIT_FAILED 阻止 EA 挂载;正常则返回 INIT_SUCCEEDED。 退出时别漏了 IndicatorRelease 释放句柄,否则多次加载脚本会堆积内存占用。OnTick 里先抓日线开盘价与收盘价判断当日方向——收高于开视为偏多,收低于开视为偏空,再 CopyBuffer 取最新一根 RSI 值,失败就打印并 return,不让脏数据触发下单。

MQL5 / C++
class="kw">input class="type">class="kw">double RSI_Overbought = class="num">70.0;        class=class="str">"cmt">// RSI overbought level
class="kw">input class="type">class="kw">double RSI_Oversold = class="num">30.0;          class=class="str">"cmt">// RSI oversold level
class="kw">input class="type">class="kw">double Lots = class="num">0.1;                   class=class="str">"cmt">// Lot size
class="kw">input class="type">class="kw">double StopLoss = class="num">100;               class=class="str">"cmt">// Stop Loss in points
class="kw">input class="type">class="kw">double TakeProfit = class="num">200;             class=class="str">"cmt">// Take Profit in points
class="kw">input class="type">class="kw">double TrailingStop = class="num">50;            class=class="str">"cmt">// Trailing Stop in points
class=class="str">"cmt">// Global variables
class="type">class="kw">double rsi_value;
class="type">int rsi_handle;
CTrade trade;   class=class="str">"cmt">// Declare an instance of the CTrade class
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   class=class="str">"cmt">// Create an RSI indicator handle
   rsi_handle = iRSI(_Symbol, PERIOD_CURRENT, RSI_Period, PRICE_CLOSE);
   if (rsi_handle == INVALID_HANDLE)
     {
      Print("Failed to create RSI indicator handle");
      class="kw">return(INIT_FAILED);
     }
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   class=class="str">"cmt">// Release the RSI indicator handle
   IndicatorRelease(rsi_handle);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   class=class="str">"cmt">// Determine current daily trend(bullish or bearish)
   class="type">class="kw">double daily_open = iOpen(_Symbol, PERIOD_D1, class="num">0);
   class="type">class="kw">double daily_close = iClose(_Symbol, PERIOD_D1, class="num">0);
   class="type">bool is_bullish = daily_close > daily_open;
   class="type">bool is_bearish = daily_close < daily_open;
   class=class="str">"cmt">// Get the RSI value for the current bar
   class="type">class="kw">double rsi_values[];
   if (CopyBuffer(rsi_handle, class="num">0, class="num">0, class="num">1, rsi_values) <= class="num">0)
     {
      Print("Failed to get RSI value");
      class="kw">return;
     }
   rsi_value = rsi_values[class="num">0];
   class=class="str">"cmt">// Close open positions if the trend changes

「逆势持仓清场与信号开仓的循环逻辑」

这段逻辑解决一个实盘常见坑:当趋势判定翻转(如 bullish 转 bearish)而账户里还躺着反向单,必须先按 ticket 精准平仓,而不是靠 PositionClose(_Symbol) 偷懒——后者在同源多单时会漏单。 循环从 PositionsTotal()-1 递减到 0,用 PositionSelect(PositionGetSymbol(i)) 定位第 i 个持仓,再读 POSITION_TYPE 与 POSITION_TICKET。若持仓是多单且当前 is_bearish,或空单且 is_bullish,直接 trade.PositionClose(ticket) 平掉;外汇与贵金属杠杆高,逆势单过夜可能扩大浮亏,这种清理要快。 开仓侧只在 PositionsTotal()==0 时动手:bullish 且 rsi_value < RSI_Oversold 读 SYMBOL_ASK 开多,sl=price-StopLoss*_Point、tp=price+TakeProfit*_Point;bearish 且 rsi_value > RSI_Overbought 读 SYMBOL_BID 开空,方向相反。把 StopLoss / TakeProfit 调成 500 / 1000 这类点数,回测里能直观看到盈亏比变化。 尾部还有第二个同构循环做 trailing stop:重新遍历持仓,取 POSITION_PRICE_OPEN 与 POSITION_SL,按 POSITION_TYPE 分支处理。复制下面代码到 MT5 策略测试器,把 RSI 周期设 14、超买超卖 70/30,跑 EURUSD 小时图大概率能看到逆势单被扫掉后再开新单的节奏。

MQL5 / C++
  for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--)
    {
      if (PositionSelect(PositionGetSymbol(i)))  class=class="str">"cmt">// Corrected usage
        {
         class="type">long position_type = PositionGetInteger(POSITION_TYPE);
         class="type">class="kw">ulong ticket = PositionGetInteger(POSITION_TICKET);  class=class="str">"cmt">// Get the position ticket
         if ((position_type == POSITION_TYPE_BUY && is_bearish) ||
             (position_type == POSITION_TYPE_SELL && is_bullish))
           {
            trade.PositionClose(ticket);  class=class="str">"cmt">// Use the class="type">class="kw">ulong variable directly
           }
        }
    }
  class=class="str">"cmt">// Check for buy condition(bullish trend + RSI oversold)
  if (is_bullish && rsi_value < RSI_Oversold)
    {
      class=class="str">"cmt">// No open positions? Place a buy order
      if (PositionsTotal() == class="num">0)
        {
         class="type">class="kw">double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         class="type">class="kw">double sl = price - StopLoss * _Point;
         class="type">class="kw">double tp = price + TakeProfit * _Point;
         class=class="str">"cmt">// Open a buy order
         trade.Buy(Lots, _Symbol, price, sl, tp, "TrendConstraintExpert Buy");
        }
    }
  class=class="str">"cmt">// Check for sell condition(bearish trend + RSI overbought)
  if (is_bearish && rsi_value > RSI_Overbought)
    {
      class=class="str">"cmt">// No open positions? Place a sell order
      if (PositionsTotal() == class="num">0)
        {
         class="type">class="kw">double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         class="type">class="kw">double sl = price + StopLoss * _Point;
         class="type">class="kw">double tp = price - TakeProfit * _Point;
         class=class="str">"cmt">// Open a sell order
         trade.Sell(Lots, _Symbol, price, sl, tp, "TrendConstraintExpert Sell");
        }
    }
  class=class="str">"cmt">// Apply trailing stop
  for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--)
    {
      if (PositionSelect(PositionGetSymbol(i)))  class=class="str">"cmt">// Corrected usage
        {
         class="type">class="kw">double price = PositionGetDouble(POSITION_PRICE_OPEN);
         class="type">class="kw">double stopLoss = PositionGetDouble(POSITION_SL);
         class="type">class="kw">double current_price;
         if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
           {

多单与空单的跟进止损分支

上面这段逻辑把双向持仓的 trailing stop 拆成了两个独立判断。多头用 BID 价比对开仓价,空头用 ASK 价,方向不同取价方式不能反,否则回测里会出现止损永远不触发或者瞬间被扫的怪象。 以多头为例:当 current_price 与记录价 price 的差额超过 TrailingStop 个 _Point 时,才进入修改判断。这里 price 通常是持仓期间某次记录的最低/最高价,TrailingStop 是交易者自己设的步长,比如 30 点。 修改条件要求新止损位比原 stopLoss 更优(多头时原止损更小),才调用 trade.PositionModify 把止损挪到 current_price - TrailingStop*_Point,止盈保持不变。空头分支镜像处理,用 current_price + TrailingStop*_Point,且允许 stopLoss==0 时直接初始化跟进。 外汇与贵金属杠杆高,这类自动改止损在滑点行情中可能成交在预期外价位,建议先在 MT5 策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍,观察 TrailingStop 从 15 调到 50 点时的回撤变化。

MQL5 / C++
current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if (current_price - price > TrailingStop * _Point)
  {
   if (stopLoss < current_price - TrailingStop * _Point)
     {
      trade.PositionModify(PositionGetInteger(POSITION_TICKET), current_price - TrailingStop * _Point, PositionGetDouble(POSITION_TP));
     }
  }
 }
 else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
   {
    current_price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    if (price - current_price > TrailingStop * _Point)
      {
       if (stopLoss > current_price + TrailingStop * _Point || stopLoss == class="num">0)
         {
          trade.PositionModify(PositionGetInteger(POSITION_TICKET), current_price + TrailingStop * _Point, PositionGetDouble(POSITION_TP));
         }
      }
   }
}

◍ 把数学原理落成可跑的算法

外汇与贵金属交易天然带高杠杆与双向波动风险,任何策略都只是提高概率,而非消除亏损可能。 文中列出的七类策略并非凭空而来,它们各自锚定了某位经济学家或实战派交易者提出的数学框架——比如均值回归对应正态分布假设,突破类则依赖波动率聚类现象。先把原理吃透,后续写代码才不会只抄个形态外壳。 真正分水岭在第二步:当你能口述清楚某策略的进出场条件,再用 MQL5 把它写成 EA,验证成本就只剩电费。很多人口中的「策略懂了」其实停在直觉层,一碰到滑点和点差重写就露怯。 所以这一节的价值不在罗列名字,而是提醒你——原理记熟后,编码实现才是把主观经验变成可回测对象的唯一路径,打开 MT5 新建一个空 EA 试着落一条规则,比反复读图文有用。

常见问题

常见做法用15分钟或30分钟图承接日线方向,避免用1分钟图噪点过多。先在日线确认多空倾向,再小周期等回踩企稳信号。
能防止每次tick重复创建句柄拖慢执行,也方便统一调参。建议在OnInit里初始化,后续只调用不重建。
可以。小布能按你设定的日线方向监控持仓,逆势单出现时推送清场提示,把重复劳动交给小布,你专注决策。
因多空止损基准和触发逻辑相反,混写易出错。分开分支可各自设偏移与回调比例,回测更清晰。
常漏掉信号开仓与逆势清场的循环互斥判断。建议先用状态机写清场→开仓→跟进三层,再填数值。