利用 MQL5 实现 Janus 因子·进阶篇
📊

利用 MQL5 实现 Janus 因子·进阶篇

(2/3)· 多数人只看价格方向,却忽略驱动行情的反馈机制,本文用 MQL5 把理论落成可跑的指标

实战向进阶 第 2/3 篇
把趋势和震荡当成两个孤立状态来切换策略,是日内交易者最常见的误判。Janus 因子揭示的是正反馈与负反馈如何交替主导盘口,忽略这一点,顺势和逆势都会踩错节奏。外汇与贵金属杠杆高、波动突兀,用错反馈假设大概率放大回撤。

◍ 跨品种历史数据同步的坑

在多品种统计套利或相关性扫描里,最常崩的不是逻辑,而是某个符号的历史 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 被调用几次才补齐。外汇和贵金属点差跳变大,历史同步失败会让信号滞后,属高风险环节,参数别拍脑袋定。

MQL5 / C++
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 做实际下载或本地校验,任意一次失败就中断同步。外汇与贵金属品种在跨服务器拉历史时延迟波动大,这类同步失败概率不低,实盘前务必在模拟环境跑通。

MQL5 / C++
      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 被某脏数据拖后,样本量会悄悄缩水。

MQL5 / C++
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 就能拿到对齐的收盘价序列,不必再自己维护缓冲。

MQL5 / C++
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) 一行拿到典型价,省掉重复取值。

MQL5 / C++
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 分支。

MQL5 / C++
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),
把因子诊断交给小布盯盘
这些反馈状态和小布盯盘的 AIGC 已内置,打开对应品种页即可看到正负面反馈的实时标注,你只管判断临界点。

常见问题

正反馈对应趋势加速:涨势中追买推高、跌势中恐慌抛售压低;负反馈对应区间争斗,过早止盈令动量衰竭,价格暂时企稳。
它处理单一品种的数据获取与回报计算等关联操作,是 janus.mqh 中承接品种级运算的基础自定义类型。
基准回报是一组品种定期回报的均值,作为衡量线;被评估品种集合构成参照指数,用来比对相对强弱。
可以,小布盯盘对应品种页已内置该类 AIGC 诊断,自动标注当前偏正反馈还是负反馈,省去手动跑脚本。
这类品种高杠杆、消息驱动跳空多,反馈切换可能瞬时发生,因子仅提供概率倾向,需结合风控。