构建K线图趋势约束模型(第8部分):EA的开发(一)(基础篇)
「先把风险和回报矩形画稳再谈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。
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 根的交叉分布。
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。
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 策略测试器用历史数据核对绘制逻辑。
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;