在 MQL5 中利用 ARIMA 模型进行预测·进阶篇
(2/3)· 从类封装到输入序列约束,解决混合模型与差异项带来的预测盲区
「把 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 参数拼文件名:
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,确认和训练时写的阶数一致。外汇与贵金属波动受事件驱动,模型阶数错配会让预测倾向失真,属于高风险操作。
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_differenced、m_leads、m_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 失去比较意义。
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 会失真,信号概率层面只作参考。
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[]) {
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 验证阶数组合。
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++)