自定义品种(符号):实践基础·进阶篇
(2/3)· 从等量图到Renko,手把手把经纪商真实报价转成可回测的自定义符号
「Renko 砖形在 MT5 里的时空错位陷阱」
用非交易 EA 把真实即时报价转成自定义品种报价,就能在 MT5 里造出 Renko 砖形图。源品种随便挑,EA 挂在哪个图表、用哪个时间帧都行,但生成的伪图表上不能跑指标或脚本——这是自定义品种备案方式决定的硬限制。 所有 Renko 柱线都落在 M1 时间帧内,因为 MT5 最小时间间隔就是 1 分钟,高波动时砖形可能几秒一根,必须让相邻柱线时间尽量贴近。切到别的周期没意义,每根 M1 的开盘时间对应砖形开盘时间,但收盘时间是假的,得看下一根 M1 的开盘。 麻烦出在 1 分钟内要出好几根砖形的时候。MT5 不允许,EA 就把它们排成相邻的 M1 序列,人为加计数。比如砖形大小 100 点,12:00:00 起 10 秒内走 300 点,本来该在 12:00:00、12:00:05、12:00:10 出三根,EA 却在 12:00、12:01、12:02 生成,正式时间比实际提前。 这种提前会在回放历史时撞车:假设 12:02 又走 100 点要生成开盘 12:02 的砖形,那个时间已被占用。SkipOverflows 参数管这事,默认 false,意思是强制把新柱时间再推后 1 分钟避免重叠;设 true 就直接覆盖,砖形会不准。剧烈行情下可能实时喷出多根“未来”柱,上面例子里那三根会在 12:00:10 就全冒出来,分析和下单都得把这事算进去。 EA 还会造虚拟即时报价,时间等于最后一根砖形开盘时间,唯一用途是触发 OnTick。若把源品种报价原样灌进自定义品种,会破坏 Renko 结构——例如真实时间 12:00:00 的报价对应索引 2 的柱而非当前柱,写进去就毁了历史或报错。慢速行情里报价憋在一根砖形内太久,新报价比第 0 根晚超 1 分钟,发过去会在“未来”长出幻像柱。 主要参数里 RenkoBoxSize 默认 100 点,ShowWicks 默认 true 显示烛芯,OutputSymbolName 留空时自动拼成“EURUSD_T_r100”这种格式。初次跑建议把 StartFrom 设近点,先看实时生成速度再放开全历史。Reset 平时关着,EA 能从最后可用砖形续算;只在柱线生成失败时才开。 EURUSD 从 2019 年起、砖形 100 点,在 H1 图表挂 EA 用默认除 StartFrom 外设置即可。重开 EA 时若 Renko 历史已在,时间帧才起作用,重算从最后一根含砖形的原周期柱时间开始。原 EURUSD H1 跑完会出纯砖形图,加两条均线看更清楚。外汇和贵金属波动剧烈,这种时间错位可能导致信号误读,实盘前务必在策略测试器里复现。
class Renko { class="kw">protected: class="type">bool incrementTime(class="kw">const class="type">class="kw">datetime time); class="type">void doWriteStruct(class="kw">const class="type">class="kw">datetime dtTime, class="kw">const class="type">class="kw">double dOpen, class="kw">const class="type">class="kw">double dHigh, class="kw">const class="type">class="kw">double dLow, class="kw">const class="type">class="kw">double dClose, class="kw">const class="type">class="kw">double dVol, class="kw">const class="type">class="kw">double dRealVol, class="kw">const class="type">int spread); class="kw">public: class="type">class="kw">datetime checkEnding(); class="type">void continueFrom(class="kw">const class="type">class="kw">datetime time); class="type">void doReset(); class="type">void onTick(class="kw">const class="type">MqlTick &t); class="type">void updateChartWindow(class="kw">const class="type">class="kw">double bid = class="num">0, class="kw">const class="type">class="kw">double ask = class="num">0); }; class TickProvider { class="kw">public: class="kw">virtual class="type">bool hasNext() = class="num">0; class="kw">virtual class="type">void getTick(class="type">MqlTick &t) = class="num">0; class="type">bool read(Renko &r) { class="kw">while(hasNext() && !IsStopped()) { class="type">MqlTick t; getTick(t); r.onTick(t); } class="kw">return IsStopped(); } };
回放与实时两种 tick 供给的实现差异
把历史 tick 灌进 Renko 合成逻辑,靠的是 HistoryTickProvider 这个类。它从 start 到 stop 按 length 秒一段一段用 CopyTicksRange 拉数据,拉空就顺推时间窗口继续,直到覆盖完整个区间或撞到 stop。 fillArray 里有个防坑判断:若 array[0].time_msc 早于请求的 start*1000,说明接口吐了更老的数据,直接把 start 跳到 stop 并置 size=0 丢弃,避免脏 tick 污染砖形。daysCount 只在真正拿到 size>0 的段时累加,配合 Comment 里的 daysCount*100.0/(numberOfDays+1) 能看出回放进度百分比。 CurrentTickProvider 则是另一路:hasNext 用 ready 布尔翻转来模拟节拍,getTick 每次调 SymbolInfoTick(_Symbol,t) 抓最新盘口。它不缓存、不回溯,适合 StopAt==0 时的在线续砖。 外汇与贵金属 tick 回放受经纪商历史深度限制,实际能拉到的天数可能远小于 (stop-start)/86400,建议先在 MT5 用 CopyTicksRange 打点确认可用区间再跑全套。
class HistoryTickProvider : class="kw">public TickProvider { class="kw">private: class="type">class="kw">datetime start; class="type">class="kw">datetime stop; class="type">class="kw">ulong length; class=class="str">"cmt">// in seconds class="type">MqlTick array[]; class="type">int size; class="type">int cursor; class="type">int numberOfDays; class="type">int daysCount; class="kw">protected: class="type">void fillArray() { cursor = class="num">0; do { size = CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, start * class="num">1000, MathMin(start + length, stop) * class="num">1000); Comment("Processing: ", DoubleToString(daysCount * class="num">100.0 / (numberOfDays + class="num">1), class="num">0), "% ", TTSM(start)); if(size == -class="num">1) { Print("CopyTicksRange failed: ", GetLastError()); } else { if(size > class="num">0 && array[class="num">0].time_msc < start * class="num">1000) class=class="str">"cmt">// prevent older than requested data returned { start = stop; size = class="num">0; } else { start = (class="type">class="kw">datetime)MathMin(start + length, stop); if(size > class="num">0) daysCount++; } } } class="kw">while(size == class="num">0 && start < stop); } class="kw">public: HistoryTickProvider(class="kw">const class="type">class="kw">datetime from, class="kw">const class="type">long secs, class="kw">const class="type">class="kw">datetime to = class="num">0): start(from), stop(to), length(secs), cursor(class="num">0), size(class="num">0) { if(stop == class="num">0) stop = TimeCurrent(); numberOfDays = (class="type">int)((stop - start) / DAY_LONG); daysCount = class="num">0; fillArray(); } class="type">bool hasNext() class="kw">override { class="kw">return cursor < size; } class="type">void getTick(class="type">MqlTick &t) class="kw">override { if(cursor < size) { t = array[cursor++]; if(cursor == size) { fillArray(); } } } }; class CurrentTickProvider : class="kw">public TickProvider { class="kw">private: class="type">bool ready; class="kw">public: class="type">bool hasNext() class="kw">override { ready = !ready; class="kw">return ready; } class="type">void getTick(class="type">MqlTick &t) class="kw">override { SymbolInfoTick(_Symbol, t); } }; class="kw">const class="type">long DAY_LONG = class="num">60 * class="num">60 * class="num">24; class="type">bool _FirstRun = true; Renko renko; CurrentTickProvider online; class="type">void OnTick(class="type">void) { if(_FirstRun) { class=class="str">"cmt">// find existing renko tail to supersede StartFrom class="kw">const class="type">class="kw">datetime trap = renko.checkEnding(); if(trap > TimeCurrent()) { Print("Symbol/Timeframe data not ready..."); class="kw">return; } if((trap == class="num">0) || Reset) renko.doReset(); else renko.continueFrom(trap); HistoryTickProvider htp((trap == class="num">0 || Reset) ? StartFrom : trap, DAY_LONG, StopAt); class="kw">const class="type">bool interrupted = htp.read(renko); _FirstRun = class="kw">false; if(!interrupted) { Comment("RenkoChart(" + (class="type">class="kw">string)RenkoBoxSize + "pt): open ", _SymbolName, " / ", renko.getBoxCount(), " bars"); } else { Print("Interrupted. Custom symbol data is inconsistent - please, reset or class="kw">delete"); } } else if(StopAt == class="num">0) class=class="str">"cmt">// process online if not stopped explicitly { online.read(renko); } }
◍ 双均线交叉EA的信号模块与Renko回测陷阱
两条均线交叉(快线穿慢线做多、穿下做空)属于逆势逻辑,MetaTrader 5 的 MQL 向导能基于标准库直接生成EA骨架,但标准库偏偏没内置双均线交叉信号,得自己写个 Signal2MACross.mqh 挂进向导。 信号类继承 CExpertSignal,内部放两个 CiMA 指标实例,暴露慢周期、快周期、均化方法、价格类型、偏移共5个参数;默认权重100,只认一种形态:快MA上穿慢MA给多、下穿给空。 代码头部那段特殊注释是给 MetaEditor 向导读的,Class 名必须和真实类名对上,否则向导拾取不到。快周期默认7、慢周期11、均化方式 LWMA、价格类型开盘价——这套默认值后面在 renko 上要改。 把文件丢进 MQL5/Include/Expert/Signal/MySignals 后重启 MetaEditor,向导里选「2条均线交叉的信号」、关掉尾随停止、用固定手数,就能吐出 MA2Cross.mq5。 用 EURUSD 生成砖形大小100点的自定义 renko 品种 EURUSD_T_r100,M1周期下跑2019上半年、周期7和11,成交价显示卖出精确贴合砖形、买入因点差偏移——看起来漂亮,但藏着坑。 Renko 第0根柱通常不完整,所以 Expert_EveryTick 设 false、从已收盘第1根取信号;且 Signal_2MACross_MAPrice 必须改 PRICE_CLOSE,别动。 测试器的 renko 回测无论用开盘价、M1 OHLC 还是即时报价模式,都会以等于砖形尺寸的步长「渗透未来」,等于白送圣杯。根因是 renko 柱标记时间和收盘价错位、逐柱测试只给当前柱开盘价当预测因子。 所以单独在 renko 品种上测系统必然失真,订单执行效力必须结合真实品种来验证;Renko 只负责分析「何时入场」的时序,不负责背回测真实的锅。 想让 EA 直接在 renko 图表上联线交易原品种,还得往生成代码里补几行,这也能顺手治一下过度优化的毛病。
class=class="str">"cmt">//--- wizard description start class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Description of the class | class=class="str">"cmt">//| Title=Signals of class="num">2 MAs crosses | class=class="str">"cmt">//| Type=SignalAdvanced | class=class="str">"cmt">//| Name=2MA Cross | class=class="str">"cmt">//| ShortName=2MACross | class=class="str">"cmt">//| Class=Signal2MACross | class=class="str">"cmt">//| Page=signal_2mac | class=class="str">"cmt">//| Parameter=SlowPeriod,class="type">int,class="num">11,Slow MA period | class=class="str">"cmt">//| Parameter=FastPeriod,class="type">int,class="num">7,Fast Ma period | class=class="str">"cmt">//| Parameter=MAMethod,ENUM_MA_METHOD,MODE_LWMA,Method of averaging | class=class="str">"cmt">//| Parameter=MAPrice,ENUM_APPLIED_PRICE,PRICE_OPEN,Price type | class=class="str">"cmt">//| Parameter=Shift,class="type">int,class="num">0,Shift | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- wizard description end class Signal2MACross : class="kw">public CExpertSignal { class="kw">protected: CiMA m_maSlow; class=class="str">"cmt">// object-indicator CiMA m_maFast; class=class="str">"cmt">// object-indicator class=class="str">"cmt">// adjustable parameters class="type">int m_slow; class="type">int m_fast; ENUM_MA_METHOD m_method; ENUM_APPLIED_PRICE m_type; class="type">int m_shift; class=class="str">"cmt">// "weights" of market models(class="num">0-class="num">100) class="type">int m_pattern_0; class=class="str">"cmt">// model class="num">0 "fast MA crosses slow MA" class="kw">public: Signal2MACross(class="type">void); ~Signal2MACross(class="type">void); class=class="str">"cmt">// parameters setters class="type">void SlowPeriod(class="type">int value) { m_slow = value; }
「双均线交叉信号的类封装与EA挂接」
这段实现把双均线交叉逻辑封装成了一个可复用信号类 Signal2MACross。构造函数里默认慢线周期 11、快线周期 7、采用线性加权 LWMA、取开盘价 PRICE_OPEN,pattern_0 权重给到 100,意味着该形态在组合信号里占满额权重。 LongCondition 与 ShortCondition 只做一件事:取 StartIndex 位置,快线高于慢线返回 m_pattern_0(即 100)看多,快线低于慢线返回 100 看空,否则返回 0。也就是说交叉一旦发生,信号强度直接拉满,没有模糊区间。 类里还暴露了 FastPeriod、MAMethod、MAPrice、Shift、Pattern_0 等 setter,方便在 EA 层通过 input 动态改写参数,不用动核心代码。ValidationSettings 和 InitIndicators 是虚函数,留给派生层做配置校验和指标句柄初始化。 挂接部分很直白:EA 主文件 include 了 Signal2MACross.mqh,并配了 TrailingNone(不追踪止损)和 MoneyFixedLot(固定手数)。外部参数里 Signal_2MACross_SlowPeriod=11、FastPeriod=7 与类默认值一致,ThresholdOpen/Close 都设 10,代表信号强度超过 10 才触发开平。 [CODE]…[/CODE] 标记里的代码可直接拷进 MT5 的 MQL5/Include/Expert/Signal/MySignals 目录,配合官方 EA 框架编译。外汇与贵金属杠杆高,均线交叉在震荡市会连续假突破,实盘前先用策略测试器跑至少 3 个月tick数据验证。
class="type">void FastPeriod(class="type">int value) { m_fast = value; } class="type">void MAMethod(ENUM_MA_METHOD value) { m_method = value; } class="type">void MAPrice(ENUM_APPLIED_PRICE value) { m_type = value; } class="type">void Shift(class="type">int value) { m_shift = value; } class=class="str">"cmt">// adjusting "weights" of market models class="type">void Pattern_0(class="type">int value) { m_pattern_0 = value; } class=class="str">"cmt">// verification of settings class="kw">virtual class="type">bool ValidationSettings(class="type">void); class=class="str">"cmt">// creating the indicator and timeseries class="kw">virtual class="type">bool InitIndicators(CIndicators *indicators); class=class="str">"cmt">// checking if the market models are formed class="kw">virtual class="type">int LongCondition(class="type">void); class="kw">virtual class="type">int ShortCondition(class="type">void); class="kw">protected: class=class="str">"cmt">// initialization of the indicators class="type">bool InitMAs(CIndicators *indicators); class=class="str">"cmt">// getting data class="type">class="kw">double FastMA(class="type">int ind) { class="kw">return(m_maFast.Main(ind)); } class="type">class="kw">double SlowMA(class="type">int ind) { class="kw">return(m_maSlow.Main(ind)); } }; Signal2MACross::Signal2MACross(class="type">void) : m_slow(class="num">11), m_fast(class="num">7), m_method(MODE_LWMA), m_type(PRICE_OPEN), m_shift(class="num">0), m_pattern_0(class="num">100) { } class="type">int Signal2MACross::LongCondition(class="type">void) { class="kw">const class="type">int idx = StartIndex(); if(FastMA(idx) > SlowMA(idx)) { class="kw">return m_pattern_0; } class="kw">return class="num">0; } class="type">int Signal2MACross::ShortCondition(class="type">void) { class="kw">const class="type">int idx = StartIndex(); if(FastMA(idx) < SlowMA(idx)) { class="kw">return m_pattern_0; } class="kw">return class="num">0; } class="macro">#include <Expert\Expert.mqh> class="macro">#include <Expert\Signal\MySignals\Signal2MACross.mqh> class="macro">#include <Expert\Trailing\TrailingNone.mqh> class="macro">#include <Expert\Money\MoneyFixedLot.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// inputs for expert class="kw">input class="type">class="kw">string Expert_Title = "MA2Cross"; class=class="str">"cmt">// Document name class="type">class="kw">ulong Expert_MagicNumber = class="num">7623; class="type">bool Expert_EveryTick = class="kw">false; class=class="str">"cmt">// inputs for main signal class="kw">input class="type">int Signal_ThresholdOpen = class="num">10; class=class="str">"cmt">// Signal threshold value to open [class="num">0...class="num">100] class="kw">input class="type">int Signal_ThresholdClose = class="num">10; class=class="str">"cmt">// Signal threshold value to close [class="num">0...class="num">100] class="kw">input class="type">class="kw">double Signal_PriceLevel = class="num">0.0; class=class="str">"cmt">// Price level to execute a deal class="kw">input class="type">class="kw">double Signal_StopLevel = class="num">0.0; class=class="str">"cmt">// Stop Loss level(in points) class="kw">input class="type">class="kw">double Signal_TakeLevel = class="num">0.0; class=class="str">"cmt">// Take Profit level(in points) class="kw">input class="type">int Signal_Expiration = class="num">0; class=class="str">"cmt">// Expiration of pending orders(in bars) class="kw">input class="type">int Signal_2MACross_SlowPeriod = class="num">11; class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Slow MA period class="kw">input class="type">int Signal_2MACross_FastPeriod = class="num">7; class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Fast Ma period class="kw">input ENUM_MA_METHOD Signal_2MACross_MAMethod = MODE_LWMA; class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Method of averaging class="kw">input ENUM_APPLIED_PRICE Signal_2MACross_MAPrice = PRICE_OPEN; class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Price type class="kw">input class="type">int Signal_2MACross_Shift = class="num">0; class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Shift
把双均线过滤挂进专家对象
这段初始化逻辑展示了一个实用做法:在 MT5 的 CExpert 框架里,把 2MA 交叉(11,7,LWMA)当作信号过滤器挂进去,而不是单独跑一条 EA。Signal_2MACross_Weight 设成 1.0 表示这个过滤器的权重拉满,实际组合信号时它话语权最重;若调小到 0.3,系统会更偏向其他过滤器。 外汇与贵金属杠杆高,这类多过滤器 EA 在震荡市可能频繁触发伪信号,回测时建议把 Money_FixLot_Percent 从默认 10% 降到 2%–3% 观察最大回撤。 代码里先 new 一个 CExpertSignal,再 new Signal2MACross 作为 filter0 用 AddFilter 挂上,最后逐个写 SlowPeriod / FastPeriod / MAMethod 等参数。注意若任何一步返回 NULL 或 ValidationSettings 不通过,OnInit 直接 INIT_FAILED,EA 不会加载——开 MT5 编译时若卡在初始化,优先查这几行指针分配。
class="kw">input class="type">class="kw">double Signal_2MACross_Weight = class="num">1.0; class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Weight [class="num">0...class="num">1.0] class=class="str">"cmt">// inputs for money class="kw">input class="type">class="kw">double Money_FixLot_Percent = class="num">10.0; class=class="str">"cmt">// Percent class="kw">input class="type">class="kw">double Money_FixLot_Lots = class="num">0.1; class=class="str">"cmt">// Fixed volume class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global expert object | class=class="str">"cmt">//+------------------------------------------------------------------+ CExpert ExtExpert; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Initialization function of the expert | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// Initializing expert if(!ExtExpert.Init(Symbol(), Period(), Expert_EveryTick, Expert_MagicNumber)) { printf(__FUNCTION__ + ": error initializing expert"); ExtExpert.Deinit(); class="kw">return(INIT_FAILED); } class=class="str">"cmt">// Creating signal CExpertSignal *signal = new CExpertSignal; if(signal == NULL) { printf(__FUNCTION__ + ": error creating signal"); ExtExpert.Deinit(); class="kw">return(INIT_FAILED); } ExtExpert.InitSignal(signal); signal.ThresholdOpen(Signal_ThresholdOpen); signal.ThresholdClose(Signal_ThresholdClose); signal.PriceLevel(Signal_PriceLevel); signal.StopLevel(Signal_StopLevel); signal.TakeLevel(Signal_TakeLevel); signal.Expiration(Signal_Expiration); class=class="str">"cmt">// Creating filter Signal2MACross Signal2MACross *filter0 = new Signal2MACross; if(filter0 == NULL) { printf(__FUNCTION__ + ": error creating filter0"); ExtExpert.Deinit(); class="kw">return(INIT_FAILED); } signal.AddFilter(filter0); class=class="str">"cmt">// Set filter parameters filter0.SlowPeriod(Signal_2MACross_SlowPeriod); filter0.FastPeriod(Signal_2MACross_FastPeriod); filter0.MAMethod(Signal_2MACross_MAMethod); filter0.MAPrice(Signal_2MACross_MAPrice); filter0.Shift(Signal_2MACross_Shift); filter0.Weight(Signal_2MACross_Weight); ... class=class="str">"cmt">// Check all trading objects parameters if(!ExtExpert.ValidationSettings()) { ExtExpert.Deinit(); class="kw">return(INIT_FAILED); } class=class="str">"cmt">// Tuning of all necessary indicators if(!ExtExpert.InitIndicators()) { printf(__FUNCTION__ + ": error initializing indicators"); ExtExpert.Deinit(); class="kw">return(INIT_FAILED); } class="kw">return(INIT_SUCCEEDED); }