构建K线趋势约束模型(第九部分):多策略EA(2)·进阶篇
(2/3)·前文铺了框架,这篇把唐奇安通道拆成可编译模块,解决主EA漏掉突破行情的老问题
「唐奇安通道指标的代码结构拆解」
MT5 自带的标准唐奇安通道指标用三个缓冲区把通道画出来:上轨存指定周期最高价,下轨存同周期最低价,中轨取两者均值。默认输入周期是 20 根 K 线,可在参数里改 InpDonchianPeriod,也能用 InpShowLabel 控制是否在图上标价格。 初始化时 OnInit 里调 SetIndexBuffer 把 0/1/2 号绘图分别绑到 ExtUpBuffer、ExtMdBuffer、ExtDnBuffer,并给每条线设了 1 根偏移(PLOT_SHIFT=1),也就是线条右移一根便于看突破。OnCalculate 里逐根算最高、最低和平均,写进对应缓冲区。 突破逻辑很直接:收盘价站上 ExtUpBuffer 倾向触发买入,跌破 ExtDnBuffer 倾向触发卖出;中轨 ExtMdBuffer 能当趋势过滤器,价格在其上方时才允许做多,可能过滤掉弱势假突破。外汇和贵金属波动大、杠杆高,这套信号只是概率优势,实盘前务必在 MT5 用历史数据验证。 下面把原文指标头部和初始化片段逐行过一遍,方便你直接抄进 MetaEditor 改。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Donchian Channel.mq5 | class=class="str">"cmt">//| Copyright class="num">2009-class="num">2024, MetaQuotes Ltd | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "class="num">2009-class="num">2024, MetaQuotes Ltd" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property description "Donchian Channel" class=class="str">"cmt">//--- class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_buffers class="num">3 class="macro">#class="kw">property indicator_plots class="num">3 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_color1 clrBlue class="macro">#class="kw">property indicator_type2 DRAW_LINE class="macro">#class="kw">property indicator_color2 clrGray class="macro">#class="kw">property indicator_type3 DRAW_LINE class="macro">#class="kw">property indicator_color3 clrRed class=class="str">"cmt">//--- labels class="macro">#class="kw">property indicator_label1 "Upper Donchian" class="macro">#class="kw">property indicator_label2 "Middle Donchian" class="macro">#class="kw">property indicator_label3 "Lower Donchian" class=class="str">"cmt">//--- input parameter input class="type">int InpDonchianPeriod=class="num">20; class=class="str">"cmt">// period of the channel input class="type">bool InpShowLabel =true; class=class="str">"cmt">// show price of the level class=class="str">"cmt">//--- indicator buffers class="type">class="kw">double ExtUpBuffer[]; class="type">class="kw">double ExtMdBuffer[]; class="type">class="kw">double ExtDnBuffer[]; class=class="str">"cmt">//--- unique prefix to identify indicator objects class="type">class="kw">string ExtPrefixUniq; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- define buffers SetIndexBuffer(class="num">0, ExtUpBuffer); SetIndexBuffer(class="num">1, ExtMdBuffer); SetIndexBuffer(class="num">2, ExtDnBuffer); class=class="str">"cmt">//--- set a class="num">1-bar offset for each line PlotIndexSetInteger(class="num">0, PLOT_SHIFT, class="num">1);
唐奇安通道的绘制偏移与循环取值
在 MT5 自定义指标初始化尾部,用 PlotIndexSetInteger 给第 1、2 条绘图线设 PLOT_SHIFT=1,通道线会整体右移一根 K 线,视觉上和价格收线位置错开半步,避免和最新 bar 黏连。 IndicatorSetString 把短名钉成 "Donchian Channel",IndicatorSetInteger 接 _Digits 保证小数位和品种一致;再用 GetTickCount64 取毫秒数截后 4 位拼 "_DN" 当对象前缀,多实例加载不会互相覆盖图形。 OnCalculate 里 prev_calculated 非零就从上一根重算,首次则从 InpDonchianPeriod+1 起步。循环中对每根 bar 用 ArrayMaximum / ArrayMinimum 在最近 N 根 high、low 里抓极值,分别写进上轨、下轨缓冲,中轨取两者均值——这就是通道核心。 若开了 InpShowLabel,会在末 bar 时间位画价格标签并 ChartRedraw 强制重绘;外汇与贵金属杠杆高,通道突破信号仅代表概率倾向,实盘前请在 MT5 策略测试器跑一遍周期参数。
PlotIndexSetInteger(class="num">1, PLOT_SHIFT, class="num">1); PlotIndexSetInteger(class="num">2, PLOT_SHIFT, class="num">1); class=class="str">"cmt">//--- indicator name IndicatorSetString(INDICATOR_SHORTNAME, "Donchian Channel"); class=class="str">"cmt">//--- number of digits of indicator value IndicatorSetInteger(INDICATOR_DIGITS, _Digits); class=class="str">"cmt">//--- prepare prefix for objects class="type">class="kw">string number=StringFormat("%I64d", GetTickCount64()); ExtPrefixUniq=StringSubstr(number, StringLen(number)-class="num">4); ExtPrefixUniq=ExtPrefixUniq+"_DN"; Print("Indicator \"Donchian Channels\" started, prefix=", ExtPrefixUniq); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator iteration function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//--- if the indicator has previously been calculated, start from the bar preceding the last one class="type">int start=prev_calculated-class="num">1; class=class="str">"cmt">//--- if this is the first calculation of the indicator, then move by InpDonchianPeriod bars form the beginning if(prev_calculated==class="num">0) start=InpDonchianPeriod+class="num">1; class=class="str">"cmt">//--- calculate levels for all bars in a loop for(class="type">int i=start; i<rates_total; i++) { class=class="str">"cmt">//--- get max/min values for the last InpDonchianPeriod bars class="type">int highest_bar_index=ArrayMaximum(high, i-InpDonchianPeriod+class="num">1, InpDonchianPeriod); class="type">int lowest_bar_index=ArrayMinimum(low, i-InpDonchianPeriod+class="num">1, InpDonchianPeriod);; class="type">class="kw">double highest=high[highest_bar_index]; class="type">class="kw">double lowest=low[lowest_bar_index]; class=class="str">"cmt">//--- write values into buffers ExtUpBuffer[i]=highest; ExtDnBuffer[i]=lowest; ExtMdBuffer[i]=(highest+lowest)/class="num">2; } class=class="str">"cmt">//--- draw labels on levels if(InpShowLabel) { ShowPriceLevels(time[rates_total-class="num">1], rates_total-class="num">1); ChartRedraw(); }
◍ 退出时清场与右侧价标绘制
指标卸载时若不清图形对象,MT5 子窗口会残留带前缀的箭头标签。OnDeinit 里用 ObjectsDeleteAll(0, ExtPrefixUniq, 0, OBJ_ARROW_RIGHT_PRICE) 按前缀精确删除,再 ChartRedraw(0) 强制重绘,避免下次加载叠影。 ShowPriceLevels 把上轨、中轨、下轨三个缓冲区的最新值分别用蓝、灰、红三色画成右向价格箭头。注意它取的是 last_index 位置的 ExtUpBuffer / ExtMdBuffer / ExtDnBuffer,也就是当前 K 线的通道边界。 ShowRightPrice 先尝试 ObjectCreate,失败才 ObjectMove,说明对象已存在就只更新位置不重建。标签宽度跟着图表缩放走:ChartGetInteger 取 CHART_SCALE,scale>1 时线宽设 2,否则 1;同时把对象设为不可选中、隐藏于对象列表(OBJPROP_HIDDEN=true)、Z序为 0,减少视觉干扰。 外汇与贵金属波动剧烈,通道边界仅反映历史区间,突破后回撤概率不低,实盘前请在 MT5 策略测试器用不同品种验证标签刷新是否卡顿。
class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- class="kw">delete all our graphical objects after use Print("Indicator \"Donchian Channels\" stopped, class="kw">delete all objects with prefix=", ExtPrefixUniq); ObjectsDeleteAll(class="num">0, ExtPrefixUniq, class="num">0, OBJ_ARROW_RIGHT_PRICE); ChartRedraw(class="num">0); } class="type">void ShowPriceLevels(class="type">class="kw">datetime time, class="type">int last_index) { ShowRightPrice(ExtPrefixUniq+"_UP", time, ExtUpBuffer[last_index], clrBlue); ShowRightPrice(ExtPrefixUniq+"_MD", time, ExtMdBuffer[last_index], clrGray); ShowRightPrice(ExtPrefixUniq+"_Dn", time, ExtDnBuffer[last_index], clrRed); } class="type">bool ShowRightPrice(const class="type">class="kw">string name, class="type">class="kw">datetime time, class="type">class="kw">double price, class="type">color clr) { if(!ObjectCreate(class="num">0, name, OBJ_ARROW_RIGHT_PRICE, class="num">0, time, price)) { ObjectMove(class="num">0, name, class="num">0, time, price); class="kw">return(class="kw">false); } class="type">long scale=class="num">2; if(!ChartGetInteger(class="num">0, CHART_SCALE, class="num">0, scale)) { Print(__FUNCTION__+", ChartGetInteger(CHART_SCALE) failed, error = ", GetLastError()); } class="type">int width=scale>class="num">1 ? class="num">2:class="num">1; ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, clr); ObjectSetInteger(class="num">0, name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(class="num">0, name, OBJPROP_WIDTH, width); ObjectSetInteger(class="num">0, name, OBJPROP_BACK, class="kw">false); ObjectSetInteger(class="num">0, name, OBJPROP_SELECTABLE, class="kw">false); ObjectSetInteger(class="num">0, name, OBJPROP_SELECTED, class="kw">false); ObjectSetInteger(class="num">0, name, OBJPROP_HIDDEN, true); ObjectSetInteger(class="num">0, name, OBJPROP_ZORDER, class="num">0); class="kw">return(true); }
「唐奇安通道的三线缓冲填充」
在 MT5 自定义指标里,唐奇安通道的上中下三线靠三个动态数组承接。先声明 ExtUpBuffer、ExtMdBuffer、ExtDnBuffer,再用 SetIndexBuffer 分别绑定到索引 0、1、2,这样绘图引擎才知道每根 K 线对应哪条线。 核心循环从 start 跑到 rates_total。每一根 i 上,用 ArrayMaximum 在 high 数组的 [i-InpDonchianPeriod+1, InpDonchianPeriod] 窗口里找最高价所在 bar 索引,ArrayMinimum 同理找最低价索引——比如 InpDonchianPeriod=20,就是回看含当前 bar 在内的 20 根。 拿到 highest 与 lowest 后,上轨缓冲写最高、下轨缓冲写最低、中轨写两者均值。外汇与贵金属波动剧烈,通道参数错配会频繁假突破,实盘前务必在 MT5 策略测试器用历史数据核对缓冲赋值是否正确。 别把缓冲索引写反 上中下三线绑定到 0/1/2 是约定俗成,若把 ExtDnBuffer 绑到 0,图表会画出上下颠倒的通道,肉眼很难第一时间察觉。
class="type">class="kw">double ExtUpBuffer[]; class="type">class="kw">double ExtMdBuffer[]; class="type">class="kw">double ExtDnBuffer[]; SetIndexBuffer(class="num">0, ExtUpBuffer); SetIndexBuffer(class="num">1, ExtMdBuffer); SetIndexBuffer(class="num">2, ExtDnBuffer); for(class="type">int i=start; i<rates_total; i++) { class=class="str">"cmt">//--- calculate highest and lowest for the Donchian period class="type">int highest_bar_index = ArrayMaximum(high, i-InpDonchianPeriod+class="num">1, InpDonchianPeriod); class="type">int lowest_bar_index = ArrayMinimum(low, i-InpDonchianPeriod+class="num">1, InpDonchianPeriod); class="type">class="kw">double highest = high[highest_bar_index]; class="type">class="kw">double lowest = low[lowest_bar_index]; class=class="str">"cmt">//--- assign values to buffers ExtUpBuffer[i] = highest; ExtDnBuffer[i] = lowest; ExtMdBuffer[i] = (highest + lowest) / class="num">2; }
从零搭一个唐奇安突破EA
唐奇安通道是 MT5 内置指标,写突破逻辑时只需盯它的上下轨缓冲区。先在 MetaEditor 5 建个空 EA 项目,取名 BreakoutEA,初始模板只勾基本项、带 #property strict 指令,逼编译器做类型检查,顺手把交易库 include 进来,地基就算打好了。 初始化阶段用 iCustom() 加载放在 Free Indicators\\Donchian Channel 子目录的通道指标,返回 INVALID_HANDLE 就直接报错退出,避免后面空跑。输入参数先留四个:通道周期(默认 20)、风险回报比(默认 1.5)、手数、止损获利点数。 OnDeinit() 里调 IndicatorRelease() 释放句柄,防内存泄漏;OnTick() 每次报价触发,先用 PositionsTotal() 确认无持仓才去判信号,天然屏蔽同屏多开。信号本身极简:CopyBuffer() 取最新上下轨,收盘价破上轨做多、破下轨做空,止损获利按输入点数算。 下单走 CTrade 类封装的买卖函数,失败就打印错误。独立回测里这套裸突破能正常撮合订单,但外汇和贵金属波动无序,裸突破在震荡市亏钱概率偏高,下一阶段必须接趋势过滤。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| BreakoutEA.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 copyright "Copyright class="num">2024, Clemence Benjamin" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class=class="str">"cmt">//--- 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">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// Input parameters input class="type">int InpDonchianPeriod = class="num">20; class=class="str">"cmt">// Period for Donchian Channel input class="type">class="kw">double RiskRewardRatio = class="num">1.5; class=class="str">"cmt">// Risk-to-reward ratio
◍ EA 初始化失败的典型日志与资源回收
把唐奇安通道 EA 挂上 MT5 前,先确认自定义指标路径是否为 Free Indicators\Donchian Channel。OnInit 里用 iCustom 加载,若返回 INVALID_HANDLE 会直接 INIT_FAILED,EA 连第一根 K 线都跑不了。
下面这段是实盘回测里真实打出来的 Journal:2024.10.20 08:49:04.117 报 cannot load custom indicator 'Donchian Channel' [4802],紧接着 OnInit critical error、tester stopped because OnInit failed。也就是说指标没进根目录的 indicators 存储,EA 在 2022.01.01 的起始点就被掐断。
外汇与贵金属杠杆高,EA 起不来就谈不上风控,先排环境再谈策略。OnDeinit 里调 IndicatorRelease(handle) 把句柄释放掉,避免重复加载拖慢终端;OnTick 则用 PositionsTotal()==0 做无持仓闸门,才进 CheckTradingConditions 读上下轨缓冲。
读通道值前需给 ExtUpBuffer / ExtDnBuffer 留 2 格空间,ArrayResize 成 2 后再取最新数据,否则可能越界报错。
input class="type">class="kw">double LotSize = class="num">0.1; class=class="str">"cmt">// Default lot size for trading input class="type">class="kw">double pipsToStopLoss = class="num">15; class=class="str">"cmt">// Stop loss in pips input class="type">class="kw">double pipsToTakeProfit = class="num">30; class=class="str">"cmt">// Take profit in pips class=class="str">"cmt">// Indicator handle storage class="type">int handle; class="type">class="kw">string indicatorKey; class=class="str">"cmt">// Expert initialization function class="type">int OnInit() { class=class="str">"cmt">// Create a unique key for the indicator based on the symbol and period indicatorKey = StringFormat("%s_%d", Symbol(), InpDonchianPeriod); class=class="str">"cmt">// Load the Donchian Channel indicator handle = iCustom(Symbol(), Period(), "Free Indicators\\Donchian Channel", InpDonchianPeriod); class=class="str">"cmt">// Check if the indicator loaded successfully if (handle == INVALID_HANDLE) { Print("Failed to load the indicator. Error: ", GetLastError()); class="kw">return INIT_FAILED; } class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//Typical Journal log when the EA fails to locate an indicator in the root indicators storage. class="num">2024.10.class="num">20 class="num">08:class="num">49:class="num">04.117 class="num">2022.01.class="num">01 class="num">00:class="num">00:class="num">00 cannot load custom indicator &class="macro">#x27;Donchian Channel&class="macro">#x27; [class="num">4802] class="num">2024.10.class="num">20 class="num">08:class="num">49:class="num">04.118 class="num">2022.01.class="num">01 class="num">00:class="num">00:class="num">00 indicator create error in &class="macro">#x27;DonchianEA.mq5&class="macro">#x27; (class="num">1,class="num">1) class="num">2024.10.class="num">20 class="num">08:class="num">49:class="num">04.118 OnInit critical error class="num">2024.10.class="num">20 class="num">08:class="num">49:class="num">04.118 tester stopped because OnInit failed class=class="str">"cmt">// Expert deinitialization function class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">// Release the indicator handle to free up resources IndicatorRelease(handle); } class=class="str">"cmt">// Main execution function with block-based control class="type">void OnTick() { class=class="str">"cmt">// Check if any positions are currently open if (PositionsTotal() == class="num">0) { CheckTradingConditions(); } } class=class="str">"cmt">// Check trading conditions based on indicator buffers class="type">void CheckTradingConditions() { class="type">class="kw">double ExtUpBuffer[], ExtDnBuffer[]; class=class="str">"cmt">// Buffers for upper and lower Donchian bands class=class="str">"cmt">// Resize buffers to hold the latest data ArrayResize(ExtUpBuffer, class="num">2); ArrayResize(ExtDnBuffer, class="num">2); class=class="str">"cmt">// Get the latest values from the Donchian Channel