开发多币种 EA 交易(第 24 部分):添加新策略(一)(基础篇)
◍ 多币种 EA 里先塞进一个策略骨架
做多币种 EA 最忌一上来就把十几个货币对的逻辑揉成一团。更稳的起步方式,是先在一个基础框架里只挂一个策略,跑通之后再加第二个、第三个。 以 MT5 为例,EA 初始化时先声明策略容器,在 OnTick 里按品种循环,但当前只调用其中一条策略函数。这样你能在单一品种上验证信号质量,再决定要不要放开到其他品种。 外汇与贵金属多币种运行存在滑点叠加和保证金连锁风险,实盘前务必在策略测试器用真实点差回测。MT5 策略测试器支持多币种模式,但默认不开启,需要手动勾选对应交易品种。
「自动优化系统的文件骨架」
上一阶段我们已经把自动优化体系跑通:核心是一个记录优化项目的数据库,配合项目创建脚本,可以把指定策略(当时用的是 SimpleVolumes)的优化任务批量建出来。该脚本本身不绑定策略,当作模板改改就能套到其他 EA 上。 项目收尾时加了自动导出能力——把筛过的策略组写进独立的「EA 数据库」。最终 EA 直接读这个库来热更新参数,不用重新编译,于是测试器里能模拟出「优化结果中途多次刷新」的真实工况。 文件结构也理顺了:所有与策略类型无关的支持文件挪到 MQL5/Include 当库,MQL5/Experts 只留阶段 EA、最终 EA 和建项目的脚本。当时 SimpleVolumes 源码仍留在库里,因为重点在验证最终 EA 的参数自动更新机制,编译时连哪都无所谓。 现在换一个全新策略接进这套系统,要生成阶段 EA 和最终 EA,第一步得先厘清手头缺哪些拼图。
从单策略代码到优化流水线的落地路径
先挑一个简单策略写成代码,丢进 Advisor 库的项目工作文件夹,这一步是后面所有自动化的地基。代码成型后就能搭第一阶段的 EA,专门用来优化这个策略单实例的参数;麻烦点在于库代码和项目代码得拆干净,否则后面复用会互相污染。 第二、三阶段的 EA 几乎可以照搬之前写的,因为库层没绑定具体策略类,只要在工作文件夹的代码里加一条 include 新策略文件的指令即可。但优化数据库里负责建项目的 EA 脚本必须改——起码第一阶段 EA 的输入参数模板得重做,新策略的参数集合和老的肯定对不上。 改完建库 EA 就能跑起来:先建优化数据库、把本项目的优化任务塞进去,再开自动优化输送机干等。耗时不是闹着玩的,它跟三个量正相关或反相关:优化时间跨度越长越慢,策略逻辑越复杂越慢,而测试代理数越多越快。 流水线跑完,最后拿最终 EA 进策略测试器跑一遍,看优化结果实不实。外汇与贵金属市场杠杆高、滑点跳空频繁,优化回测漂亮不代表实盘概率稳,开 MT5 照这条路径搭一遍最直观。
◍ 连续同向烛形后的反向开仓逻辑
假设某个品种连续出现 N 根同方向收盘的烛形,下一根反向收盘的概率会略高。基于这一价格现象,可在新柱开启、前 signalSeqLen 根已关闭烛形方向一致时反向挂单:阳线序列后做卖,阴线序列后做买。 开仓时机锁定为“新柱形成”,因为方向判定只作用于已收盘烛形,避免未来函数。平仓采用最朴素的止损止盈触碰机制,不中途人工干预。若已有未平仓位且信号再现,只要当前持仓数小于 maxCountOfOrders,可继续加仓,不限制多空同时持有。 止损止盈有两种算法:periodATR=0 时直接按 stopLevel、takeLevel 点数偏移;periodATR>0 时先算日线 ATR,再按 ATR*stopLevel、ATR*takeLevel 偏移挂线。连续单向序列在真实图表中很少超过 10 根,signalSeqLen 实盘验证上限建议设在 10 以内。 外汇与贵金属杠杆高、滑点跳空频繁,连续同向反转仅为概率倾向,不是确定性结构。参数可在 MT5 策略测试器里跑优化,仓位规模交给自动优化以 10% 回撤为目标去乘最小手数,不必手填。
「从成交量策略派生出同向蜡烛策略类」
在 MT5 里做策略原型,最快的路子不是重写,而是继承。CSimpleVolumesStrategy 已经跑通了父类 CVirtualStrategy 的虚拟仓位框架,我们只要新建 CSimpleCandlesStrategy 并声明为 CVirtualStrategy 的子类,就能直接复用 m_orders 数组和虚拟开仓逻辑。 新类需要自己管的字段就几样:交易品种 m_symbol、周期 m_timeframe、同向蜡烛根数 m_signalSeqLen、ATR 周期 m_periodATR,以及止损 m_stopLevel、止盈 m_takeLevel 和最大持仓数 m_maxCountOfOrders。为了拿品种属性,还得挂一个 CSymbolInfo 指针。 构造函数从初始化字符串读参,靠的是 CFactorable 里的 ReadString / ReadLong。读不出错,IsValid() 就返回 true。虚拟仓位实例数量由 m_maxCountOfOrders 决定,用 CVirtualReceiver::Get() 一次性塞进 m_orders。 策略只在「新柱」触发,所以构造函数里要建一个新品种+周期的新柱检测器;同时向品种监视器申请 CSymbolInfo 信息对象。下面这段头文件和构造片段,直接放进 Strategies/SimpleCandlesStrategy.mqh 就能编译。 止损止盈的计算分两路:m_periodATR 为 0 时,m_stopLevel / m_takeLevel 被当作点数原样写进 m_sl / m_tp;非 0 时按 ATR 的对应比例乘出来。外汇和贵金属杠杆高,ATR 随波动率跳变,实盘前务必在策略测试器里用不同 m_periodATR 跑一遍验证滑点敏感度为妥。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Trading strategy class="kw">using unidirectional candlesticks | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSimpleCandlesStrategy : class="kw">public CVirtualStrategy { class="kw">protected: class="type">class="kw">string m_symbol; class=class="str">"cmt">// Symbol(trading instrument) ENUM_TIMEFRAMES m_timeframe; class=class="str">"cmt">// Chart period(timeframe) class=class="str">"cmt">//--- Open signal parameters class="type">int m_signalSeqLen; class=class="str">"cmt">// Number of unidirectional candles class="type">int m_periodATR; class=class="str">"cmt">// ATR period class=class="str">"cmt">//--- Position parameters class="type">class="kw">double m_stopLevel; class=class="str">"cmt">// Stop Loss(in points or % ATR) class="type">class="kw">double m_takeLevel; class=class="str">"cmt">// Take Profit(in points or % ATR) class=class="str">"cmt">//--- Money management parameters class="type">int m_maxCountOfOrders; class=class="str">"cmt">// Max number of simultaneously open positions CSymbolInfo *m_symbolInfo; class=class="str">"cmt">// Object for getting information about the symbol properties class=class="str">"cmt">// ... class="kw">public: class=class="str">"cmt">// Constructor CSimpleCandlesStrategy(class="type">class="kw">string p_params); class="kw">virtual class="type">class="kw">string class="kw">operator~() class="kw">override; class=class="str">"cmt">// Convert object to class="type">class="kw">string class="kw">virtual class="type">void Tick() class="kw">override; class=class="str">"cmt">// OnTick event handler }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CSimpleCandlesStrategy::CSimpleCandlesStrategy(class="type">class="kw">string p_params) { class=class="str">"cmt">// Read parameters from the initialization class="type">class="kw">string m_params = p_params; m_symbol = ReadString(p_params); m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
把信号逻辑接进 Tick 生命周期
策略初始化收尾时,会从参数流里按固定顺序读出信号序列长度、ATR 周期、止损止盈比例和最大持仓数。这几个值直接决定后面 SignalForOpen 里 CopyRates 要抓多少根 K 线,以及同屏最多挂几单,改一个参数就可能让开仓频率完全不同。 校验通过后,代码向虚拟持仓管理器按最大订单数预申请对象,并绑定 symbol 的行情监控实例。真正驱动交易的是 Tick():每来一个新 bar,且当前订单数低于上限,才去取 SignalForOpen 的返回值,等于 1 开 BUY、等于 -1 开 SELL_STOP,其余情况不动。 SignalForOpen 目前骨架里默认 signal=0,仅当 CopyRates(m_symbol, m_timeframe, 1, m_signalSeqLen, rates) 的返回条数严格等于 m_signalSeqLen 时才先置为 1(买信号),后续还需补循环判断形态。你在 MT5 里把 m_signalSeqLen 设成 3,就只复制最近 3 根已完成蜡烛,设成 20 则要看 20 根,样本窗口长短会明显改变信号触发概率。外汇与贵金属杠杆高,这类裸信号未加过滤,实盘前务必用策略测试器跑历史数据验证。
m_signalSeqLen = (class="type">int) ReadLong(p_params); m_periodATR = (class="type">int) ReadLong(p_params); m_stopLevel = ReadDouble(p_params); m_takeLevel = ReadDouble(p_params); m_maxCountOfOrders = (class="type">int) ReadLong(p_params); if(IsValid()) { class=class="str">"cmt">// Request the required number of objects for class="kw">virtual positions CVirtualReceiver::Get(&this, m_orders, m_maxCountOfOrders); class=class="str">"cmt">// Add tracking a new bar on the required timeframe IsNewBar(m_symbol, m_timeframe); class=class="str">"cmt">// Create an information object for the desired symbol m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol]; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Convert an object to a class="type">class="kw">string | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CSimpleCandlesStrategy::class="kw">operator~() { class="kw">return StringFormat("%s(%s)", class="kw">typename(this), m_params); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "Tick" event handler function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSimpleCandlesStrategy::Tick() class="kw">override { class=class="str">"cmt">// If a new bar has arrived for a given symbol and timeframe if(IsNewBar(m_symbol, m_timeframe)) { class=class="str">"cmt">// If the number of open positions is less than the allowed number if(m_ordersTotal < m_maxCountOfOrders) { class=class="str">"cmt">// Get an open signal class="type">int signal = SignalForOpen(); if(signal == class="num">1) { class=class="str">"cmt">// If there is a buy signal, then OpenBuy(); class=class="str">"cmt">// open a BUY position } else if(signal == -class="num">1) { class=class="str">"cmt">// If there is a sell signal, then OpenSell(); class=class="str">"cmt">// open a SELL_STOP position } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Signal for opening pending orders | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSimpleCandlesStrategy::SignalForOpen() { class=class="str">"cmt">// By class="kw">default, there is no signal class="type">int signal = class="num">0; class="type">MqlRates rates[]; class=class="str">"cmt">// Copy the quote values(candles) to the destination array class="type">int res = CopyRates(m_symbol, m_timeframe, class="num">1, m_signalSeqLen, rates); class=class="str">"cmt">// If the required number of candles has been copied if(res == m_signalSeqLen) { signal = class="num">1; class=class="str">"cmt">// buy signal class=class="str">"cmt">// Loop through all the candles