构建K线图趋势约束模型(第8部分):EA的开发(一)·进阶篇
(2/3)· 指标卡顿、矩形不显的坑已填平,这一篇把手动脚本逻辑搬进EA雏形
不少人把指标缓冲区堆到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。外汇与贵金属波动剧烈,这类可视化仅辅助判读,实际仓位仍可能因滑点偏离矩形边界。
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) —— 把传入的文字写进标签。
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 是否已有未平仓,再从两个缓冲复制最新值,条件满足就填交易请求发单。回测用策略测试器跑,模拟账户验证后再上实盘——外汇和贵金属杠杆高,实盘前必须确认缓冲读取和发单逻辑在真实点差下不漂移。
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 减点。外汇与贵金属杠杆高,这类市价单在跳空时段滑点可能扩大,先用策略测试器跑历史再上模拟盘。
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 数据。
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% 风险匹配的值,比死磕指标参数更迫切。
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());