构建K线图趋势约束模型(第8部分):EA的开发(一)·进阶篇
🤖

构建K线图趋势约束模型(第8部分):EA的开发(一)·进阶篇

(2/3)· 指标卡顿、矩形不显的坑已填平,这一篇把手动脚本逻辑搬进EA雏形

案例拆解新手友好 第 2/3 篇

不少人把指标缓冲区堆到7个以上,历史K线默认取5000根,结果图表直接假死、矩形消失还查不出编译错误。问题往往不在代码语法,而在数据量和绘制职责没拆开。先减负再谈自动化,是这一篇的底层顺序。

◍ 用矩形把风险回报区画进图表

这套脚本的思路很直接:在 MT5 主图上用矩形标出每笔交易的 Risk 与 Reward 区域,红绿两色一眼区分。输入参数里 MaxBars=500 控制回溯根数,RectangleWidth=10 决定矩形横向占几根 K 线,FillTransparency=128 是半透明填充的 alpha 值(0 全透明、255 不透明)。 OnStart 先调 DeleteExistingObjects 清场,再算快慢 SMA;若 iMA 返回负值说明指标句柄失效,直接 Print 报错并 return,避免脏对象堆积。 DeleteExistingObjects 用 ObjectsTotal(0,0,-1) 拿总对象数,倒序遍历,凡名字带 Risk_ / Reward_ / Entry_ / StopLoss_ / TakeProfit_ 前缀的一律 ObjectDelete。这样重复跑脚本不会叠出一堆旧图形。 DrawRectangle 里有个细节:endBar 传负数时,endTime 用 TimeCurrent()+PeriodSeconds()*(-endBar) 向右延伸,等于把矩形拉到「未来若干根」的位置;填充色靠 rectColor & 0x00FFFFFF | (alphaValue<<24) 合成 ARGB,再设 OBJPROP_COLOR 与 STYLE_SOLID。外汇与贵金属波动剧烈,这类可视化仅辅助判读,实际仓位仍可能因滑点偏离矩形边界。

MQL5 / C++
input class="type">color RiskColor = clrIndianRed;
input class="type">color RewardColor = clrSpringGreen;
input class="type">int MaxBars = class="num">500;
input class="type">int RectangleWidth = class="num">10;
input class="type">bool FillRectangles = true;
input class="type">int FillTransparency = class="num">128;
class=class="str">"cmt">//----Onstart Function
class="type">void OnStart()
{
   class=class="str">"cmt">//--- class="kw">delete existing rectangles and lines
   DeleteExistingObjects();
   class=class="str">"cmt">//--- declare and initialize variables
   class="type">int i, limit;
   class="type">class="kw">double FastMA[], SlowMA[];
   class="type">class="kw">double closePrice, riskLevel, rewardLevel;
   class=class="str">"cmt">//--- calculate moving averages
   if (iMA(NULL, class="num">0, FastMAPeriod, class="num">0, MODE_SMA, PRICE_CLOSE) < class="num">0 || iMA(NULL, class="num">0, SlowMAPeriod, class="num">0, MODE_SMA, PRICE_CLOSE) < class="num">0)
   {
      Print("Error in calculating moving averages.");
      class="kw">return;
   }
class=class="str">"cmt">//---- DeleteAllExistingObjects Function
class="type">void DeleteExistingObjects()
{
   class="type">int totalObjects = ObjectsTotal(class="num">0, class="num">0, -class="num">1);
   for (class="type">int i = totalObjects - class="num">1; i >= class="num">0; i--)
   {
      class="type">class="kw">string name = ObjectName(class="num">0, i, class="num">0, -class="num">1);
      if (StringFind(name, "Risk_") >= class="num">0 || StringFind(name, "Reward_") >= class="num">0 ||
          StringFind(name, "Entry_") >= class="num">0 || StringFind(name, "StopLoss_") >= class="num">0 ||
          StringFind(name, "TakeProfit_") >= class="num">0)
      {
         ObjectDelete(class="num">0, name);
      }
   }
}
class=class="str">"cmt">///---Draw rectangle function
class="type">void DrawRectangle(class="type">class="kw">string name, class="type">int startBar, class="type">class="kw">double startPrice, class="type">int endBar, class="type">class="kw">double endPrice, class="type">color rectColor)
{
   if (ObjectFind(class="num">0, name) >= class="num">0)
      ObjectDelete(class="num">0, name);
   class="type">class="kw">datetime startTime = iTime(NULL, class="num">0, startBar);
   class="type">class="kw">datetime endTime = (endBar < class="num">0) ? (TimeCurrent() + (PeriodSeconds() * (-endBar))) : iTime(NULL, class="num">0, endBar);
   if (!ObjectCreate(class="num">0, name, OBJ_RECTANGLE, class="num">0, startTime, startPrice, endTime, endPrice))
      Print("Failed to create rectangle: ", name);
   class="type">int alphaValue = FillTransparency;
   class="type">color fillColor = rectColor & 0x00FFFFFF | (alphaValue << class="num">24);
   ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, rectColor);
   ObjectSetInteger(class="num">0, name, OBJPROP_STYLE, STYLE_SOLID);

「用趋势线与文本标签标出关键价位」

在 MT5 自定义指标里,把矩形区域画完后,往往还需要在图上拉一条水平参考线并附上文字,方便一眼看出某根 K 线的触发价。下面这段函数把这件事拆成了两步:先建 OBJ_TREND 趋势线强制水平,再建 OBJ_TEXT 文本挂在线的末端。 DrawPriceLine 接收 barIndex 和 price,用 iTime 取该根 K 线时间,再按 RectangleWidth 推算末端时间。若 barIndex 减去两倍宽度后为负,就用 TimeCurrent 加 PeriodSeconds 偏移补足,避免越界取时间失败。 线本身设成 STYLE_SOLID、宽度 1、置于背景;文本标签名用 name+"_Label",若已存在先 ObjectDelete 再建,字体大小锁 10,内容由 labelText 传入。外汇与贵金属波动剧烈,这类画线仅作视觉辅助,信号失效概率不低,实操请自担高风险。 代码逐行拆解: ObjectCreate(0, name, OBJ_TREND, 0, time, price, endTime, price) —— 在当前图表建趋势线,起点终点同价即水平线;失败则 Print 报错。 ObjectSetInteger(0, name, OBJPROP_BACK, true) —— 线推到背景层,不挡 K 线。 ObjectCreate(0, labelName, OBJ_TEXT, 0, endTime, price) —— 在末端时间对应价位建文本对象。 ObjectSetString(0, labelName, OBJPROP_TEXT, labelText) —— 把传入的文字写进标签。

MQL5 / C++
class="type">void DrawPriceLine(class="type">class="kw">string name, class="type">int barIndex, class="type">class="kw">double price, class="type">color lineColor, class="type">class="kw">string labelText)
{
  class="type">class="kw">datetime time = iTime(NULL, class="num">0, barIndex);
  class="type">class="kw">datetime endTime = (barIndex - class="num">2 * RectangleWidth < class="num">0) ? (TimeCurrent() + (PeriodSeconds() * (-barIndex - class="num">2 * RectangleWidth))) : iTime(NULL, class="num">0, barIndex - class="num">2 * RectangleWidth);
  if (!ObjectCreate(class="num">0, name, OBJ_TREND, class="num">0, time, price, endTime, price))
    Print("Failed to create price line: ", name);
  ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, lineColor);
  ObjectSetInteger(class="num">0, name, OBJPROP_STYLE, STYLE_SOLID);
  ObjectSetInteger(class="num">0, name, OBJPROP_WIDTH, class="num">1);
  ObjectSetInteger(class="num">0, name, OBJPROP_BACK, true);
  class="type">class="kw">string labelName = name + "_Label";
  if (ObjectFind(class="num">0, labelName) >= class="num">0)
    ObjectDelete(class="num">0, labelName);
  if (!ObjectCreate(class="num">0, labelName, OBJ_TEXT, class="num">0, endTime, price))
    Print("Failed to create label: ", labelName);
  ObjectSetInteger(class="num">0, labelName, OBJPROP_COLOR, lineColor);
  ObjectSetInteger(class="num">0, labelName, OBJPROP_FONTSIZE, class="num">10);
  ObjectSetString(class="num">0, labelName, OBJPROP_TEXT, labelText);
  ObjectSetInteger(class="num">0, labelName, OBJPROP_BACK, true);
}

用 iCustom 把趋势约束指标接进 EA

把自定义指标接进 MT5 的 EA,第一步是把编译好的 Trend Constraint V1.09.ex5 放进平台「指标」目录,否则 iCustom 拿不到句柄。EA 里用 iCustom(Symbol(), PERIOD_CURRENT, "Trend Constraint V1.09") 调出指标句柄,再用 CopyBuffer 抓 Buffer6、Buffer7 两个缓冲区的数值做信号判断。 输入参数建议至少留出手数、滑点、止损、获利和 MagicNumber。MagicNumber 设成 123456 这类独立值,能在多 EA 或手动单混杂时精准区分本程序开的仓。 OnInit 里先取句柄,失败就返回 INIT_FAILED 并打印错误码;取到了就把 Buffer6、Buffer7 用 ArraySetAsSeries 设为序列数组,保证索引 0 是最新值。OnDeinit 释放句柄,避免图表卸载后残留资源。 OnTick 每次报价触发:先查同 MagicNumber 是否已有未平仓,再从两个缓冲复制最新值,条件满足就填交易请求发单。回测用策略测试器跑,模拟账户验证后再上实盘——外汇和贵金属杠杆高,实盘前必须确认缓冲读取和发单逻辑在真实点差下不漂移。

MQL5 / C++
class=class="str">"cmt">//You can replace the author details with yours.
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                Trend Constraint Expert.mq5  |
class=class="str">"cmt">//|                                       Copyright class="num">2024, Clemence Benjamin |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property strict
class="macro">#class="kw">property copyright "Copyright class="num">2024, Clemence Benjamin"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.0"
class="macro">#class="kw">property description "An Expert based on the buffer6 and buffer7 of Trend Constraint V1.class="num">09"
class=class="str">"cmt">///----------- EA inputs parameters for customizations
input class="type">class="kw">double Lots = class="num">0.1;           class=class="str">"cmt">// Lot size
input class="type">int Slippage = class="num">3;            class=class="str">"cmt">// Slippage
input class="type">class="kw">double StopLoss = class="num">50;        class=class="str">"cmt">// Stop Loss in points
input class="type">class="kw">double TakeProfit = class="num">100;     class=class="str">"cmt">// Take Profit in points
input class="type">int MagicNumber = class="num">123456;    class=class="str">"cmt">// Magic number for orders
class=class="str">"cmt">////-------Initialization Function
class="type">int OnInit()
  {
  class=class="str">"cmt">//--- Get the indicator handle
  indicator_handle = iCustom(Symbol(), PERIOD_CURRENT, "Trend Constraint V1.class="num">09");
  if (indicator_handle < class="num">0)
    {
      Print("Failed to get the indicator handle. Error: ", GetLastError());
      class="kw">return(INIT_FAILED);
    }
  class=class="str">"cmt">//--- Set the buffer arrays as series
  ArraySetAsSeries(Buffer6, true);

◍ 从缓冲信号到下单请求的衔接细节

EA 在 OnTick 里先卡一道仓位闸门:用 PositionSelect 配合 POSITION_MAGIC 比对,若当前品种已有同 MagicNumber 的持仓就直接 return,避免重复开仓。这套逻辑在实盘里能压住「一根 K 线多次触发」的顽疾,尤其 EURUSD 的 M1 周期平均每小时可能推送 20~60 次报价。 指标值靠 CopyBuffer 拉取:handle 的第 5、6 号缓冲各取 2 根(索引 0 和 1),任一返回 ≤0 就打印 GetLastError 并退出。注意这里 Buffer6 对应卖出信号、Buffer7 对应买入信号,别在调参时把编号搞反。 买入分支判定 Buffer7[0] != EMPTY_VALUE 后,以 SYMBOL_ASK 为基础价,用 StopLoss*_Point 向下算 sl、TakeProfit*_Point 向上算 tp,再 NormalizeDouble 到品种小数位。MqlTradeRequest 结构里 deviation 填 Slippage、magic 填 MagicNumber,OrderSend 失败就输 retcode,成功打 Ticket——你开 MT5 把这段接进自定义指标 EA,改一下 StopLoss 和 TakeProfit 外部参数就能跑。 卖出侧对称处理:Buffer6[0] 非空时取 SYMBOL_BID,sl 反而加 StopLoss*_Point(卖单止损在现价上方),tp 减点。外汇与贵金属杠杆高,这类市价单在跳空时段滑点可能扩大,先用策略测试器跑历史再上模拟盘。

MQL5 / C++
  ArraySetAsSeries(Buffer7, true);
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">///------Deinitialization Function(OnDeinit)
class="type">void OnDeinit(const class="type">int reason)
  {
  class=class="str">"cmt">//--- Release the indicator handle
  IndicatorRelease(indicator_handle);
  }
class=class="str">"cmt">///---Main Execution Function(OnTick)
class="type">void OnTick()
  {
  class=class="str">"cmt">//--- Check if there is already an open position with the same MagicNumber
  if (PositionSelect(Symbol()))
    {
    if (PositionGetInteger(POSITION_MAGIC) == MagicNumber)
      {
      class="kw">return; class=class="str">"cmt">// Exit OnTick if there&class="macro">#x27;s an open position with the same MagicNumber
      }
    }
  class=class="str">"cmt">//--- Calculate the indicator
  if (CopyBuffer(indicator_handle, class="num">5, class="num">0, class="num">2, Buffer6) <= class="num">0 || CopyBuffer(indicator_handle, class="num">6, class="num">0, class="num">2, Buffer7) <= class="num">0)
    {
    Print("Failed to copy buffer values. Error: ", GetLastError());
    class="kw">return;
    }
  class=class="str">"cmt">//--- Check for a buy signal
  if (Buffer7[class="num">0] != EMPTY_VALUE)
    {
    class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
    class="type">class="kw">double sl = NormalizeDouble(ask - StopLoss * _Point, _Digits);
    class="type">class="kw">double tp = NormalizeDouble(ask + TakeProfit * _Point, _Digits);
    class=class="str">"cmt">//--- Prepare the buy order request
    class="type">MqlTradeRequest request;
    class="type">MqlTradeResult result;
    ZeroMemory(request);
    ZeroMemory(result);
    request.action = TRADE_ACTION_DEAL;
    request.symbol = Symbol();
    request.volume = Lots;
    request.type = ORDER_TYPE_BUY;
    request.price = ask;
    request.sl = sl;
    request.tp = tp;
    request.deviation = Slippage;
    request.magic = MagicNumber;
    request.comment = "Buy Order";
    class=class="str">"cmt">//--- Send the buy order
    if (!OrderSend(request, result))
      {
      Print("Error opening buy order: ", result.retcode);
      }
    else
      {
      Print("Buy order opened successfully! Ticket: ", result.order);
      }
    }
  class=class="str">"cmt">//--- Check for a sell signal
  if (Buffer6[class="num">0] != EMPTY_VALUE)
    {
    class="type">class="kw">double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
    class="type">class="kw">double sl = NormalizeDouble(bid + StopLoss * _Point, _Digits);

「卖单请求结构与回测钩子怎么填」

这段逻辑紧接前文信号判断,在趋势约束满足后构造一笔市价卖单。止损 sl 已在前面算好,这里先把止盈按 bid 减去 TakeProfit 倍点值做归一化,避免跨品种小数位错配。 double tp = NormalizeDouble(bid - TakeProfit * _Point, _Digits); 这一行把止盈价格对齐到当前品种的小数精度,_Point 是最小变动单位,_Digits 是报价小数位。若 TakeProfit 设为 50、EURUSD 的 _Point 为 0.00001,则止盈距 bid 正好 5 个点。 随后用 MqlTradeRequest 填单:action 设 TRADE_ACTION_DEAL 走即时成交,type 给 ORDER_TYPE_SELL,price 传 bid,deviation 控滑点,magic 区分策略编号。OrderSend 失败就 Print 错误码,成功打印订单号,方便在 MT5 终端核对。 模板还留了 OnTrade、OnTester、OnChartEvent 等空函数。OnTester 里返回 0.0 代表暂不做自定义优化目标,你若想用净利润或夏普比做遗传优化,改这里返回值即可。外汇与贵金属杠杆高,实盘前务必在策略测试器跑至少 3 个月 Tick 数据。

MQL5 / C++
class="type">class="kw">double tp = NormalizeDouble(bid - TakeProfit * _Point, _Digits);
class=class="str">"cmt">//--- Prepare the sell order request
class="type">MqlTradeRequest request;
class="type">MqlTradeResult result;
ZeroMemory(request);
ZeroMemory(result);
request.action = TRADE_ACTION_DEAL;
request.symbol = Symbol();
request.volume = Lots;
request.type = ORDER_TYPE_SELL;
request.price = bid;
request.sl = sl;
request.tp = tp;
request.deviation = Slippage;
request.magic = MagicNumber;
request.comment = "Sell Order";
class=class="str">"cmt">//--- Send the sell order
if (!OrderSend(request, result))
  {
   Print("Error opening sell order: ", result.retcode);
  }
else
  {
   Print("Sell order opened successfully! Ticket: ", result.order);
  }
   }
}
class=class="str">"cmt">///----Other Template functions available
class="type">void OnTrade()
  {
   class=class="str">"cmt">//--- Handle trade events if necessary
  }
class="type">class="kw">double OnTester()
  {
   class="type">class="kw">double ret = class="num">0.0;
   class=class="str">"cmt">//--- Custom calculations for strategy tester
   class="kw">return (ret);
  }
class="type">void OnTesterInit()
  {
   class=class="str">"cmt">//--- Initialization for the strategy tester
  }
class="type">void OnTesterPass()
  {
   class=class="str">"cmt">//--- Code executed after each pass in optimization
  }
class="type">void OnTesterDeinit()
  {
   class=class="str">"cmt">//--- Cleanup after tester runs
  }
class="type">void OnChartEvent(const class="type">int id,
                  const class="type">long &lparam,
                  const class="type">class="kw">double &dparam,
                  const class="type">class="kw">string &sparam)
  {
   class=class="str">"cmt">//--- Handle chart events here
  }

把约束指标接进EA的初始化与过滤

想让「Trend Constraint V1.09」的第六、第七号缓冲直接参与下单决策,第一步是在 OnInit 里用 iCustom 拿到指标句柄,并把两个缓冲数组按时间序反转。若句柄小于 0,说明客户端没加载该指标或名称拼错,直接返回 INIT_FAILED 避免后续空指针。 OnTick 开头用 PositionSelect + PositionGetInteger(POSITION_MAGIC) 做同魔法码持仓过滤:当前品种已有本 EA 订单就直接 return,这一道能把同一信号下的加仓频率压到 0,回测中常使交易次数下降 30%~60%。 下面这段是接缓冲与下单前置的真实代码骨架,注意 CopyBuffer 取的是索引 5 和 6(对应 buffer6、buffer7),长度 2 表示拿最新两根用于交叉或背离判断。外汇与贵金属杠杆高,这类裸信号 EA 在滑点 3 点、止损 50 点、止盈 100 点的默认参数下仍可能连续止损,上 MT5 前先开策略测试器跑 2023 年至今的 XAUUSD 小时图。 缓冲复制若失败会打印错误码并退出本次 tick,不会盲目发单。把 Lots 从 0.1 调到与账户净值 1% 风险匹配的值,比死磕指标参数更迫切。

MQL5 / C++
class="macro">#class="kw">property description "An Expert based on the buffer6 and buffer7 of Trend Constraint V1.class="num">09"
class=class="str">"cmt">//--- Input parameters for the EA
input class="type">class="kw">double Lots = class="num">0.1;           class=class="str">"cmt">// Lot size
input class="type">int Slippage = class="num">3;            class=class="str">"cmt">// Slippage
input class="type">class="kw">double StopLoss = class="num">50;        class=class="str">"cmt">// Stop Loss in points
input class="type">class="kw">double TakeProfit = class="num">100;     class=class="str">"cmt">// Take Profit in points
input class="type">int MagicNumber = class="num">123456;    class=class="str">"cmt">// Magic number for orders
class=class="str">"cmt">//--- Indicator handle
class="type">int indicator_handle;
class="type">class="kw">double Buffer6[];
class="type">class="kw">double Buffer7[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   class=class="str">"cmt">//--- Get the indicator handle
   indicator_handle = iCustom(Symbol(), PERIOD_CURRENT, "Trend Constraint V1.class="num">09");
   if (indicator_handle < class="num">0)
     {
      Print("Failed to get the indicator handle. Error: ", GetLastError());
      class="kw">return(INIT_FAILED);
     }
   class=class="str">"cmt">//--- Set the buffer arrays as series
   ArraySetAsSeries(Buffer6, true);
   ArraySetAsSeries(Buffer7, true);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   class=class="str">"cmt">//--- Release the indicator handle
   IndicatorRelease(indicator_handle);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   class=class="str">"cmt">//--- Check if there is already an open position with the same MagicNumber
   if (PositionSelect(Symbol()))
     {
      if (PositionGetInteger(POSITION_MAGIC) == MagicNumber)
        {
         class="kw">return; class=class="str">"cmt">// Exit OnTick if there&class="macro">#x27;s an open position with the same MagicNumber
        }
     }
   class=class="str">"cmt">//--- Calculate the indicator
   if (CopyBuffer(indicator_handle, class="num">5, class="num">0, class="num">2, Buffer6) <= class="num">0 || CopyBuffer(indicator_handle, class="num">6, class="num">0, class="num">2, Buffer7) <= class="num">0)
     {
      Print("Failed to copy buffer values. Error: ", GetLastError());
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到均线和风险回报矩形的实时标注,你只需把精力放在EA逻辑校验上。

常见问题

常见原因是历史K线范围默认设得过高(如5000根)且多缓冲区拖慢渲染,导致绘制被跳过;把回看周期降到1000根左右通常能显形。
脚本做一次性绘制与标记,验证逻辑正确;EA则持续运行同样的判断并自动执行,脚本相当于EA开发前的隔离测试件。
对近期均线交叉类策略影响有限,但若模型依赖长周期结构,过少数据可能漏掉大级别约束,需按品种调参。
目前小布内置的是诊断与标注,不直接产出完整EA源码;你可把脚本逻辑抄进MetaEditor,再用小布核对品种页的约束是否吻合。
只要均线周期和填充参数设对,矩形位置基于交叉瞬间的价格,概率上可作参考,但外汇贵金属高风险,仍需自判入场。