MQL5交易策略自动化(第二十部分):基于CCI和AO指标的多品种策略·进阶篇
(2/3)·单品种脚本在多货币对上频繁漏信号?这篇教你用类结构一次管全市场
「策略类的骨架与调试开关」
多品种 EA 的核心往往落在一个交易策略类上。下面这段声明把品种数据、参数、交易日标记和 CCI 阈值都收进私有成员,相当于给后续信号计算留好了插槽。 CTradingStrategy 里 m_symbols[] 存品种数组,m_array_size 跟踪实际个数,m_last_day 与 m_is_new_day 配合用来识别跨日。CCI_TREND_BUY_VALUE 和 CCI_TREND_SELL_VALUE 是两个 const int 阈值常量,后面判断趋势方向时直接比对。 调试输出做了两层拦截:PrintDebug 只在 debug_mode 为真且不在优化器中才打印;PrintMessage 则屏蔽掉优化阶段的冗余日志。这样回测跑几千代时不会把终端刷爆。 PrepareSymbolsList 用 StringSplit 按逗号切分参数字符串,sep 取 ',' 的字符码。m_array_size 就是切出来的有效品种数,后面循环初始化 m_symbols 就靠它。外汇与贵金属波动剧烈,多品种同时跑务必先在策略测试器里用少量品种验证数组越界风险。
class="type">class="kw">double ao_signal_data[]; class=class="str">"cmt">//--- Store AO signal data class="type">class="kw">double ao_trend_data[]; class=class="str">"cmt">//--- Store AO trend data }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Trading Strategy Class | //--- Implement a class for trading strategy logic class=class="str">"cmt">//+------------------------------------------------------------------+ class CTradingStrategy { class="kw">private: CTrade m_trade; class=class="str">"cmt">//--- Initialize trade object for trading operations TradingParameters m_params; class=class="str">"cmt">//--- Store trading parameters SymbolData m_symbols[]; class=class="str">"cmt">//--- Store array of symbol data class="type">int m_array_size; class=class="str">"cmt">//--- Track number of symbols class="type">class="kw">datetime m_last_day; class=class="str">"cmt">//--- Store last day timestamp class="type">bool m_is_new_day; class=class="str">"cmt">//--- Indicate new day detection class="type">int m_candle_shift; class=class="str">"cmt">//--- Set candle shift for signal calculation class="kw">const class="type">int CCI_TREND_BUY_VALUE; class=class="str">"cmt">//--- Define constant for CCI trend buy threshold class="kw">const class="type">int CCI_TREND_SELL_VALUE; class=class="str">"cmt">//--- Define constant for CCI trend sell threshold } class="type">void PrintDebug(class="type">class="kw">string text) { class=class="str">"cmt">//--- Define method to print debug messages if(m_params.debug_mode && !MQLInfoInteger(MQL_OPTIMIZATION)) { class=class="str">"cmt">//--- Check debug mode and optimization status Print(text); class=class="str">"cmt">//--- Output debug message } } class="type">void PrintMessage(class="type">class="kw">string text) { class=class="str">"cmt">//--- Define method to print informational messages if(!MQLInfoInteger(MQL_OPTIMIZATION)) { class=class="str">"cmt">//--- Check if not in optimization mode Print(text); class=class="str">"cmt">//--- Output message } } class="type">void PrepareSymbolsList() { class=class="str">"cmt">//--- Define method to prepare symbol list class="type">class="kw">string symbols_array[]; class=class="str">"cmt">//--- Initialize temporary array for symbols class="type">class="kw">ushort sep = StringGetCharacter(",", class="num">0); class=class="str">"cmt">//--- Get comma separator character m_array_size = StringSplit(m_params.symbols, sep, symbols_array); class=class="str">"cmt">//--- Split symbols class="type">class="kw">string into array
多品种下指标句柄的批量初始化
做跨品种扫描时,先把符号数组按 m_array_size 扩容,再逐个把名称写进结构体、把 last_bar_time 清零,并用 SymbolSelect 保证该品种已在市场报价窗内。否则后面取指标数据会直接拿到空序列。 初始化函数 InitializeIndicators 对每个品种分别挂 4 个句柄:信号周期与趋势周期的 CCI(iCCI)、信号周期与趋势周期的 AO(iAO)。任一句柄返回 INVALID_HANDLE 就打印失败品种并回 false,避免半初始化状态跑后续逻辑。 挂完句柄后必须用 ArraySetAsSeries 把四个数据缓冲都设为时间序列(最新柱在索引 0)。外汇与贵金属波动剧烈、点差跳变频繁,若序列方向错乱,CCI/AO 的穿越判断会整体反向,实盘可能触发错误信号。 下面这段是核心初始化的可直接粘进 EA 的结构:先扩数组填符号,再按索引建指标。开 MT5 把 m_params 的周期参数改成自己常用的 H1/H4 组合,能立刻验证多品种加载是否报错。
ArrayResize(m_symbols, m_array_size); class=class="str">"cmt">//--- Resize symbol data array for(class="type">int i = class="num">0; i < m_array_size; i++) { class=class="str">"cmt">//--- Iterate through symbols m_symbols[i].name = symbols_array[i]; class=class="str">"cmt">//--- Set symbol name m_symbols[i].last_bar_time = class="num">0; class=class="str">"cmt">//--- Initialize last bar time SymbolSelect(m_symbols[i].name, true); class=class="str">"cmt">//--- Ensure symbol is in market watch } } class="type">bool InitializeIndicators(class="type">int index) { class=class="str">"cmt">//--- Define method to initialize indicators m_symbols[index].cci_signal_handle = iCCI(m_symbols[index].name, m_params.tf_signal, m_params.per_signal_cci, m_params.price_cci); class=class="str">"cmt">//--- Create CCI signal indicator if(m_symbols[index].cci_signal_handle == INVALID_HANDLE) { class=class="str">"cmt">//--- Check for invalid handle Print("INITIALIZATION OF CCI SIGNAL FAILED: ", m_symbols[index].name); class=class="str">"cmt">//--- Log error class="kw">return class="kw">false; class=class="str">"cmt">//--- Return failure } m_symbols[index].cci_trend_handle = iCCI(m_symbols[index].name, m_params.tf_trend, m_params.per_trend_cci, m_params.price_cci); class=class="str">"cmt">//--- Create CCI trend indicator if(m_symbols[index].cci_trend_handle == INVALID_HANDLE) { class=class="str">"cmt">//--- Check for invalid handle Print("INITIALIZATION OF CCI TREND FAILED: ", m_symbols[index].name); class=class="str">"cmt">//--- Log error class="kw">return class="kw">false; class=class="str">"cmt">//--- Return failure } m_symbols[index].ao_signal_handle = iAO(m_symbols[index].name, m_params.tf_signal); class=class="str">"cmt">//--- Create AO signal indicator if(m_symbols[index].ao_signal_handle == INVALID_HANDLE) { class=class="str">"cmt">//--- Check for invalid handle Print("INITIALIZATION OF AO SIGNAL FAILED: ", m_symbols[index].name); class=class="str">"cmt">//--- Log error class="kw">return class="kw">false; class=class="str">"cmt">//--- Return failure } m_symbols[index].ao_trend_handle = iAO(m_symbols[index].name, m_params.tf_trend); class=class="str">"cmt">//--- Create AO trend indicator if(m_symbols[index].ao_trend_handle == INVALID_HANDLE) { class=class="str">"cmt">//--- Check for invalid handle Print("INITIALIZATION OF AO TREND FAILED: ", m_symbols[index].name); class=class="str">"cmt">//--- Log error class="kw">return class="kw">false; class=class="str">"cmt">//--- Return failure } ArraySetAsSeries(m_symbols[index].cci_signal_data, true); class=class="str">"cmt">//--- Set CCI signal data as series ArraySetAsSeries(m_symbols[index].cci_trend_data, true); class=class="str">"cmt">//--- Set CCI trend data as series ArraySetAsSeries(m_symbols[index].ao_signal_data, true); class=class="str">"cmt">//--- Set AO signal data as series ArraySetAsSeries(m_symbols[index].ao_trend_data, true); class=class="str">"cmt">//--- Set AO trend data as series class="kw">return true; class=class="str">"cmt">//--- Return success }
◍ 多品种指标缓冲与持仓统计的实现骨架
做跨品种监控时,先把每个品种的 CCI 与 AO 双线信号缓冲拉进本地数组,是后续比价的地基。下面这段更新函数一次性拷贝 4 个句柄的最近 3 根数据,任意一处不足 3 根就直接返回失败并打印品种名,避免脏数据进入判断逻辑。
class="type">bool UpdateIndicatorData(class="type">int index) { if(CopyBuffer(m_symbols[index].cci_signal_handle, class="num">0, class="num">0, class="num">3, m_symbols[index].cci_signal_data) < class="num">3) { Print("UNABLE TO COPY CCI SIGNAL DATA: ", m_symbols[index].name); class="kw">return class="kw">false; } if(CopyBuffer(m_symbols[index].cci_trend_handle, class="num">0, class="num">0, class="num">3, m_symbols[index].cci_trend_data) < class="num">3) { Print("UNABLE TO COPY CCI TREND DATA: ", m_symbols[index].name); class="kw">return class="kw">false; } if(CopyBuffer(m_symbols[index].ao_signal_handle, class="num">0, class="num">0, class="num">3, m_symbols[index].ao_signal_data) < class="num">3) { Print("UNABLE TO COPY AO SIGNAL DATA: ", m_symbols[index].name); class="kw">return class="kw">false; } if(CopyBuffer(m_symbols[index].ao_trend_handle, class="num">0, class="num">0, class="num">3, m_symbols[index].ao_trend_data) < class="num">3) { Print("UNABLE TO COPY AO TREND DATA: ", m_symbols[index].name); class="kw">return class="kw">false; } class="kw">return true; }
class="type">int CountOrders(class="type">class="kw">string symbol, class="type">int magic, class="type">ENUM_POSITION_TYPE type) { class="type">int count = class="num">0; for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) { class="type">class="kw">ulong ticket = PositionGetTicket(i); if(PositionSelectByTicket(ticket)) { if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_TYPE) == type) {
class="type">bool UpdateIndicatorData(class="type">int index) { if(CopyBuffer(m_symbols[index].cci_signal_handle, class="num">0, class="num">0, class="num">3, m_symbols[index].cci_signal_data) < class="num">3) { Print("UNABLE TO COPY CCI SIGNAL DATA: ", m_symbols[index].name); class="kw">return class="kw">false; } if(CopyBuffer(m_symbols[index].cci_trend_handle, class="num">0, class="num">0, class="num">3, m_symbols[index].cci_trend_data) < class="num">3) { Print("UNABLE TO COPY CCI TREND DATA: ", m_symbols[index].name); class="kw">return class="kw">false; } if(CopyBuffer(m_symbols[index].ao_signal_handle, class="num">0, class="num">0, class="num">3, m_symbols[index].ao_signal_data) < class="num">3) { Print("UNABLE TO COPY AO SIGNAL DATA: ", m_symbols[index].name); class="kw">return class="kw">false; } if(CopyBuffer(m_symbols[index].ao_trend_handle, class="num">0, class="num">0, class="num">3, m_symbols[index].ao_trend_data) < class="num">3) { Print("UNABLE TO COPY AO TREND DATA: ", m_symbols[index].name); class="kw">return class="kw">false; } class="kw">return true; } class="type">int CountOrders(class="type">class="kw">string symbol, class="type">int magic, class="type">ENUM_POSITION_TYPE type) { class="type">int count = class="num">0; for(class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) { class="type">class="kw">ulong ticket = PositionGetTicket(i); if(PositionSelectByTicket(ticket)) { if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_TYPE) == type) {
「策略初始化与下单函数的代码骨架」
这段代码片段把多品种 CCI 趋势策略的底层动作拆开了:先数已有订单,再封装一个带日志的下单函数,最后在构造函数和 Init() 里落定参数。直接复制到 MT5 的 EA 头文件里就能跑通编译,但逻辑是否盈利得你自己回测。 count++ 那几行处在某个遍历持仓的循环末尾,每匹配到一个符合条件的订单计数器加一,最终 return count 把当前策略管理的订单数交出去。注意它只返回数量,不区分多空,统计日内持仓密度时得自己加过滤。 OpenOrder() 用 m_trade.PositionOpen 发市价单,失败走 PrintMessage 打错误码,成功打成交明细。价格、SL、TP 全用 %.5f 精度记录,对黄金这类小数位多的品种比对 EURUSDm 更必要。 构造函数里写死了 CCI_TREND_BUY_VALUE(-114) 和 CCI_TREND_SELL_VALUE(134),但 Init() 又把信号阈值设成 -90 和 130,两组常数不统一,实盘前必须确认以哪组为准。Init() 默认盯 EURUSDm、GBPUSDm、AUDUSDm 三个货币对,信号 CCI 周期 20、趋势 CCI 周期 24,信号看 M5、趋势看 H1——外汇和贵金属杠杆高,参数直接上实盘可能触发连续止损。
count++;
}
}
class="kw">return count;
}
class="type">long OpenOrder(class="type">class="kw">string symbol, ENUM_ORDER_TYPE type, class="type">class="kw">double price, class="type">class="kw">double sl, class="type">class="kw">double tp, class="type">class="kw">double lots, class="type">int magic, class="type">class="kw">string comment) {
class="type">long ticket = m_trade.PositionOpen(symbol, type, lots, price, sl, tp, comment);
if(ticket < class="num">0) {
PrintMessage(StringFormat("Info - OrderSend %s %d_%s_%.5f error %.5f_%.5f_%.5f_#%d",
comment, type, symbol, price, price, sl, tp, GetLastError()));
} else {
PrintMessage(StringFormat("Info - OrderSend done. Comment:%s, Type:%d, Sym:%s, Price:%.5f, SL:%.5f, TP:%.5f",
comment, type, symbol, price, sl, tp));
}
class="kw">return ticket;
}
class="kw">public:
CTradingStrategy() : CCI_TREND_BUY_VALUE(-class="num">114), CCI_TREND_SELL_VALUE(class="num">134) {
m_last_day = class="num">0;
m_is_new_day = true;
m_array_size = class="num">0;
}
class="type">bool Init() {
m_params.symbols = "EURUSDm,GBPUSDm,AUDUSDm";
m_params.per_signal_cci = class="num">20;
m_params.per_trend_cci = class="num">24;
m_params.price_cci = PRICE_TYPICAL;
m_params.cci_signal_buy_value = -class="num">90;
m_params.cci_signal_sell_value = class="num">130;
m_params.tf_signal = PERIOD_M5;
m_params.tf_trend = PERIOD_H1;EA 初始化时的参数与品种装载
这段初始化逻辑把一套 AO+突破策略的默认参数直接写死在结构体里,先看清几个关键数字:take-profit 设 200 点、stop-loss 设 300 点,仓位固定 0.01 手,滑点容忍 5 点,最大点差限制 20 点。AO 趋势过滤默认关闭(use_ao_for_trend=false),说明基础版本不靠 AO 方向筛单,只靠价格行为突破。 代码里 magic number 写死 123456789,订单注释标 "EA_AO_BP",方便在 MT5 终端里把这套 EA 的成交和其他脚本区分开。trade_anytime=false 时,m_candle_shift 被赋为 1,意味着信号判定会跳过当前未完成 K 线,只用已闭合的上一根——这是避免盘中假突破的硬约束。 初始化末尾会跑 PrepareSymbolsList() 并逐个 InitializeIndicators(i),任一品种指标句柄创建失败就直接 return false,EA 起不来。最后 PrintMessage 把当前品种点差用整数打进日志,你可以在策略测试器或实盘日志里核对:若点差超过 20,后续开单逻辑大概率被拦。外汇与贵金属杠杆高,点差跳变常发生在数据行情,这类写死阈值要先在模拟盘验证。
m_params.use_ao_for_trend = class="kw">false; class=class="str">"cmt">//--- Disable AO trend by class="kw">default m_params.take = class="num">200; class=class="str">"cmt">//--- Set take-profit m_params.stop = class="num">300; class=class="str">"cmt">//--- Set stop-loss m_params.lots = class="num">0.01; class=class="str">"cmt">//--- Set lot size m_params.slip = class="num">5; class=class="str">"cmt">//--- Set slippage m_params.max_spread = class="num">20; class=class="str">"cmt">//--- Set maximum spread m_params.magic = class="num">123456789; class=class="str">"cmt">//--- Set magic number m_params.trade_anytime = class="kw">false; class=class="str">"cmt">//--- Disable trade anytime m_params.comment = "EA_AO_BP"; class=class="str">"cmt">//--- Set trade comment m_params.tester_max_balance = class="num">0; class=class="str">"cmt">//--- Set tester balance limit m_params.debug_mode = class="kw">false; class=class="str">"cmt">//--- Disable debug mode m_candle_shift = m_params.trade_anytime ? class="num">0 : class="num">1; class=class="str">"cmt">//--- Set candle shift based on trade mode m_trade.SetExpertMagicNumber(m_params.magic); class=class="str">"cmt">//--- Set magic number for trade object PrepareSymbolsList(); class=class="str">"cmt">//--- Prepare symbol list for(class="type">int i = class="num">0; i < m_array_size; i++) { class=class="str">"cmt">//--- Iterate through symbols if(!InitializeIndicators(i)) { class=class="str">"cmt">//--- Initialize indicators class="kw">return class="kw">false; class=class="str">"cmt">//--- Return failure on error } } PrintMessage("Current Spread on " + Symbol() + ": " + class=class="str">"cmt">//--- Log current spread IntegerToString((class="type">int)SymbolInfoInteger(Symbol(), SYMBOL_SPREAD))); class="kw">return true; class=class="str">"cmt">//--- Return success } class="type">void OnTick() { class=class="str">"cmt">//--- Define tick handling method class="type">class="kw">datetime new_day = iTime(Symbol(), PERIOD_D1, class="num">0); class=class="str">"cmt">//--- Get current day timestamp m_is_new_day = (m_last_day != new_day); class=class="str">"cmt">//--- Check for new day if(m_is_new_day) { class=class="str">"cmt">//--- Handle new day
◍ 多品种信号循环的收口写法
这段逻辑跑在 EA 的 OnTick 尾部,负责把前面算好的 CCI、AO 数据落到具体品种上。外层 for 循环从 i=0 扫到 m_array_size-1,也就是你监控的品种总数——如果你在配置里挂了 10 个货币对,这里一跳价就轮询 10 遍,CPU 占用随品种数线性走。 new_time 用 iTime 取信号周期 0 号 K 线的开盘时间,只有跟 m_symbols[i].last_bar_time 不同才置 is_new_bar。trade_anyime 关掉时,信号只在新柱触发,避免一根柱内重复发单;实盘里黄金 M5 一根柱平均 5~8 跳报价,不锁新柱会刷出一堆同方向市价单。 ask/bid/point/spread 全用 SymbolInfoDouble 现取,spread 却用了 Symbol() 而非 m_symbols[i].name——这是隐性 bug,多品种下 spread 永远读的是当前图表的品种,跨品种点差判断会失真。 buy_signal 要求 CCI 上穿买入阈值且 AO 前一柱低于当前柱,sell_signal 反之。注意 m_candle_shift+1 是前一柱、m_candle_shift 是当前柱,偏移写错就变成用未来柱验证过去,回测会虚高。外汇和贵金属杠杆高,这类信号只是概率倾向,真跑之前先在 MT5 策略测试器用 2023 全年数据过一遍。
m_last_day = new_day; class=class="str">"cmt">//--- Update last day } for(class="type">int i = class="num">0; i < m_array_size; i++) { class=class="str">"cmt">//--- Iterate through symbols class="type">bool is_new_bar = class="kw">false; class=class="str">"cmt">//--- Initialize new bar flag class="type">bool buy_signal = class="kw">false, sell_signal = class="kw">false; class=class="str">"cmt">//--- Initialize signal flags class="type">bool buy_trend = class="kw">false, sell_trend = class="kw">false; class=class="str">"cmt">//--- Initialize trend flags class="type">class="kw">datetime new_time = iTime(m_symbols[i].name, m_params.tf_signal, class="num">0); class=class="str">"cmt">//--- Get current bar time if(!m_params.trade_anytime && m_symbols[i].last_bar_time != new_time) { class=class="str">"cmt">//--- Check for new bar is_new_bar = true; class=class="str">"cmt">//--- Set new bar flag m_symbols[i].last_bar_time = new_time; class=class="str">"cmt">//--- Update last bar time } if(!UpdateIndicatorData(i)) class="kw">continue; class=class="str">"cmt">//--- Update indicators, skip on failure class="type">class="kw">double ask = SymbolInfoDouble(m_symbols[i].name, SYMBOL_ASK); class=class="str">"cmt">//--- Get ask price class="type">class="kw">double bid = SymbolInfoDouble(m_symbols[i].name, SYMBOL_BID); class=class="str">"cmt">//--- Get bid price class="type">class="kw">double point = SymbolInfoDouble(m_symbols[i].name, SYMBOL_POINT); class=class="str">"cmt">//--- Get point value class="type">long spread = SymbolInfoInteger(Symbol(), SYMBOL_SPREAD); class=class="str">"cmt">//--- Get current spread class="type">int total_orders = CountOrders(m_symbols[i].name, m_params.magic, POSITION_TYPE_BUY) + class=class="str">"cmt">//--- Count buy orders CountOrders(m_symbols[i].name, m_params.magic, POSITION_TYPE_SELL); class=class="str">"cmt">//--- Count sell orders class=class="str">"cmt">// Generate signals buy_signal = m_symbols[i].cci_signal_data[m_candle_shift+class="num">1] < m_params.cci_signal_buy_value && class=class="str">"cmt">//--- Check CCI buy signal condition m_symbols[i].cci_signal_data[m_candle_shift] > m_params.cci_signal_buy_value && class=class="str">"cmt">//--- Confirm CCI buy signal m_symbols[i].ao_signal_data[m_candle_shift+class="num">1] < m_symbols[i].ao_signal_data[m_candle_shift]; class=class="str">"cmt">//--- Confirm AO buy signal sell_signal = m_symbols[i].cci_signal_data[m_candle_shift+class="num">1] > m_params.cci_signal_sell_value && class=class="str">"cmt">//--- Check CCI sell signal condition m_symbols[i].cci_signal_data[m_candle_shift] < m_params.cci_signal_sell_value && class=class="str">"cmt">//--- Confirm CCI sell signal m_symbols[i].ao_signal_data[m_candle_shift+class="num">1] > m_symbols[i].ao_signal_data[m_candle_shift]; class=class="str">"cmt">//--- Confirm AO sell signal buy_trend = m_symbols[i].cci_trend_data[m_candle_shift+class="num">1] < m_symbols[i].cci_signal_data[m_candle_shift] && class=class="str">"cmt">//--- Check CCI trend buy condition