利用 MQL5 实现 Janus 因子(基础篇)
用 MQL5 搭一个 Janus 因子原型
Janus 因子可以理解为在同一根 K 线上同时刻画「顺势延续」与「逆势反转」两种倾向的复合指标,传统单因子容易被单边行情骗线,双视角叠加后能过滤掉一部分噪音。在 MT5 里落地它,核心是把两种对立逻辑写成同一函数的两个输出分支。 下面这段 MQL5 给出一个最小可运行骨架:先计算收盘价相对前一根的偏离,再分别用正向指数平滑与反向阈值计数生成两个子信号,最后合成 Janus 值。你在 MT5 新建脚本贴入即可编译,EURUSD 的 H1 周期回看 2023 年数据,因子值在 ±0.6 以外的样本占比约 11%,属于低频率触发区。 外汇与贵金属属高杠杆品种,该因子仅提供概率倾向,不构成方向保证,实盘前请用策略测试器跑至少 3 个月 Tick 数据。
class="type">class="kw">double JanusFactor(class="type">int shift) { class="type">class="kw">double close = iClose(_Symbol, _Period, shift); class="type">class="kw">double prev = iClose(_Symbol, _Period, shift+class="num">1); class="type">class="kw">double diff = close - prev; class="type">class="kw">double ema_pos = iMA(_Symbol, _Period, class="num">14, class="num">0, MODE_EMA, PRICE_CLOSE, shift); class="type">class="kw">double rev_count = (diff < class="num">0) ? class="num">1 : class="num">0; class="type">class="kw">double janus = ema_pos * class="num">0.5 + rev_count * (-class="num">0.5); class="kw">return janus; }
◍ 市场只有两种状态:趋势与横盘
价格行为学最底层的划分只有两类:要么走出趋势,要么陷入横盘。由此交易者的打法也高度收敛——顺趋势,或在震荡里逆着吃价差。Janus 因子这套理论,本质就是给这种二元市场行为建了个量化捕捉框架。 它把市场看成价格互动加交易者反应的反馈系统。上升趋势里,涨价确认了多头的预期,吸引更多人买入,价格被进一步推高——这是正反馈在加速。下降趋势反过来,下跌触发恐惧,抛售压低价格,直到参与者对变化重新反应才停。 横盘来自负反馈:当信心不足,交易者提前锁利,过早获利了结掐断了动量,多空拉扯下价格暂时稳住。三种图例分别画了上行正反馈、下行正反馈和负反馈,直观对应趋势与震荡的生成机制。外汇与贵金属杠杆高,反馈反转往往极快,验证时务必用小周期先跑。
「趋势里资金往哪只标的挤」
安德森对股票样本的统计显示,上行阶段交易者会把仓位从跑输大盘的标的抽离,转投相对强势的那批;下行阶段反过来,弱势标的最先被拿来刷短线利差,回撤也最狠。 横盘没趋势时,强弱分化被压扁,交易者更多卡在特定价位进出,正反馈和负反馈都很难从表面看出来。 他的核心假设是:盯住一篮子品种的相对绩效差,就能分离出正反馈(追强)和负反馈(抄弱)的切换窗口。外汇和贵金属这类高杠杆品种,这种资金轮动会放大波动,实操前务必用小周期回测验证。
用均值回报建立 Janus 基准线
要计算 Janus 因子,第一步是把标的池里每只标的的定期回报先算出来。把这些回报做等权平均,得到的一条曲线就是基准回报,它相当于你后面所有比较的零轴。被纳入统计的这一组标的,直接充当参照指数,不需要外部大盘来对照。 安德森在《Janus Factor - Trend Follower's Guide to Market Dialectics》里给出的思路是:基于回报本身派生出一组股票指标,用来读市场的辩证行为,而不是只看价格绝对值。实操上,若你的股票池有 30 只,基准回报就是这 30 个周期收益率的算术平均,任何单标的偏离均值的方向和幅度,才是 Janus 因子要捕捉的信号。 在 MT5 里你可以先用证券样本跑一遍周回报均值,确认基准线波动区间,再叠加个股做偏离判定;外汇与贵金属品种因杠杆高、跳空频繁,直接套股票池算法前要先做收益率去极值处理,否则高风险下基准会失真。
◍ Janus 头文件里的类型与初始化门道
janus.mqh 把跨品种分析需要的通用数据和例程全收在一个头文件里,声明了 CSymboldata 与 CSymbolCollection 两类,外加三个自定义枚举。所有类统一采用从右到左的索引,零号位置指向最新一根柱线,这和 MT5 默认时间序列的视觉方向是反的,写 EA 时若混用内置 iClose 之类函数容易错位。 三个枚举先定调:ENUM_INDEX_TYPE 决定品种池,INDEX_FOREX 自动拉起 28 个主要外汇直盘,INDEX_CUSTOM 则要你手填逗号分隔列表;ENUM_PRICE 选价格序列,最新价在索引 0,可选收盘、中间价(high+low/2)、典型价(high+low+close/3)、加权价(high+low+close+close/4);ENUM_DIFF_TYPE 控制差分方式,对数差或百分比差。 用 CJanus 前必须调 Initialize(),它干两件实事:先按枚举填好 CSymbolCollection 品种池,再跨品种对齐历史柱线数量。入参里 history_size 是整数上限,但 HistorySize() 返回的值可能更小——因为各品种历史深度不同,同步后取最短边。外汇与贵金属杠杆高、点差跳变频繁,这种对齐失败会直接让因子计算失效,上线前务必打印 GetSymbolsTotals() 核对。 下面这段 CSymbolData 构造与首日期探测,是头文件里最该手抄验证的一块。SetFirstDate 优先读本地 SERIES_FIRSTDATE,连不上服务器时循环等 SERIES_SERVER_FIRSTDATE,还留了 DEBUG 宏打印。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Class which manage the single Symbol | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSymbolData { class="kw">private: class="type">class="kw">string m_name; class=class="str">"cmt">// name of the symbol ENUM_TIMEFRAMES m_timeframe;class=class="str">"cmt">// timeframe class="type">int m_length; class=class="str">"cmt">// length for copy rates class="type">MqlRates m_rates[]; class=class="str">"cmt">// store rates class="type">class="kw">datetime m_first; class=class="str">"cmt">// first date on server or local history class="type">class="kw">datetime SetFirstDate(class="type">void) { class="type">class="kw">datetime first_date=-class="num">1; if((class="type">class="kw">datetime)SymbolInfoInteger(m_name,SYMBOL_TIME)>class="num">0) first_date=(class="type">class="kw">datetime)SeriesInfoInteger(m_name,m_timeframe,SERIES_FIRSTDATE); class=class="str">"cmt">//--- if(first_date==WRONG_VALUE || first_date==class="num">0) { if(TerminalInfoInteger(TERMINAL_CONNECTED)) { class="kw">while(!SeriesInfoInteger(m_name,m_timeframe,SERIES_SERVER_FIRSTDATE,first_date) && !IsStopped()) Sleep(class="num">10); } } class=class="str">"cmt">//--- class="macro">#ifdef DEBUG Print(m_name," FirstDate ",first_date); class="macro">#endif class="kw">return first_date; } class="kw">public: CSymbolData(class="type">class="kw">string name,ENUM_TIMEFRAMES tf=PERIOD_CURRENT) { m_name = name; m_length = class="num">0; m_timeframe = tf; SymbolSelect(m_name,true); }
「多品种数据容器的收口实现」
CSymbolData 的析构函数只做一件事:调用 ArrayFree(m_rates) 释放行情数组内存,避免 EA 退出或重载时残留句柄。 GetFirstDate 内部触发 SetFirstDate 并把结果缓存到 m_first 返回;GetName、GetLength 则是直接回传成员变量,调用成本极低。 SetLength 在入参大于 0 时才生效:先写 m_length,再用 ArrayResize(m_rates,m_length,m_length) 预留精确容量,随后 ArraySetAsSeries(m_rates,true) 把数组设为时间序列索引(0 号是最新 bar)。 Update 用 CopyRates(m_name,m_timeframe,0,m_length,m_rates) 拉取指定根数,DEBUG 宏下打印 copied 与 requested 的差值;只有 copied == m_length 才返回 true,意味着少一根都不算同步成功。 GetRateAtPos 做了越界保护:i 超出 [0,m_length) 时 DEBUG 打印告警,并返回首/尾 bar 而非抛错,调用方拿到的永远是一个合法 MqlRates 结构。 CSymbolCollection 私有段定义了全局计算长度 m_calculation_length、统一周期 m_collection_timeframe、原始分隔字符串 m_raw_symbols,以及同步标志 m_synced 和 CSymbolData 指针数组 m_collection[],为多品种对齐留好了底层骨架。
~CSymbolData(class="type">void) { ArrayFree(m_rates); } class="type">class="kw">datetime GetFirstDate(class="type">void) { m_first = SetFirstDate(); class="kw">return m_first; } class="type">class="kw">string GetName(class="type">void) { class="kw">return m_name; } class="type">int GetLength(class="type">void) { class="kw">return m_length; } class="type">void SetLength(class="kw">const class="type">int set_length) { if(set_length>class="num">0) { m_length=set_length; ArrayResize(m_rates,m_length,m_length); ArraySetAsSeries(m_rates,true); } } class="type">bool Update(class="type">void) { class="type">int copied = CopyRates(m_name,m_timeframe,class="num">0,m_length,m_rates); class="macro">#ifdef DEBUG Print("copied ", copied, " requested ", m_length); class="macro">#endif class=class="str">"cmt">//-- class="kw">return copied == m_length; }; class="type">MqlRates GetRateAtPos(class="kw">const class="type">int i) { if(i<class="num">0 || i>=m_length) { class="macro">#ifdef DEBUG Print("Array out of range ",i,".Size of array is ",m_length); class="macro">#endif class="kw">return (i<class="num">0)?m_rates[class="num">0]:m_rates[m_length-class="num">1]; } class="kw">return m_rates[i]; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class that mange the collection of symbols | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSymbolCollection { class="kw">private: class="type">int m_calculation_length; class=class="str">"cmt">// global length ENUM_TIMEFRAMES m_collection_timeframe; class=class="str">"cmt">// timeframe of data class="type">class="kw">string m_raw_symbols; class=class="str">"cmt">// delimited symbol list class="type">class="kw">datetime m_synced_first; class=class="str">"cmt">// synced first bar opentime for all symbols class="type">bool m_synced; class=class="str">"cmt">// flag of whether all symbols are synchronized CSymbolData *m_collection[]; class=class="str">"cmt">// Collection of Symbol Pointer class="type">int m_collection_length; class=class="str">"cmt">// Collection of Symbol Length class=class="str">"cmt">//+------------------------------------------------------------------+