利用 MQL5 实现 Janus 因子·进阶篇
(2/3)· 多数人只看价格方向,却忽略驱动行情的反馈机制,本文用 MQL5 把理论落成可跑的指标
◍ 跨品种历史数据同步的坑
在多品种统计套利或相关性扫描里,最常崩的不是逻辑,而是某个符号的历史 K 线根本没加载完。MT5 默认只在图表打开时惰性同步,EA 启动时直接读 Bars() 可能拿到个位数。 下面这段代码用 iBarShift 反推已同步的 bar 数:以 m_synced_first 这个已知时间为锚,算偏移量再加 1。若结果大于等于 m_calculation_length(你设定的最小计算长度),就认为够用;不够就扔给 SyncSymbol 去补。 [CODE] bool CheckSymbolBars(const string __sym) { int bars=-1; bars=iBarShift(__sym,m_collection_timeframe,m_synced_first)+1;//SeriesInfoInteger(__sym,PERIOD_CURRENT,SERIES_BARS_COUNT); #ifdef DEBUG Print("Bars found in history for ",__sym," ",bars); #endif if(bars>=m_calculation_length) return(true); //--- return(SyncSymbol(__sym)); } [/CODE] SyncSymbol 的核心是一个带终止条件的 while:只要未停止、未下载完、且终端连着,就先 Sleep(5) 等 SERIES_SYNCHRONIZED 置位,再查 Bars。若仍不足,用 CopyTime 去拽下一截——CopyTime 返回 1 即认为触发了底层加载。 [CODE] bool SyncSymbol(const string __sym) { //--- load data step by step bool downloaded=false; datetime times[1]; int bars=-1; #ifdef DEBUG Print(" downloading ",__sym," history"); #endif while(!IsStopped() && !downloaded && TerminalInfoInteger(TERMINAL_CONNECTED)) { //--- while(!SeriesInfoInteger(__sym,m_collection_timeframe,SERIES_SYNCHRONIZED) && !IsStopped()) Sleep(5); //--- bars=Bars(__sym,PERIOD_CURRENT); if(bars>=m_calculation_length) { downloaded=true; break; } //--- copying of next part forces data loading if(CopyTime(__sym,m_collection_timeframe,m_calculation_length-1,1,times)==1) { downloaded=true; [/CODE] 开 MT5 把 m_calculation_length 设成 500 跑一遍 EURUSD 和 XAUUSD 的冷启动,能在日志里看到 CopyTime 被调用几次才补齐。外汇和贵金属点差跳变大,历史同步失败会让信号滞后,属高风险环节,参数别拍脑袋定。
class="type">bool CheckSymbolBars(class="kw">const class="type">class="kw">string __sym) { class="type">int bars=-class="num">1; bars=iBarShift(__sym,m_collection_timeframe,m_synced_first)+class="num">1;class=class="str">"cmt">//SeriesInfoInteger(__sym,PERIOD_CURRENT,SERIES_BARS_COUNT); class="macro">#ifdef DEBUG Print("Bars found in history for ",__sym," ",bars); class="macro">#endif if(bars>=m_calculation_length) class="kw">return(true); class=class="str">"cmt">//--- class="kw">return(SyncSymbol(__sym)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool SyncSymbol(class="kw">const class="type">class="kw">string __sym) { class=class="str">"cmt">//--- load data step by step class="type">bool downloaded=class="kw">false; class="type">class="kw">datetime times[class="num">1]; class="type">int bars=-class="num">1; class="macro">#ifdef DEBUG Print(" downloading ",__sym," history"); class="macro">#endif class="kw">while(!IsStopped() && !downloaded && TerminalInfoInteger(TERMINAL_CONNECTED)) { class=class="str">"cmt">//--- class="kw">while(!SeriesInfoInteger(__sym,m_collection_timeframe,SERIES_SYNCHRONIZED) && !IsStopped()) Sleep(class="num">5); class=class="str">"cmt">//--- bars=Bars(__sym,PERIOD_CURRENT); if(bars>=m_calculation_length) { downloaded=true; class="kw">break; } class=class="str">"cmt">//--- copying of next part forces data loading if(CopyTime(__sym,m_collection_timeframe,m_calculation_length-class="num">1,class="num">1,times)==class="num">1) { downloaded=true;
「历史同步与内存回收的实现细节」
这段类实现负责把多品种集合的历史数据对齐,并在对象销毁时干净地释放指针。构造函数 CSymbolCollection 把集合长度初始化为 0、计算长度设为 -1、同步标志置 false,同时记录传入的时间框架,默认是 PERIOD_CURRENT,也就是调用时图表所在的周期。 析构函数遍历 m_collection 数组,用 CheckPointer 判断是否为 POINTER_DYNAMIC 动态对象,是才执行 delete。漏掉这步在 EA 反复新建删除集合时会造成内存泄漏,MT5 策略测试器里跑几十万根 bar 可能直接爆内存。 CheckHistory 是真正做跨品种对齐的入口:先用 iBarShift 算从 m_synced_first 这个时间点到现在的可用 bar 数并加 1,若小于请求的 size 就以可用量为准,否则用 size。DEBUG 宏下会打印同步起始时间和最终采用的历史长度,方便你开 MT5 终端看日志核对。 循环里对每个品种调 CheckSymbolBars 做实际下载或本地校验,任意一次失败就中断同步。外汇与贵金属品种在跨服务器拉历史时延迟波动大,这类同步失败概率不低,实盘前务必在模拟环境跑通。
class="kw">break; } class=class="str">"cmt">//--- Sleep(class="num">5); } class="macro">#ifdef DEBUG if(downloaded) Print(bars," ",__sym," bars downloaded "); else Print("Downloading ",__sym," bars failed"); class="macro">#endif class="kw">return(downloaded); } class="kw">public: CSymbolCollection(class="kw">const ENUM_TIMEFRAMES tf=PERIOD_CURRENT) { m_raw_symbols=""; m_collection_length = class="num">0; m_calculation_length = -class="num">1; m_synced_first=class="num">0; m_synced=class="kw">false; m_collection_timeframe=tf; } ~CSymbolCollection(class="type">void) { for(class="type">int i=class="num">0; i<m_collection_length; i++) { if(CheckPointer(m_collection[i])==POINTER_DYNAMIC) class="kw">delete m_collection[i]; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|class="kw">return the set timeframe for bars stored in the collection | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_TIMEFRAMES GetTimeFrame(class="type">void) { class="kw">return(m_collection_timeframe); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Checks the history available and syncs it across all symbols | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CheckHistory(class="kw">const class="type">int size) { if(size<=class="num">0) class="kw">return(class="kw">false); class="type">int available=iBarShift(NULL,m_collection_timeframe,m_synced_first)+class="num">1; if(available<size) m_calculation_length=available; else m_calculation_length=size; class="macro">#ifdef DEBUG Print("synced first date is ", m_synced_first); Print("Proposed size of history ",m_calculation_length); class="macro">#endif if(m_calculation_length<=class="num">0) class="kw">return(class="kw">false); ResetLastError(); for(class="type">int i=class="num">0; i<m_collection_length; i++) { m_synced=CheckSymbolBars(m_collection[i].GetName()); if(!m_synced) {
多品种容器的防重与首根对齐
往自定义品种集合里塞新标的时,先拿 GetFirstDate() 取该品种在指定周期上的最早 K 线时间,再用 GetIndex(name) 扫一遍已有数组。若返回时间等于 WRONG_VALUE 或索引大于 -1,说明要么拿不到历史、要么已经收过,直接 delete 掉临时对象并退出,避免重复占用内存。 ArrayResize 这里第二个参数填 m_collection_length + 1、第三参数填 1,代表每次只扩一个槽位且预留 1 个缓冲,属于典型的按需增长写法。新对象挂到 m_collection[m_collection_length] 后,若它的首根时间 f 比当前 m_synced_first 更晚,就把同步起点推到更晚的位置——这保证了后续计算只从所有品种都共有的时间段开始。 GetIndex 用线性遍历做名称匹配,品种数在两位数内开销可忽略;真要上几十个交叉盘,可能得换哈希表。外汇与贵金属杠杆高、点差跳变频繁,这类多品种回测前务必确认各自历史深度,否则 m_synced_first 被某脏数据拖后,样本量会悄悄缩水。
class="type">int Add(class="type">class="kw">string name) { CSymbolData *ref = new CSymbolData(name,m_collection_timeframe); class="type">class="kw">datetime f=ref.GetFirstDate(); class="type">int found=GetIndex(name); if(f==WRONG_VALUE || found>-class="num">1) { class="macro">#ifdef DEBUG if(f==WRONG_VALUE) Print("Failed to retrieve information for symbol ",name,". Symbol removed from collection"); if(found>-class="num">1) Print("Symbol ",name,"already part of collection"); class="macro">#endif class="kw">delete ref; class="kw">return(m_collection_length); } ArrayResize(m_collection, m_collection_length + class="num">1,class="num">1); m_collection[m_collection_length] = ref; if(f>m_synced_first) m_synced_first=f; class="kw">return(++m_collection_length); } class="type">class="kw">string GetSymbolNameAtPos(class="type">int pos) { class="kw">return m_collection[pos].GetName(); } class="type">int GetIndex(class="kw">const class="type">class="kw">string symbol_name) { for(class="type">int i=class="num">0; i<m_collection_length; i++) { if(symbol_name==m_collection[i].GetName()) class="kw">return(i); } class="kw">return(-class="num">1); }
◍ 多币种容器的取数与刷新接口
这段代码是某个多货币对管理类的尾部方法集,负责把内部集合的长度、历史计算窗口以及逐根 K 线的开收盘价暴露给外部调用者。 GetCollectionLength 直接返回成员变量 m_collection_length,也就是当前被纳入监控的货币对数量;GetHistoryBarsLength 则返回 m_calculation_length,对应回测或实时计算所用的历史 Bar 数量,这两个值决定了后面循环和矩阵维度的上限。 Update 方法是实盘里最该留意的:它对 0 到 m_collection_length-1 的每个币种调用各自的 Update(),只要有一个返回 false,就立刻 Print 出「missing data on 币种名」并整体返回 false。这意味着任一符号拉不到行情,整个多币种扫描就会中断,外汇与贵金属市场休市或报价源抖动时极易触发。 GetRateAtPos、GetOpenAtPos、GetCloseAtPos 三个接口按 pos(币种序号)和 i(K线序号)定位,后两者直接从 MqlRates 结构里取 open / close 字段。写 EA 时若想算跨品种相关性,直接循环调 GetCloseAtPos 就能拿到对齐的收盘价序列,不必再自己维护缓冲。
class="type">int GetCollectionLength(class="type">void) { class="kw">return m_collection_length; } class="type">bool Update(class="type">void) { class="type">int i; for(i = class="num">0; i < m_collection_length; i++) { class="type">bool res = m_collection[i].Update(); if(res==class="kw">false) { Print("missing data on " + m_collection[i].GetName()); class="kw">return class="kw">false; } } class="kw">return true; } class="type">int GetHistoryBarsLength(class="type">void) { class="kw">return m_calculation_length; } class="type">MqlRates GetRateAtPos(class="type">int pos, class="type">int i) { class="kw">return m_collection[pos].GetRateAtPos(i); } class="type">class="kw">double GetOpenAtPos(class="type">int pos, class="type">int i) { class="kw">return m_collection[pos].GetRateAtPos(i).open; } class="type">class="kw">double GetCloseAtPos(class="type">int pos, class="type">int i) { class="kw">return m_collection[pos].GetRateAtPos(i).close; }
「多品种价格字段的取数封装」
做跨品种统计时,如果每个品种都单独调 CopyClose / CopyHigh 会很乱。下面这组方法把持仓集合 m_collection 按 pos 定位品种、按 i 定位 K 线序号,直接吐出 close / high / low 等基础字段。 中位价、典型价、加权价都是在高低收基础上二次计算:中位价 = (高+低)/2,典型价 = (高+低+收)/3,加权价 = (高+低+收×2)/4。外汇与贵金属波动剧烈,这些派生价只用于描述分布形态,不代表方向判断。 代码里 pos 是品种在集合中的下标,i 是该品种历史柱的下标,从 0 开始倒数。开 MT5 把这段塞进你的 CSymbolsCollection 类里,就能用 GetTypicalAtPos(pos,i) 一行拿到典型价,省掉重复取值。
class="type">class="kw">double GetCloseAtPos(class="type">int pos, class="type">int i) { class="kw">return m_collection[pos].GetRateAtPos(i).close; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return High price of currency at position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetHighAtPos(class="type">int pos, class="type">int i) { class="kw">return m_collection[pos].GetRateAtPos(i).high; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return Low price of currency at position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetLowAtPos(class="type">int pos, class="type">int i) { class="kw">return m_collection[pos].GetRateAtPos(i).low; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return Median price of currency at position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetMedianAtPos(class="type">int pos, class="type">int i) { class="kw">return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i))/class="num">2; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return Typical price of currency at position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetTypicalAtPos(class="type">int pos, class="type">int i) { class="kw">return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i) + GetCloseAtPos(pos,i))/class="num">3; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return Weighted price of currency at position | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetWeightedAtPos(class="type">int pos, class="type">int i) { class="kw">return (GetHighAtPos(pos,i) + GetLowAtPos(pos, i) + GetCloseAtPos(pos,i) * class="num">2)/class="num">4; } };
指标类的枚举与私有方法定义
Janus 指标族的核心计算被封装在 CJanus 类里,先看清它支持的两种价格与差分枚举,才能明白后面收益率怎么算。 ENUM_PRICE 给了 4 种取值:CLOSE=0 收市价、MEDIAN 中间价、TYPICAL 典型价、WEIGHTED 加权价;ENUM_DIFF_TYPE 则区分 DIFF_PERCENT=0 百分比差分与 DIFF_LOG 对数差分,这两种处理在贵金属跨品种波动率上结果可能差出 15%~20%。 类私有段暴露了几个关键函数:market_return 按柱偏移和品种偏移取市场收益,rs 带 lag 参数算相对强度,market_offense_defense 同时吐出进攻与防御值。这些方法是多品种同步历史计算的基础,m_hist_size 控制集合内所有品种对齐的 K 线根数。 外汇与贵金属杠杆高、跳空频繁,直接把这类类丢进实盘前,建议先在 MT5 用 EURUSD+XAUUSD 小周期跑通 rs 的 lag=0 分支。
enum ENUM_PRICE { CLOSE=class="num">0,class=class="str">"cmt">//close price MEDIAN,class=class="str">"cmt">//median price TYPICAL,class=class="str">"cmt">//typical price WEIGHTEDclass=class="str">"cmt">//weighted price }; enum ENUM_DIFF_TYPE { DIFF_PERCENT=class="num">0,class=class="str">"cmt">//percent difference DIFF_LOGclass=class="str">"cmt">//log difference }; class CJanus { class="kw">private: CSymbolCollection* m_symbol_list; class=class="str">"cmt">//object container of symbols ENUM_PRICE m_price_type; class=class="str">"cmt">//applied price for calculations ENUM_DIFF_TYPE m_diff_type; class=class="str">"cmt">//method of differencing applied ENUM_INDEX_TYPE m_index_type; class=class="str">"cmt">//type of index class="type">int m_hist_size; class=class="str">"cmt">// synchronized size of history across all selected symbols in collection ENUM_TIMEFRAMES m_list_timeframe; class=class="str">"cmt">//timeframe for bars to be used in calculations class=class="str">"cmt">//---class="kw">private methods class="type">class="kw">double market_return(class="kw">const class="type">uint barshift, class="kw">const class="type">uint symbolshift); class="type">void market_offense_defense(class="kw">const class="type">uint barshift,class="kw">const class="type">uint symbolshift,class="kw">const class="type">uint rs_period,class="type">class="kw">double& out_offense,class="type">class="kw">double& out_defense); class="type">class="kw">double rs(class="kw">const class="type">uint barshift,class="kw">const class="type">uint symbolshift,class="kw">const class="type">uint rs_period,class="type">uint lag=class="num">0); class="type">class="kw">double rs_off_def(class="kw">const class="type">uint barshift, class="kw">const class="type">uint symbolshift,class="kw">const class="type">uint lag,class="kw">const class="type">class="kw">double median,class="kw">const class="type">class="kw">double index_offense, class="kw">const class="type">class="kw">double index_defense, class="type">class="kw">double &array[]); class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//constructor CJanus(class="type">void):m_symbol_list(NULL), m_price_type(WRONG_VALUE), m_diff_type(WRONG_VALUE),