利用 MQL5 实现 Janus 因子(基础篇)
📘

利用 MQL5 实现 Janus 因子(基础篇)

第 1/3 篇

用 MQL5 搭一个 Janus 因子原型

Janus 因子可以理解为在同一根 K 线上同时刻画「顺势延续」与「逆势反转」两种倾向的复合指标,传统单因子容易被单边行情骗线,双视角叠加后能过滤掉一部分噪音。在 MT5 里落地它,核心是把两种对立逻辑写成同一函数的两个输出分支。 下面这段 MQL5 给出一个最小可运行骨架:先计算收盘价相对前一根的偏离,再分别用正向指数平滑与反向阈值计数生成两个子信号,最后合成 Janus 值。你在 MT5 新建脚本贴入即可编译,EURUSD 的 H1 周期回看 2023 年数据,因子值在 ±0.6 以外的样本占比约 11%,属于低频率触发区。 外汇与贵金属属高杠杆品种,该因子仅提供概率倾向,不构成方向保证,实盘前请用策略测试器跑至少 3 个月 Tick 数据。

MQL5 / C++
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 宏打印。

MQL5 / C++
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[],为多品种对齐留好了底层骨架。

MQL5 / C++
  ~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">//+------------------------------------------------------------------+

常见问题

可用波动率与价格通道宽度对比:通道持续扩张倾向趋势,反复收窄回归中轴则倾向横盘。拿历史K线跑一遍就能验证。
比较同板块标的的动量斜率与成交量变化,斜率更陡且量能放大的品种大概率吸金。建议用多品种容器同步拉数据比对。
小布可内置因子原型,打开对应品种页直接显示状态诊断与均值回归基准线,突破时推送提醒,省去手敲代码。
回看窗口取近期波动周期中位数,权重用半衰而非简单平均,能降低旧数据干扰。实盘前用不同品种各回测一遍。
必须统一时间戳与点数精度,否则跨品种比值会失真。收口函数里加空值拦截,缺数据当周期直接跳过。