构建蜡烛图趋势约束模型(第8部分):EA开发(II)·进阶篇
🤖

构建蜡烛图趋势约束模型(第8部分):EA开发(II)·进阶篇

(2/3)·手动跑R-R脚本太慢?这篇把Trend Constraint信号和收益风险框直接焊进EA主循环

含代码示例 第 2/3 篇
很多人在V1.09指标上手动拖R-R脚本画风报比矩形, tick一跳就得重画。把风险收益计算挪进OnTick,EA自己算自己画,你只管看信号质量。

◍ 回测与图表事件的空壳钩子

这段源码里先铺了一组策略测试器相关的回调函数:OnTester 直接返回 0.0,OnTesterInit、OnTesterPass、OnTesterDeinit 和 OnChartEvent 全部只留了注释占位,没有任何实际逻辑。也就是说,在 MT5 里跑优化时,这些钩子目前不会往回测结果里塞任何自定义统计,也不会对图表点击、键盘事件做响应。 真正干了点实事的是 OnInit:它用 iRSI(_Symbol, PERIOD_CURRENT, RSI_Period, PRICE_CLOSE) 给当前品种建了一个 RSI 指标句柄,并判断若返回 INVALID_HANDLE 就打印失败并终止初始化。外汇与贵金属杠杆高、滑点跳空频繁,句柄建不起来时硬跑后续代码大概率会错得无声无息,这一步拦截是必要的。 想验证很简单:把 RSI_Period 改成 7 或 21,在 MT5 策略测试器里加载 EA,看 Experts 日志里是否出现 'Failed to create RSI indicator handle';正常情况应无此报错,且初始化返回 INIT_SUCCEEDED(代码未贴全,但失败分支已明确)。

MQL5 / C++
class="type">class="kw">double OnTester()
  {
class=class="str">"cmt">//---
   class="type">class="kw">double ret=class="num">0.0;
class=class="str">"cmt">//---
class=class="str">"cmt">//---
   class="kw">return(ret);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterInit function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterInit()
  {
class=class="str">"cmt">//---

  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterPass function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterPass()
  {
class=class="str">"cmt">//---

  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterDeinit function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterDeinit()
  {
class=class="str">"cmt">//---

  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//---

  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     Trend Constraint Expert Advisor.mq5|
class=class="str">"cmt">//|           Copyright class="num">2024, Clemence Benjamin|
class=class="str">"cmt">//| [MQL5官方文档]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   class=class="str">"cmt">// Initialization code here
   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=class="str">"cmt">// Any other initialization tasks

「EA 生命周期钩子的空壳与释放点」

这段 MT5 专家顾问代码把除初始化外的其余系统回调都铺开了,但多数函数体只留了注释占位。真正落了实锤的只有 OnDeinit 里的一行:用 IndicatorRelease(rsi_handle) 把之前申请的 RSI 指标句柄交还系统,避免退出时句柄泄漏。 OnTick 是每笔报价触发的主循环,原文注释提示你在这里写趋势判定、RSI 阈值判断和下单逻辑,以及移动止损;OnTrade 则专门接订单成交、修改、平仓事件。两者目前都是空架子,需要你按自己的价格行为规则填肉。 回测相关的钩子也齐了:OnTester 默认返回 0.0,可作为自定义优化目标(如收益回撤比)的出口;OnTesterInit / OnTesterPass / OnTesterDeinit 分别对应测试前、每遍优化后、测试收尾,现均为空。外汇与贵金属杠杆高,实盘前务必在策略测试器跑多品种多周期验证句柄释放与逻辑稳定性。

MQL5 / C++
  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">// Cleanup code here
   IndicatorRelease(rsi_handle);   class=class="str">"cmt">// Release RSI indicator handle
   class=class="str">"cmt">// Any other deinitialization tasks
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   class=class="str">"cmt">// Main trading logic goes here

   class=class="str">"cmt">// Determine market conditions(e.g., daily trend, RSI levels)

   class=class="str">"cmt">// Check for trade conditions and execute orders if necessary
   class=class="str">"cmt">// Implement trailing stop logic if necessary
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trade event function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTrade()
  {
   class=class="str">"cmt">// Handle trade events(e.g., order placement, modification, closure)
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Tester function                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester()
  {
   class="type">class="kw">double ret = class="num">0.0;
   class=class="str">"cmt">// Custom optimization criteria(if any) go here
   class="kw">return(ret);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterInit function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterInit()
  {
   class=class="str">"cmt">// Initialization for testing(if needed)
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterPass function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterPass()
  {
   class=class="str">"cmt">// Actions after each optimization pass(if needed)
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterDeinit function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterDeinit()
  {
   class=class="str">"cmt">// Cleanup after testing(if needed)
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

图表事件钩子怎么留空接活

MT5 的 OnChartEvent 是 EA 或指标接管鼠标点击、按键等图表交互的唯一入口,原型接收四个参数:事件 id、long 型 lparam、double 型 dparam、string 型 sparam。 上面这段代码只是官方模板的空壳,注释写明「在这里处理图表事件」,函数体里什么都没做。但它给出了接入点——你之后想抓十字光标坐标、右键菜单、自定义按键,都得从这四个参数里取数。 实盘接这种事件要小心:外汇和贵金属杠杆高、跳空频繁,用图表事件触发下单必须加滑点保护和开关锁,否则一次误点可能直接市价进场。

MQL5 / C++
class=class="str">"cmt">//| Chart event function                                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,
                  class="kw">const class="type">long &lparam,
                  class="kw">const class="type">class="kw">double &dparam,
                  class="kw">const class="type">class="kw">string &sparam)
  {
   class=class="str">"cmt">// Handle chart events(e.g., mouse clicks, key presses) here
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把日线趋势和RSI塞进EA骨架

先把交易能力接进来:引入 Trade.mqh 后,CTrade 类才能下单、改仓、平仓,EA 才算能跟市场对话。输入参数留给交易者自己调——RSI周期14、超买70/超卖30、手数0.1、止损100点、止盈200点、追踪止损50点,这几行决定了程序在不同品种上的脾气。 全局变量里挂了 rsi_value、rsi_handle 和一个 trade 实例,跨函数保状态用。OnInit 里用 iRSI 建句柄,建不出就 INIT_FAILED 直接拦掉,避免没指标硬跑;OnDeinit 用 IndicatorRelease 回收句柄,防内存泄漏。 真正干活在 OnTick:先比日K开盘收盘判趋势,再用 CopyBuffer 抓RSI。趋势翻脸就清反向仓,例如日线由多转空就平掉多头;只有「看涨+RSI超卖」或「看跌+RSI超买」才进场。最后挂50点追踪止损,利润随行情滚动锁定。外汇和贵金属波动剧烈,这套逻辑仍可能连续止损,实盘前务必在 MT5 策略测试器跑历史数据。 下面这段是骨架代码,已含头文件到 OnTick 取RSI 的部分,复制进 MQ5 文件即可编译验证。

MQL5 / C++
class="macro">#include <Trade\Trade.mqh>  class=class="str">"cmt">// Include the trade library
class=class="str">"cmt">// Input parameters
class="kw">input class="type">int      RSI_Period = class="num">14;          class=class="str">"cmt">// RSI period
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="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="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   class=class="str">"cmt">// Release the RSI indicator handle
   IndicatorRelease(rsi_handle);
  }
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 数值,再扫一遍所有持仓,只要趋势方向和仓位类型撞车就砍仓。比如持有多单但 is_bearish 为真,或者持有空单却 is_bullish,trade.PositionClose(ticket) 会用持仓 ticket 直接发送平仓指令,避免逆势硬扛。 开仓条件卡得很死:必须趋势信号和 RSI 极值同时成立,且当前无持仓(PositionsTotal()==0)才动手。多头要 is_bullish 且 rsi_value < RSI_Oversold,空单要 is_bearish 且 rsi_value > RSI_Overbought;止损止盈按 StopLoss、TakeProfit 乘以 _Point 算点差距离,外汇和贵金属杠杆高,点值波动可能瞬间扫掉止损,实盘前先在 MT5 策略测试器跑一遍。 trailing stop 那段从持仓开仓价和当前 SL 读起,按买卖方向分别处理。下面这段是核心平仓与开仓的 MQL5 片段,逐行看能少踩很多坑。

MQL5 / C++
  rsi_value = rsi_values[class="num">0];
  class=class="str">"cmt">// Close open positions if the trend changes
  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">int position_type = PositionGetInteger(POSITION_TYPE);
         class="type">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">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");
        }
    }

多空双向的移动止损触发逻辑

上面这段 trailing stop 逻辑分买、卖两条线处理。多头仓用 SYMBOL_BID 取当前价,当价格相对开仓价上浮超过 TrailingStop 个 _Point,且原止损位低于「现价 - TrailingStop*_Point」时,才把止损提到该价位,止盈不变。 空头仓反过来用 SYMBOL_ASK 取价,价格回落超阈值且当前止损高于「现价 + TrailingStop*_Point」或止损为 0 时,才将止损下移到「现价 + TrailingStop*_Point」。这样能保证止损只朝盈利方向推,不会回撤时反复改。 下面附上这段核心代码的原样,方便你直接拷进 MT5 的 EA 里对照改参: 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 * _PointstopLoss == 0)

{ trade.PositionModify(PositionGetInteger(POSITION_TICKET), current_price + TrailingStop * _Point, PositionGetDouble(POSITION_TP)); } } } 外汇与贵金属杠杆高,移动止损只降浮亏不保盈利,实盘前先用策略测试器跑历史数据验证触发频率。

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));
        }
     }
  }
让小布替你跑这套
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到EA信号与手动框的偏离;把重复劳动交给小布,你专注决策。

常见问题

按模型定义,信号需低周期汇聚,仅日K方向对不构成开仓条件,可能跳过该次机会。
OnTick做条件评估与发单,OnTrade处理成交回执与仓位修正,分离可降低重单概率。
小布盯盘支持MT5品种页叠加自写指标/EA输出,你把EA信号缓冲画到图表,小布侧就能同步显示。
不影响实盘执行,但会丢失策略测试器里的自定义优化统计,倾向补全以便回测。
滑点与极端波动可能让风控矩形失效,贵金属杠杆高、回撤快,务必先仿真再上真实仓位。