开发多币种 EA 交易(第 24 部分):添加新策略(二)·进阶篇
(2/3)·从SimpleVolumes切到SimpleCandles,库与项目解耦下的集成坑比想象深
给策略挂上最大点差闸门
给 EA 加输入参数不是只动 extern 就完事。以「最大点差」为例:想在收到开仓信号时,若实时点差超过阈值就不下单,得同步改三处——输入声明、初始化字符串拼装、策略类内部属性读取。 输入侧用 input int maxSpread_ = 10 表示允许的最大点差(单位:点)。初始化函数里,StringFormat 的格式化串要从原 7 个参数扩成 8 个,尾部追 ,%d 并补 maxSpread_ 实参,否则类构造时读到的字符串会错位。 类内要新增 m_maxSpread 成员,在构造函数里从初始化串解析赋值。之后在开仓信号方法头部加一句判断:若 (int)SymbolInfoInteger(symbol,SYMBOL_SPREAD) > m_maxSpread 则直接 return,不进单。外汇与贵金属点差跳动剧烈,这类闸门能降低滑点吞噬概率,但无法消除高风险。 顺带一提,MQL5 的 input group "===" 仅影响 MT5 参数面板的视觉分组,不改逻辑;删参数时反向操作即可,但初始化串参数顺序必须和类构造签名严格对齐。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="num">4. Strategy inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ sinput class="type">class="kw">string symbol_ = ""; class=class="str">"cmt">// Symbol sinput ENUM_TIMEFRAMES period_ = PERIOD_CURRENT; class=class="str">"cmt">// Timeframe for candles input group "=== Opening signal parameters" input class="type">int signalSeqLen_ = class="num">6; class=class="str">"cmt">// Number of unidirectional candles input class="type">int periodATR_ = class="num">0; class=class="str">"cmt">// ATR period(if class="num">0, then TP/SL in points) input group "=== Pending order parameters" input class="type">class="kw">double stopLevel_ = class="num">25000; class=class="str">"cmt">// Stop Loss(in ATR fraction or points) input class="type">class="kw">double takeLevel_ = class="num">3630; class=class="str">"cmt">// Take Profit(in ATR fraction or points) input group "=== Money management parameters" input class="type">int maxCountOfOrders_ = class="num">9; class=class="str">"cmt">// Max number of simultaneously open orders input class="type">int maxSpread_ = class="num">10; class=class="str">"cmt">// Max acceptable spread(in points) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="num">5. Strategy initialization class="type">class="kw">string generation function | class=class="str">"cmt">//| from the inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string GetStrategyParams() { class="kw">return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d,%d)", (symbol_ == "" ? Symbol() : symbol_), period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_, maxSpread_ ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Trading strategy using unidirectional candlesticks | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSimpleCandlesStrategy : class="kw">public CVirtualStrategy { class="kw">protected: class=class="str">"cmt">// ... class=class="str">"cmt">//--- Money management parameters class="type">int m_maxCountOfOrders; class=class="str">"cmt">// Max number of simultaneously open positions
「用连续阴线触发买信号的坑」
这段策略类把参数从初始化字符串里逐个读出来,其中 m_maxSpread 以 point 为单位记录最大可接受的点差,构造函数里用 ReadLong 强转后赋值。实盘里若点差超过这个值,后续挂单逻辑会被直接拦掉,避免在流动性差的时候吃滑点。 SignalForOpen 先 CopyRates 拉取 m_signalSeqLen+1 根 K 线:索引 0 是当前未平仓蜡烛,1 到 m_signalSeqLen 是已收盘的。默认给 signal=1(买),只要遍历中发现任意一根收盘阳线(open<close)就归零并跳出;若买信号被取消则翻转成 -1(卖),再查是否出现阴线取消。 这里有个反直觉点:连续 m_signalSeqLen 根全为阴线才给买信号,逻辑等价于“跌透反弹”假设,但原文未给任何胜率回测。外汇与贵金属波动受消息驱动,这种纯形态信号在历史数据上可能失效,建议先在 MT5 策略测试器用 2020—2023 年 XAUUSD 的 M15 跑一遍看最大回撤。
class="type">int m_maxSpread; class=class="str">"cmt">// Max acceptable spread(in points) class=class="str">"cmt">// ... }; 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 the 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); 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); m_maxSpread = (class="type">int) ReadLong(p_params); class=class="str">"cmt">// ... } 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=class="str">"cmt">// To check the signal we need m_signalSeqLen of closed candles and the current candle, class=class="str">"cmt">// so in total m_signalSeqLen + class="num">1 class="type">int res = CopyRates(m_symbol, m_timeframe, class="num">0, m_signalSeqLen + class="num">1, rates); class=class="str">"cmt">// If the required number of candles has been copied if(res == m_signalSeqLen + class="num">1) { signal = class="num">1; class=class="str">"cmt">// buy signal class=class="str">"cmt">// Go through all closed candles for(class="type">int i = class="num">1; i <= m_signalSeqLen; i++) { class=class="str">"cmt">// If at least one upward candle occurs, cancel the signal if(rates[i].open < rates[i].close ) { signal = class="num">0; class="kw">break; } } if(signal == class="num">0) { signal = -class="num">1; class=class="str">"cmt">// otherwise, sell signal class=class="str">"cmt">// Go through all closed candles for(class="type">int i = class="num">1; i <= m_signalSeqLen; i++) { class=class="str">"cmt">// If at least one downward candle occurs, cancel the signal if(rates[i].open > rates[i].close ) { signal = class="num">0; class="kw">break; } } } } class=class="str">"cmt">// If there is a signal, then if(signal != class="num">0) { class=class="str">"cmt">// If the current spread is greater than the maximum allowed, then if(rates[class="num">0].spread > m_maxSpread) {
class="type">int m_maxSpread; class=class="str">"cmt">// Max acceptable spread(in points) class=class="str">"cmt">// ... }; 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 the 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); 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); m_maxSpread = (class="type">int) ReadLong(p_params); class=class="str">"cmt">// ... } 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=class="str">"cmt">// To check the signal we need m_signalSeqLen of closed candles and the current candle, class=class="str">"cmt">// so in total m_signalSeqLen + class="num">1 class="type">int res = CopyRates(m_symbol, m_timeframe, class="num">0, m_signalSeqLen + class="num">1, rates); class=class="str">"cmt">// If the required number of candles has been copied if(res == m_signalSeqLen + class="num">1) { signal = class="num">1; class=class="str">"cmt">// buy signal class=class="str">"cmt">// Go through all closed candles for(class="type">int i = class="num">1; i <= m_signalSeqLen; i++) { class=class="str">"cmt">// If at least one upward candle occurs, cancel the signal if(rates[i].open < rates[i].close ) { signal = class="num">0; class="kw">break; } } if(signal == class="num">0) { signal = -class="num">1; class=class="str">"cmt">// otherwise, sell signal class=class="str">"cmt">// Go through all closed candles for(class="type">int i = class="num">1; i <= m_signalSeqLen; i++) { class=class="str">"cmt">// If at least one downward candle occurs, cancel the signal if(rates[i].open > rates[i].close ) { signal = class="num">0; class="kw">break; } } } } class=class="str">"cmt">// If there is a signal, then if(signal != class="num">0) { class=class="str">"cmt">// If the current spread is greater than the maximum allowed, then if(rates[class="num">0].spread > m_maxSpread) {
◍ 用点差阈值卡掉失真信号
在 MT5 的 EA 逻辑里,点差一旦超过预设上限,继续交易信号就不可信。上面这段把 BUY/SELL 信号强制归零,等于把高滑点环境下的假突破过滤掉。 具体看代码:先用 PrintFormat 打出函数名、信号方向、当前点差和上限值,方便在专家日志里复盘哪一笔被丢弃;随后 signal=0 直接取消信号。m_maxSpread 是外部可调参数,做黄金这类平均点差 20~50 点的品种,建议先在策略测试器里跑一周 tick 数据,把阈值定在常态点差的 1.5 倍附近。 外汇与贵金属杠杆高、点差跳变频繁,这种硬过滤只能降低概率性亏损,不保证胜率。开 MT5 把这段接进你现有的信号函数,观察日志里 IGNORE 的频率,再反调 m_maxSpread。
PrintFormat(__FUNCTION__" | IGNORE %s Signal, spread is too big(%d > %d)", (signal > class="num">0 ? "BUY" : "SELL"), rates[class="num">0].spread, m_maxSpread); signal = class="num">0; class=class="str">"cmt">// Cancel the signal } } class="kw">return signal; }
把建项目 EA 拆成类才扛得住新策略
看 CreateProject.mq5 的 OnInit,旧写法把连库、建项目、建阶段、建作业、排队、关库全堆在一个函数里。CreateJobs() 尤其臃肿:它既要预处理输入、拼参数模板、写数据库,又顺手建了优化任务。这种「一个函数包打天下」的结构,换策略就得动深层代码,没法直接挪进库区复用。 原来的全局变量 paramsTemplate1 写死了第一阶段参数:signalPeriod_ 从 12 扫到 240、步长 40,stopLevel_ 从 200.0 到 20000.0,共 8 组参数带 Y/N 开关。旧策略所有第一阶段作业都用同一模板,但新策略把交易品种和周期也塞进参数,模板就出了可变部分——你改一次得钻进任务创建函数内部硬改,库区隔离直接破功。 另外,当前 EA 写死三阶段流水线。开发时试过加第 18、19 部分的额外阶段,实测对最终结果没显著改善,才固定成三阶段。可别家策略未必合适,一旦把代码搬进库还锁死阶段数,以后想加就难了。 与其以后返工,不如现在重构:把逻辑拆成类移进库,项目侧只管拼阶段和内容。初步目标先抽出 COptimizationProject 类,下面这段新 OnInit 就是方向——用对象方法链式搭项目,阶段和参数都能灵活插。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// Connect to the database DB::Connect(fileName_); class=class="str">"cmt">// Create a project CreateProject(projectName_, projectVersion_, StringFormat("%s - %s", TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE) ) ); class=class="str">"cmt">// Create project stages CreateStages(); class=class="str">"cmt">// Creating jobs and tasks CreateJobs(); class=class="str">"cmt">// Queueing the project for execution QueueProject(); class=class="str">"cmt">// Close the database DB::Close(); class=class="str">"cmt">// Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">// Template of optimization parameters at the first stage class="type">class="kw">string paramsTemplate1 = "; === Open signal parameters\n" "signalPeriod_=class="num">212||class="num">12||class="num">40||class="num">240||Y\n" "signalDeviation_=class="num">0.1||class="num">0.1||class="num">0.1||class="num">2.0||Y\n" "signaAddlDeviation_=class="num">0.8||class="num">0.1||class="num">0.1||class="num">2.0||Y\n" "; === Pending order parameters\n" "openDistance_=class="num">10||class="num">0||class="num">10||class="num">250||Y\n" "stopLevel_=class="num">16000||class="num">200.0||class="num">200.0||class="num">20000.0||Y\n" "takeLevel_=class="num">240||class="num">100||class="num">10||class="num">2000.0||Y\n" "ordersExpiration_=class="num">22000||class="num">1000||class="num">1000||class="num">60000||Y\n" "; === Capital management parameters\n" "maxCountOfOrders_=class="num">3||class="num">3||class="num">1||class="num">30||N\n"; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// Create an optimization project object for the given database COptimizationProject p; class=class="str">"cmt">// Create a new project in the database p.Create(...); class=class="str">"cmt">// Add the first stage p.AddStage(...); class=class="str">"cmt">// Adding the first stage jobs p.AddJobs(...); class=class="str">"cmt">// Add tasks for the first stage jobs p.AddTasks(...); class=class="str">"cmt">// Add the second stage p.AddStage(...); class=class="str">"cmt">// Add the second stage jobs p.AddJobs(...); class=class="str">"cmt">// Add tasks for the second stage jobs p.AddTasks(...); class=class="str">"cmt">// Add the third stage p.AddStage(...);
「把多阶段任务塞进队列并自毁 EA」
在 MQL5 的 CExpert 或自定义项目对象 p 上,三轮任务不是一次跑完,而是分阶段压进同一个执行管道。先 AddJobs 补上第三阶段的作业,再用 AddTasks 把该阶段的具体任务挂进去,此时前两层若已就绪,调度器会在后台按依赖顺序吞吐。 p.Queue() 这一步才是真正把整个项目推入终端的执行队列,漏掉它,前面所有的 Add 都只停留在内存对象里,MT5 不会主动跑。 任务递交后直接调 ExpertRemove() 让 EA 自我注销,常见于一次性批处理脚本——初始化函数返回 INIT_SUCCEEDED 仅表示挂载成功,不代表策略会继续驻留图表。开 MT5 把这段接在你自己的 p 配置后,能验证 EA 跑完即消失的现象。
class=class="str">"cmt">// Add the third stage job p.AddJobs(...); class=class="str">"cmt">// Add a task for the third stage job p.AddTasks(...); class=class="str">"cmt">// Put the project in the execution queue p.Queue(); class=class="str">"cmt">// Delete the EA ExpertRemove(); class=class="str">"cmt">// Successful initialization class="kw">return(INIT_SUCCEEDED); }
◍ 把优化工程封装成可持久化的项目类
做批量参数优化时,最忌讳把品种、周期、标准全塞进一个扁平结构。这里把优化数据库里的实体拆成三层:项目(COptimizationProject)、阶段作业(COptimizationJob)、具体任务(COptimizationTask),每一层对应库里一张表,类字段直接映射表字段,再补一点执行所需的方法。 目前图省事,这些类的属性方法全 public,每个类自带往库里插新记录的函数;改记录和读记录留到后面再做,因为建项目用不到。原先用的测试器参数模板,被换成按项目指针返回填充参数的独立函数,模板逻辑收进函数内部,项目对象能直接取数替换。 COptimizationProject 构造时立刻连库并开事务,析构时根据 CDatabase::s_res 决定提交还是回滚——有错就取消,没错才确认,同时释放动态对象。添加工作有两种入口:一种收逗号分隔的字符串(品种/周期/标准),内部转数组后调第二种数组版。 第三个参数是指向阶段EA优化参数生成函数的指针,类型 TJobsTemplateFunc 已在库里声明。下面这段就是项目类的骨架,开 MT5 建个空库对照 fields 就能验证表结构是否对得上。
class=class="str">"cmt">// Create a new type - a pointer to a class="type">class="kw">string generation function class=class="str">"cmt">// for optimization job parameters(job) accepting the pointer class=class="str">"cmt">// to the optimization project object as an argument typedef class="type">class="kw">string (*TJobsTemplateFunc)(COptimizationProject*); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Optimization project class | class=class="str">"cmt">//+------------------------------------------------------------------+ class COptimizationProject { class="kw">public: class="type">class="kw">string m_fileName; class=class="str">"cmt">// Database name class=class="str">"cmt">// Properties stored directly in the database class="type">ulong id_project; class=class="str">"cmt">// Project ID class="type">class="kw">string name; class=class="str">"cmt">// Name class="type">class="kw">string version; class=class="str">"cmt">// Version class="type">class="kw">string description; class=class="str">"cmt">// Description class="type">class="kw">string status; class=class="str">"cmt">// Status class=class="str">"cmt">// Arrays of all stages, jobs and tasks COptimizationStage* m_stages[]; class=class="str">"cmt">// Project stages COptimizationJob* m_jobs[]; class=class="str">"cmt">// Jobs of all project stages COptimizationTask* m_tasks[]; class=class="str">"cmt">// Tasks of all jobs of project stages class=class="str">"cmt">// Properties for the current state of the project creation class="type">class="kw">string m_symbol; class=class="str">"cmt">// Current symbol class="type">class="kw">string m_timeframe; class=class="str">"cmt">// Current timeframe COptimizationStage* m_stage; class=class="str">"cmt">// Last created stage(current stage) COptimizationJob* m_job; class=class="str">"cmt">// Last created job(current job) COptimizationTask* m_task; class=class="str">"cmt">// Last created task(current task) class=class="str">"cmt">// Methods COptimizationProject(class="type">class="kw">string p_fileName); class=class="str">"cmt">// Constructor ~COptimizationProject(); class=class="str">"cmt">// Destructor class=class="str">"cmt">// Create a new project in the database COptimizationProject* COptimizationProject::Create(class="type">class="kw">string p_name, class="type">class="kw">string p_version = "", class="type">class="kw">string p_description = "", class="type">class="kw">string p_status = "Done"); class="type">void Insert(); class=class="str">"cmt">// Insert an entry into the database