在 MQL5 中利用 ARIMA 模型进行预测·进阶篇
📈

在 MQL5 中利用 ARIMA 模型进行预测·进阶篇

(2/3)· 从类封装到输入序列约束,解决混合模型与差异项带来的预测盲区

实战向进阶 第 2/3 篇
不少交易者直接拿上篇的 CArima 类跑预测,结果输出序列和模型阶数对不上。混合 MA 项没有真实误差序列,输入不够长时预测只是数学幻觉。外汇贵金属高杠杆下,这类错误信号可能放大亏损概率。

「把 ARIMA 模型落盘到二进制文件」

做贵金属或外汇的时序建模,最怕 MT5 重启后模型参数丢失。CArima 的 SaveModel 方法直接把阶数、滞后项与残差平方和写进 models\ 目录下的 .model 二进制文件,路径带 FILE_COMMON 标志,意味着它落在终端公共目录,多账户可共享。 函数先拼一个 uint 数组 model_order = {m_const, m_ar_order, m_diff_order, m_ma_order},用 WriteIntegerArray 整体写入。若 AR 阶数非零,再单独写 m_arlags;MA 阶数非零则写 m_malags;随后 WriteDoubleArray 把 m_model 系数数组落盘,最后补一个 m_sse(拟合误差平方和)。 每一处写操作都检查返回值 written,失败就 Print 出错行号与 GetLastError 并 return false。实战里若发现某次保存返回 false,优先查 models 文件夹权限或磁盘满——外汇和贵金属行情高频刷新,模型文件损坏会导致下次加载用错参数,回测与实盘信号可能偏离。 下面这段就是 SaveModel 的核心实现,注意它不序列化字符串名,只靠 model_name 参数拼文件名:

MQL5 / C++
class="type">bool CArima::SaveModel(const class="type">class="kw">string model_name)
  {
   class="type">uint model_order[]= {m_const,m_ar_order,m_diff_order,m_ma_order};
   CFileBin file;
   ResetLastError();
   if(!file.Open("models\\"+model_name+".model",FILE_WRITE|FILE_COMMON))
     {
       Print("Failed to save model.Error: ",GetLastError());
       class="kw">return false;
     }
   m_modelname=(m_modelname=="")?model_name:m_modelname;
   class="type">long written=class="num">0;
   written = file.WriteIntegerArray(model_order);
   if(!written)
     {
       Print("Failed write operation, ",__LINE__,".Error: ",GetLastError());
       class="kw">return false;
     }
   if(m_ar_order)
     {
       written = file.WriteIntegerArray(m_arlags);
       if(!written)
         {
          Print("Failed write operation, ",__LINE__,".Error: ",GetLastError());
          class="kw">return false;
         }
     }
   if(m_ma_order)
     {
       written = file.WriteIntegerArray(m_malags);
       if(!written)
         {
          Print("Failed write operation, ",__LINE__,".Error: ",GetLastError());
          class="kw">return false;
         }
     }
   written = file.WriteDoubleArray(m_model);
   if(!written)
     {
       Print("Failed write operation, ",__LINE__,".Error: ",GetLastError());
       class="kw">return false;
     }
   written = file.WriteDouble(m_sse);
   if(!written)
     {
       Print("Failed write operation, ",__LINE__,".Error: ",GetLastError());
       class="kw">return false;
     }
   file.Close();
   class="kw">return true;
  }

◍ 从二进制模型文件里抠出ARIMA结构

ARIMA 预测模块要干活,第一步是把磁盘上的 .model 二进制文件读进内存并解析出阶数。LoadModel 这段逻辑就是干这个的:先剥离扩展名、拒绝带反斜杠的非法路径,再拼出 models\ 目录下的完整文件名,用 FILE_COMMON 标志去公共目录找文件。 文件打开后,前 4 个 uint 是模型元信息——分别是是否有常数项、AR 阶、差分阶、MA 阶。代码里 ReadIntegerArray(model_order,0,4) 一次性读这 4 个值,随后赋值给 m_const / m_ar_order / m_diff_order / m_ma_order 四个成员。 如果 m_ar_order 大于 0,就从偏移 sizeof(uint)*4 处读 AR 滞后数组;MA 部分则视 AR 是否存在把文件指针再往后跳 sizeof(uint)*ar_order。读不到就 ArrayFree 释放,避免脏数组参与后续计算。 实盘加载前建议先在 MT5 脚本里手动调一次 LoadModel 并打印 m_ar_order / m_ma_order,确认和训练时写的阶数一致。外汇与贵金属波动受事件驱动,模型阶数错配会让预测倾向失真,属于高风险操作。

MQL5 / C++
class="type">bool CArima::LoadModel(const class="type">class="kw">string model_name)
  {
   class="type">int found=StringFind(model_name,".model");
   if(found>=class="num">0)
      m_modelname=StringSubstr(model_name,class="num">0,found);
   else
      m_modelname=model_name;
   if(StringFind(m_modelname,"\\")>=class="num">0)
      class="kw">return false;
   class="type">class="kw">string filename="models\\"+m_modelname+".model";
   if(!FileIsExist(filename,FILE_COMMON))
     {
       Print("Failed to find model, ",__LINE__,".Error: ",GetLastError());
       class="kw">return false;
     }
   CFileBin file;
   ResetLastError();
   if(file.Open(filename,FILE_READ|FILE_COMMON)<class="num">0)
     {
       Print("Failed open operation, ",__LINE__,".Error: ",GetLastError());
       class="kw">return false;
     }
   class="type">uint model_order[];
   file.Seek(class="num">0,SEEK_SET);
   if(!file.ReadIntegerArray(model_order,class="num">0,class="num">4))
     {
       Print("Failed read operation, ",__LINE__,".Error: ",GetLastError());
       class="kw">return false;
     }
   m_const=class="type">bool(model_order[class="num">0]);
   m_ar_order=model_order[class="num">1];
   m_diff_order=model_order[class="num">2];
   m_ma_order=model_order[class="num">3];
   file.Seek(class="kw">sizeof(class="type">uint)*class="num">4,SEEK_SET);
   if(m_ar_order && !file.ReadIntegerArray(m_arlags,class="num">0,m_ar_order))
     {
       Print("Failed read operation, ",__LINE__,".Error: ",GetLastError());
       class="kw">return false;
     }
   if(!m_ar_order)
      ArrayFree(m_arlags);
   if(m_ar_order)
      file.Seek(class="kw">sizeof(class="type">uint)*(class="num">4+m_ar_order),SEEK_SET);
   if(m_ma_order && !file.ReadIntegerArray(m_malags,class="num">0,m_ma_order))
     {
       Print("Failed read operation, ",__LINE__,".Error: ",GetLastError());
       class="kw">return false;
     }
   ArrayPrint(m_malags);
   if(!m_ma_order)
      ArrayFree(m_malags);
   if(m_ar_order || m_ma_order)

从存档里捞模型与BIC/AIC的实算口径

ARIMA 类在做持久化读取时,先按 sizeof(uint)*(4+ar_order+ma_order) 偏移定位参数区,再把长度为 ma_order+ar_order+1 的模型系数读进 m_model 数组;随后在系数区末尾再读一个 double 作为残差平方和 m_sse。若 m_model[1] 非零,则标记 m_istrained=true,否则认为模型未训练。 读盘后顺手把 m_differencedm_leadsm_innovation 三个缓冲区 ZeroMemory,并将 m_insize 归零,等于把上一次差分与滞后状态清空,避免旧序列污染新预测。 BIC 与 AIC 的计算都依赖已训练标记和 m_sse,且要求 m_differenced 里有训练集差分序列。公式上 BIC = n·ln(m_sse/n) + k·ln(n),AIC 则去掉了 k·ln(n) 中的对数样本项权重(标准 AIC 为 n·ln(m_sse/n)+2k,此处代码段未贴完,但 k 定义为 ar_order+ma_order+diff_order+常数开关)。n 取差分后样本量,k 是待估参数总数。 在 MT5 里验证时,可故意让 m_model[1]=0 后调用 BIC(),应看到终端打印 'Model not trained' 并返回 0;这能帮你确认模型加载链路是否真正生效,外汇与贵金属序列用 ARIMA 拟合本身高风险,阶数误选会让 BIC/AIC 失去比较意义。

MQL5 / C++
file.Seek(class="kw">sizeof(class="type">uint)*(class="num">4+m_ar_order+m_ma_order),SEEK_SET);
 if(!file.ReadDoubleArray(m_model,class="num">0,m_ma_order+m_ar_order+class="num">1))
   {
     Print("Failed read operation, ",__LINE__,".Error: ",GetLastError());
     class="kw">return false;
   }
 file.Seek(class="kw">sizeof(class="type">uint)*(class="num">4+m_ar_order+m_ma_order) + class="kw">sizeof(class="type">class="kw">double)*ArraySize(m_model),SEEK_SET);
 if(!file.ReadDouble(m_sse))
   {
     Print("Failed read operation, ",__LINE__,".Error: ",GetLastError());
     class="kw">return false;
   }
 if(m_model[class="num">1])
   m_istrained=true;
 else
   m_istrained=false;
 ZeroMemory(m_differenced);
 ZeroMemory(m_leads);
 ZeroMemory(m_innovation);
 m_insize=class="num">0;
 class="kw">return true;
}
class="type">class="kw">string GetModelName(class="type">void) { class="kw">return m_modelname;}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| calculate the bayesian information criterion                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CArima::BIC(class="type">void)
  {
   if(!m_istrained||!m_sse)
     {
      Print(m_modelname," Model not trained. Train the model first to calculate the BIC.");
      class="kw">return class="num">0;
     }
   if(!m_differenced.Size())
     {
      Print("To calculate the BIC, supply a training data set");
      class="kw">return class="num">0;
     }
   class="type">uint n = m_differenced.Size();
   class="type">uint k = m_ar_order+m_ma_order+m_diff_order+class="type">uint(m_const);
   class="kw">return((n*MathLog(m_sse/n)) + (k*MathLog(n)));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CArima::AIC(class="type">void)
  {
   if(!m_istrained||!m_sse)
     {
      Print(m_modelname," Model not trained. Train the model first to calculate the AIC.");
      class="kw">return class="num">0;
     }
   if(!m_differenced.Size())
     {
      Print("To calculate the AIC, supply a training data set");
      class="kw">return class="num">0;
     }
   class="type">uint n = m_differenced.Size();
   class="type">uint k = m_ar_order+m_ma_order+m_diff_order+class="type">uint(m_const);

「ARIMA 模型汇总与最小样本约束」

这段 CArima 类的收尾逻辑,把拟合结果和样本门槛一次性暴露出来,方便在 MT5 策略测试器里直接读日志。 GetMinModelInputs 返回的是建模所需最少样本数:差分阶数 m_diff_order,加上最大自回归滞后 GetMaxArLag(),再加上最大移动平均滞后乘上输入因子 m_infactor。实际调参时,若历史 K 线数低于该返回值,模型会直接缺数据,EURUSD 这类点差跳变品种更容易踩坑。 Summary 函数拼出模型名与 Arima(p,d,q) 结构,先打 SSE(误差平方和),再按常量、AR 系数、MA 系数顺序逐行输出。打开 MT5 终端日志,能看见类似「AR coefficient at lag 1: 0.83」的明文,据此判断阶数是否过拟合。 外汇与贵金属杠杆高,ARIMA 残差若在非平稳段膨胀,SSE 会失真,信号概率层面只作参考。

MQL5 / C++
  class="kw">return((class="num">2.0*k)+(class="type">class="kw">double(n)*MathLog(m_sse/class="type">class="kw">double(n))));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">uint                GetMinModelInputs(class="type">void)                 { class="kw">return(m_diff_order + GetMaxArLag() + (GetMaxMaLag()*m_infactor));}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CArima::Summary(class="type">void)
  {
   class="type">class="kw">string print = m_modelname+" Arima("+IntegerToString(m_ar_order)+","+IntegerToString(m_diff_order)+","+IntegerToString(m_ma_order)+")\n";
   print+= "SSE : "+class="type">class="kw">string(m_sse);
   class="type">int k=class="num">0;
   if(m_const)
      print+="\nConstant: "+class="type">class="kw">string(m_model[k++]);
   else
      k++;
   for(class="type">uint i=class="num">0; i<m_ar_order; i++)
      print+="\nAR coefficient at lag "+IntegerToString(m_arlags[i])+": "+class="type">class="kw">string(m_model[k++]);
   for(class="type">uint j=class="num">0; j<m_ma_order; j++)
      print+="\nMA coefficient at lag "+IntegerToString(m_malags[j])+": "+class="type">class="kw">string(m_model[k++]);
   Print(print);
   class="kw">return;
  }

◍ 两套 Predict 重载怎么调

ARIMA 类把预测入口拆成了两个 Predict() 重载,核心差异只在输入序列由谁提供。第一个重载只吃 num_pred 和 predictions[],直接拿训练时缓存的 m_differenced 序列往后推;第二个重载多了 in_raw[],允许你塞一段新原始序列,适合在线更新后再预测。两者都返回 bool,true 代表跑通,false 多半是未训练或没数据。 内部真正算数的是私有 evaluate(num_pred),它按模型阶数扫最多五个数组,把新预测写进差分序列、把残差(创新项)续到 m_innovation,再拷给 predictions。看第一段重载代码:若 num_pred 为 0 直接 Print 报错返回 false;若 m_istrained 或 m_insize 为空,则清空输出数组并提示「Model not trained」或「No input data」。 有差分阶数时(m_diff_order≠0),代码会调 integrate() 还原成原始价,再用 ArrayCopy 从 m_insize+m_diff_order 位置抽取预测段;无差分则直接从 m_insize 拷 m_differenced。想验证,开 MT5 挂个 EURUSD 的 H1 历史,训练完调 Predict(10, preds) 看 preds 是不是出了 10 个值——外汇与贵金属杠杆高,模型外推仅作概率参考,实盘须控仓。 [CODE] bool Predict(const uint num_pred,double &predictions[]); bool Predict(const uint num_pred,double &in_raw[], double &predictions[]); bool SaveModel(const string model_name); bool LoadModel(const string model_name); double BIC(void); double AIC(void); //+------------------------------------------------------------------+

//

//+------------------------------------------------------------------+ bool CArima::Predict(const uint num_pred,double &predictions[]) { if(!num_pred) { Print("Invalid number of predictions"); return false; }

if(!m_istrained!m_insize)

{ ZeroMemory(predictions); if(m_istrained) Print("Model not trained"); else Print("No input data available to make predictions"); return false; } ArrayResize(m_differenced,ArraySize(m_differenced)+num_pred,num_pred); ArrayResize(m_innovation,ArraySize(m_differenced)); evaluate(num_pred); if(m_diff_order) { double raw[]; integrate(m_differenced,m_leads,raw); ArrayPrint(raw,_Digits,NULL,m_insize-5); ArrayCopy(predictions,raw,0,m_insize+m_diff_order); ArrayFree(raw); } else ArrayCopy(predictions,m_differenced,0,m_insize); return true; } //+------------------------------------------------------------------+

//

//+------------------------------------------------------------------+ bool CArima::Predict(const uint num_pred,double &in_raw[],double &predictions[]) {

MQL5 / C++
  class="type">bool                Predict(const class="type">uint num_pred,class="type">class="kw">double &predictions[]);
  class="type">bool                Predict(const class="type">uint num_pred,class="type">class="kw">double &in_raw[], class="type">class="kw">double &predictions[]);
  class="type">bool                SaveModel(const class="type">class="kw">string model_name);
  class="type">bool                LoadModel(const class="type">class="kw">string model_name);
  class="type">class="kw">double              BIC(class="type">void);
  class="type">class="kw">double              AIC(class="type">void);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CArima::Predict(const class="type">uint num_pred,class="type">class="kw">double &predictions[])
  {
   if(!num_pred)
     {
      Print("Invalid number of predictions");
      class="kw">return false;
     }
   if(!m_istrained || !m_insize)
     {
      ZeroMemory(predictions);
      if(m_istrained)
        Print("Model not trained");
      else
        Print("No class="kw">input data available to make predictions");
      class="kw">return false;
     }
   ArrayResize(m_differenced,ArraySize(m_differenced)+num_pred,num_pred);
   ArrayResize(m_innovation,ArraySize(m_differenced));
   evaluate(num_pred);
   if(m_diff_order)
     {
      class="type">class="kw">double raw[];
      integrate(m_differenced,m_leads,raw);
      ArrayPrint(raw,_Digits,NULL,m_insize-class="num">5);
      ArrayCopy(predictions,raw,class="num">0,m_insize+m_diff_order);
      ArrayFree(raw);
     }
   else
      ArrayCopy(predictions,m_differenced,class="num">0,m_insize);
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CArima::Predict(const class="type">uint num_pred,class="type">class="kw">double &in_raw[],class="type">class="kw">double &predictions[])
  {

ARIMA 预测调用的前置闸门与回积逻辑

预测函数入口先卡三道闸:预测步数 num_pred 非法、模型未训练 m_istrained 为假、输入序列长度不足 numofinputs,任一不满足就清零 predictions 并 return false。其中 numofinputs 由 AR 最大滞后、MA 最大滞后乘 m_infactor、差分阶数三者累加,实盘里若喂入的 in_raw 比这个数短,终端会直接打印所需尺寸而中断,不会静默出假信号。 过了闸门后,若设了差分阶数就先 difference 得到 m_differenced,否则直接 ArrayCopy 原序列;随后把 m_differenced 扩到 m_insize+num_pred 并向后填 0,m_innovation 同步扩容并初始化为 0。这一步保证了 ARMA 递推时尾部空位不携脏数据。 evaluate(num_pred) 跑完递推后,若有差分则 integrate 还原成 raw 再截拷到 predictions,否则直接拷 m_differenced。外汇与贵金属价格用这套做多步推演时波动聚集明显,模型外推超过 10 步后误差可能快速放大,属高风险用法,建议先在 MT5 策略测试器用历史 tick 验证阶数组合。

MQL5 / C++
  if(!num_pred)
    {
      Print("Invalid number of predictions");
      class="kw">return false;
    }
  if(!m_istrained)
    {
      ZeroMemory(predictions);
      Print("Model not trained");
      class="kw">return false;
    }
  class="type">int numofinputs=class="num">0;
  if(m_ar_order)
    numofinputs+=(class="type">int)GetMaxArLag();
  if(m_ma_order)
    numofinputs+=class="type">int(GetMaxMaLag()*m_infactor);
  if(m_diff_order)
    numofinputs+=(class="type">int)m_diff_order;
  if(in_raw.Size()<(class="type">uint)numofinputs)
    {
      ZeroMemory(predictions);
      Print("Input dataset size inadequate. Size required: ",numofinputs);
      class="kw">return false;
    }
  ZeroMemory(m_differenced);
  if(m_diff_order)
    {
      difference(m_diff_order,in_raw,m_differenced,m_leads);
      m_insize=m_differenced.Size();
    }
  else
    {
      m_insize=in_raw.Size();
      ArrayCopy(m_differenced,in_raw);
    }
  if(m_differenced.Size()!=(m_insize+num_pred))
    ArrayResize(m_differenced,m_insize+num_pred,num_pred);
  ArrayFill(m_differenced,m_insize,num_pred,class="num">0.0);
  if(m_innovation.Size()!=m_insize+num_pred)
    ArrayResize(m_innovation,ArraySize(m_differenced));
  ArrayInitialize(m_innovation,class="num">0.0);
  evaluate(num_pred);
  if(m_diff_order)
    {
      class="type">class="kw">double raw[];
      integrate(m_differenced,m_leads,raw);
      ArrayCopy(predictions,raw,class="num">0,m_insize+m_diff_order);
      ArrayFree(raw);
    }
  else
    ArrayCopy(predictions,m_differenced,class="num">0,m_insize);
  class="kw">return true;
  }
class="type">void CArima::evaluate(const class="type">uint num_p)
  {
  class="type">class="kw">double pred=class="num">0;
  class="type">uint start_shift=(m_ma_order)?((!m_innovation[m_insize-class="num">1])?m_insize-(GetMaxMaLag()*m_infactor):m_insize):m_insize;
  class="type">uint d_size=(class="type">uint)ArraySize(m_differenced);
  class="type">int p_shift;
  for(class="type">uint i=start_shift; i<d_size; i++)
    {
      p_shift=class="num">0;
      pred=class="num">0;
      if(i>=m_insize)
        m_innovation[i]=class="num">0.0;
      if(m_const)
        pred+=m_model[p_shift++];
      for(class="type">uint j=class="num">0; j<m_ar_order; j++)
把冗余循环交给小布盯盘
计算初始误差的冗余预测循环很耗神,这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到输入尺度是否达标,你专注决策。

常见问题

纯 AR 模型所需输入与最大滞后相等,例如滞后 4 就至少需 4 个时序值来维持时间关系,实际往往略多。
因没有真实误差序列,须先假定 MA 项为 0 跑初始状态,再依据已知值循环算初始误差,冗余次数影响误差有效性。
任一 AR 或 MA 的最大滞后都会拉高输入尺度,即使只用到个别滞后点,也要补足中间时序槽位。
能,小布盯盘的品种页可展示当前输入长度与模型阶数匹配度,减少手动核算出错概率。
差分后序列比原序列少一个长度,传递进模型前需额外补偿输入,具体数量由差异阶数决定。