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

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

第 1/3 篇

「先把风险和回报矩形画稳再谈EA」

在 MT5 里用 EA 自动绘制风险/回报矩形时,最常见的坑是:价格跳空或周期切换后矩形锚点漂移,导致视觉上的盈亏比和实际订单参数对不上。原文作者在前几篇手动绘制阶段就卡在这里,本篇开始转用基于指标的 EA 来固化锚点。 做法是把矩形对象绑定到具体指标缓冲区和时间索引,而不是屏幕坐标。这样切换周期或重连服务器时,矩形仍按 K 线实体边界重算,不会乱跑。 该系列首篇发布于 2025-03-10,原文页面显示 1260 次浏览、2 条评论,说明这类'可视化约束模型'在手动交易者里需求不低。外汇和贵金属品种波动大、滑点频繁,用 EA 固化矩形只是降低人工误差,不预示任何收益,实际效果请在 MT5 策略测试器里用历史数据验证。

◍ 编译器揪出风险回报矩形不显示的根因

MetaEditor 自带的编译器不只是过语法,它能把分析尝试里捕获的错误直接定位出来。我之前那版指标能正常编译通过,但风险-回报矩形死活不画在图上,查了一圈才发现锅不在代码逻辑,而在历史 K 线范围的默认值被设成了 5000 根,导致指标在默认视窗内取不到该画的区域。 另一个容易被忽略的点:同一个程序里挂多个缓冲区,会明显抬高计算复杂度,图表子窗口的刷新速度可能变慢,尤其在低频机或重载品种上更明显。 先把这套坑排掉,后面才谈得上拿改进版趋势约束指标去写 EA。下面这张独立脚本的运行图,已经能把最初想靠主指标解决的风险、回报矩形自动绘制跑通。

把风险回报矩形拆成独立脚本跑

指标原本回看 5000 根 K 线来画风险-回报矩形,数据量压得终端发卡。后来把回看周期砍到 1000 根,再把矩形绘制逻辑抽成一个独立脚本,专门检查缓冲区 6、7 的条件,终端卡顿明显缓解。 这个脚本不是常驻 EA,它只做一次性任务:你手动拖到图表上,它才画矩形和标注入场、止损、获利线,然后停。想看历史信号就得自己再跑一次,不适合挂机自动盯。 输入参数留了口子:快/慢 MA 周期默认 14 和 50,风险矩形高 5000 点、回报矩形高 15000 点,最大处理 500 根 K 线,矩形宽 10 根,填充透明度 128(约半透)。改这几个数就能套不同策略偏好。 OnStart 里先调 DeleteExistingObjects 清场,再用 iMA 算双线存数组;遍历 K 线抓「快线从下穿上慢线」的看涨交叉,算出入场/止损/获利后调绘制函数。三个子功能各管一摊:清旧对象按名字扫图表、画矩形先删同名再算起止时间设颜色、画价格线拉满宽度并贴价标。 外汇和贵金属波动大、杠杆高,这套图形只是帮你肉眼预演方向和点位,真动手前得自己核一下信号有效期。脚本逻辑跑通了,下一步才是把它接进 Trend Constraint V1.09 做专属 EA。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                    Trend Constraint R-R.mq5  |
class=class="str">"cmt">//|                                                  Script program  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property strict
class="macro">#class="kw">property script_show_inputs
class="macro">#class="kw">property copyright "class="num">2024 Clemence Benjamin"
class="macro">#class="kw">property version "class="num">1.00"
class="macro">#class="kw">property link "[MQL5官方文档]
class="macro">#class="kw">property description "A script program for drawing risk and rewars rectangles based on Moving Averaage crossover."
class=class="str">"cmt">//--- input parameters
input class="type">int FastMAPeriod = class="num">14;
input class="type">int SlowMAPeriod = class="num">50;
input class="type">class="kw">double RiskHeightPoints = class="num">5000.0; class=class="str">"cmt">// Default height of the risk rectangle in points
input class="type">class="kw">double RewardHeightPoints = class="num">15000.0; class=class="str">"cmt">// Default height of the reward rectangle in points
input class="type">color RiskColor = clrIndianRed; class=class="str">"cmt">// Default risk class="type">color
input class="type">color RewardColor = clrSpringGreen; class=class="str">"cmt">// Default reward class="type">color
input class="type">int MaxBars = class="num">500; class=class="str">"cmt">// Maximum bars to process
input class="type">int RectangleWidth = class="num">10; class=class="str">"cmt">// Width of the rectangle in bars
input class="type">bool FillRectangles = true; class=class="str">"cmt">// Option to fill rectangles
input class="type">int FillTransparency = class="num">128; class=class="str">"cmt">// Transparency level(class="num">0-class="num">255), class="num">128 is class="num">50% transparency
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
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

「快慢均线交叉的可视化落点」

这段逻辑先把快慢 SMA 算出来做错误拦截:任一均线返回值小于 0 就打印报错并退出,避免后面拿脏数据画图。注意 iMA 取的是当前图表(NULL)、当前周期(0)、收盘价(PRICE_CLOSE) 的 SMA,FastMAPeriod 与 SlowMAPeriod 是外部传入的周期参数。 ArraySetAsSeries 把 FastMA、SlowMA 设为时间序列(索引 0 是最新 bar),再用 CopyBuffer 各拉 MaxBars 根到数组。limit 取两数组尺寸的较小值,循环从 i=1 跑到 limit-1,留一根前值做交叉判定。 金叉条件写得很直白:FastMA[i] > SlowMA[i] 且前一根 FastMA[i-1] <= SlowMA[i-1],此时取 i 号收盘价作入场,riskLevel 在收盘价上方加 RiskHeightPoints*Point(),rewardLevel 在下方减同样点数——这是多头的风险/回报框。空头反过来,riskLevel 在下方、rewardLevel 在上方。 每触发一次交叉,代码用 DrawRectangle 画风险/回报矩形(宽 RectangleWidth 根 bar),再用 DrawPriceLine 标蓝线入场、红线止损、绿线止盈,标签带具体价格。外汇与贵金属杠杆高,这类框线只是历史形态标注,交叉后价格延续概率不等于确定性,开 MT5 把这段塞进脚本就能直观回看近 N 根的交叉分布。

MQL5 / C++
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;
}
ArraySetAsSeries(FastMA, true);
ArraySetAsSeries(SlowMA, true);
CopyBuffer(iMA(NULL, class="num">0, FastMAPeriod, class="num">0, MODE_SMA, PRICE_CLOSE), class="num">0, class="num">0, MaxBars, FastMA);
CopyBuffer(iMA(NULL, class="num">0, SlowMAPeriod, class="num">0, MODE_SMA, PRICE_CLOSE), class="num">0, class="num">0, MaxBars, SlowMA);
limit = MathMin(ArraySize(FastMA), ArraySize(SlowMA));
for (i = class="num">1; i < limit - class="num">1; i++)
{
   class=class="str">"cmt">//--- check for crossover
   if (FastMA[i] > SlowMA[i] && FastMA[i - class="num">1] <= SlowMA[i - class="num">1])
   {
      class=class="str">"cmt">//--- class="type">long position entry point(bullish crossover)
      closePrice = iClose(NULL, class="num">0, i);
      riskLevel = closePrice + RiskHeightPoints * Point();
      rewardLevel = closePrice - RewardHeightPoints * Point();
      class=class="str">"cmt">//--- draw risk rectangle
      DrawRectangle("Risk_" + IntegerToString(i), i, closePrice, i - RectangleWidth, riskLevel, RiskColor);
      class=class="str">"cmt">//--- draw reward rectangle
      DrawRectangle("Reward_" + IntegerToString(i), i, closePrice, i - RectangleWidth, rewardLevel, RewardColor);
      class=class="str">"cmt">//--- draw entry, stop loss, and take profit lines
      DrawPriceLine("Entry_" + IntegerToString(i), i, closePrice, clrBlue, "Entry: " + DoubleToString(closePrice, _Digits));
      DrawPriceLine("StopLoss_" + IntegerToString(i), i, riskLevel, clrRed, "Stop Loss: " + DoubleToString(riskLevel, _Digits));
      DrawPriceLine("TakeProfit_" + IntegerToString(i), i, rewardLevel, clrGreen, "Take Profit: " + DoubleToString(rewardLevel, _Digits));
   }
   else if (FastMA[i] < SlowMA[i] && FastMA[i - class="num">1] >= SlowMA[i - class="num">1])
   {
      class=class="str">"cmt">//--- class="type">short position entry point(bearish crossover)
      closePrice = iClose(NULL, class="num">0, i);
      riskLevel = closePrice - RiskHeightPoints * Point();
      rewardLevel = closePrice + RewardHeightPoints * Point();
      class=class="str">"cmt">//--- draw risk rectangle
      DrawRectangle("Risk_" + IntegerToString(i), i, closePrice, i - RectangleWidth, riskLevel, RiskColor);
      class=class="str">"cmt">//--- draw reward rectangle
      DrawRectangle("Reward_" + IntegerToString(i), i, closePrice, i - RectangleWidth, rewardLevel, RewardColor);
      class=class="str">"cmt">//--- draw entry, stop loss, and take profit lines

◍ 用对象清理和矩形把风险回报区画出来

在 MT5 脚本里反复重画前,得先把旧图形清掉,否则图表会被同名对象堆死。DeleteExistingObjects 用 ObjectsTotal(0,0,-1) 拿当前图表全部对象数,倒序遍历并用 StringFind 匹配 Risk_ / Reward_ / Entry_ 等前缀,命中就 ObjectDelete,避免历史残影干扰新的价格行为判读。 DrawRectangle 负责把某段 K 线区间的价位带渲染成半透明矩形。它先用 ObjectFind 查重再删,然后用 iTime 取起止 bar 的时间;若 endBar 为负,则按 PeriodSeconds 往右延展到未来时间,方便标「未闭合」的持仓区。 填充透明度由 FillTransparency 控制(0–255),代码里用 rectColor & 0x00FFFFFF | (alphaValue << 24) 把 alpha 拼进 ARGB,但紧接着又用 OBJPROP_COLOR 设了不透明描边色——想让填充真透明,得把 fillColor 也写进 OBJPROP_FILL_COLOR(若终端版本支持)。外汇与贵金属杠杆高,图形仅作概率参考,实盘须自担风险。 DrawPriceLine 那三行给每笔信号打标签:蓝色 Entry、红色 StopLoss、绿色 TakeProfit,均带 DoubleToString(closePrice, _Digits) 自适应报价精度。复制下面片段到你的 EA,改 FillTransparency 从 64 试到 180,能直接看出哪档透明度不挡裸 K。

MQL5 / C++
DrawPriceLine("Entry_" + IntegerToString(i), i, closePrice, clrBlue, "Entry: " + DoubleToString(closePrice, _Digits));
DrawPriceLine("StopLoss_" + IntegerToString(i), i, riskLevel, clrRed, "Stop Loss: " + DoubleToString(riskLevel, _Digits));
DrawPriceLine("TakeProfit_" + IntegerToString(i), i, rewardLevel, clrGreen, "Take Profit: " + DoubleToString(rewardLevel, _Digits));
}
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function to class="kw">delete existing rectangles and lines                |
class=class="str">"cmt">//+------------------------------------------------------------------+
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">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function to draw rectangles                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
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=class="str">"cmt">// Set the class="type">color with transparency(alpha value)
  class="type">int alphaValue = FillTransparency; class=class="str">"cmt">// Adjust transparency level(class="num">0-class="num">255)
  class="type">color fillColor = rectColor & 0x00FFFFFF | (alphaValue << class="num">24); class=class="str">"cmt">// Combine alpha with RGB
  ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, rectColor);
  ObjectSetInteger(class="num">0, name, OBJPROP_STYLE, STYLE_SOLID);
  ObjectSetInteger(class="num">0, name, OBJPROP_WIDTH, class="num">1);

把矩形和价格线画进背景层

这段脚本收尾处处理了两类图形对象的属性:矩形是否填充,以及价格延伸线的绘制与标注。矩形部分用 OBJPROP_BACK 推到背景,避免遮住 K 线;FillRectangles 为真时给填充色(带透明度),否则用 rectColor & 0x00FFFFFF 把 Alpha 通道清零,只留边框。 DrawPriceLine 函数先取 barIndex 对应的时间,再按 RectangleWidth 计算向右延伸的终点时间——当 barIndex - 2*RectangleWidth 小于 0 时,用 TimeCurrent 加 PeriodSeconds 推算,保证线能拉到图表右侧可视区之外。OBJ_TREND 画出来的是水平线(两端 price 相同),线宽固定 1、实心、背景层。 标注用了独立的 OBJ_TEXT 对象,锚点设 ANCHOR_LEFT、字号 10、右上角对齐并向右偏移 5 像素,先查重再删重建,防止重复加载时标签堆叠。最后一组 input 参数里 RiskHeightPoints=5000.0、RewardHeightPoints=15000.0 是按点数(point)计的,直盘上 1 点=0.0001,意味着风险区高 500 点、奖励区高 1500 点,外汇与贵金属杠杆环境下这种区间对应的浮亏浮盈幅度极大,实盘使用前务必在 MT5 策略测试器用历史数据核对绘制逻辑。

MQL5 / C++
  ObjectSetInteger(class="num">0, name, OBJPROP_BACK, true); class=class="str">"cmt">// Set to background
  if (FillRectangles)
  {
    ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, fillColor); class=class="str">"cmt">// Fill class="type">color with transparency
  }
  else
  {
    ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, rectColor & 0x00FFFFFF); class=class="str">"cmt">// No fill class="type">color
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function to draw price lines                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
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); class=class="str">"cmt">// Extend line to the right
  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=class="str">"cmt">// Set to background
  class=class="str">"cmt">// Create text label
  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_ANCHOR, ANCHOR_LEFT);
  ObjectSetString(class="num">0, labelName, OBJPROP_TEXT, labelText);
  ObjectSetInteger(class="num">0, labelName, OBJPROP_FONTSIZE, class="num">10);
  ObjectSetInteger(class="num">0, labelName, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
  ObjectSetInteger(class="num">0, labelName, OBJPROP_XOFFSET, class="num">5);
  ObjectSetInteger(class="num">0, labelName, OBJPROP_YOFFSET, class="num">0);
}
class=class="str">"cmt">//---- Input Parameters
input class="type">int FastMAPeriod = class="num">14;
input class="type">int SlowMAPeriod = class="num">50;
input class="type">class="kw">double RiskHeightPoints = class="num">5000.0;
input class="type">class="kw">double RewardHeightPoints = class="num">15000.0;

常见问题

多半是对象名称重复或坐标算错被编译器拦了。先把矩形拆成独立脚本单跑,确认坐标和名字唯一再并入主程序。
用背景层对象画交叉点,设 back=true 推到最底层。只在交叉柱画小标记,避免满屏乱点。
可以。小布能读取品种页上的矩形对象,核对名称、坐标和层级,并提示不显示或重叠的异常。
在初始化和退出时调用对象删除函数按前缀清理。养成用统一前缀命名矩形的习惯,清场不误删。
背景层矩形用低透明度色块,价格线用前景细线。调色时别让矩形透明度低于15%,否则容易糊成一片。