重构经典策略(第七部分):基于USDJPY的外汇市场与主权债务分析·综合运用
📘

重构经典策略(第七部分):基于USDJPY的外汇市场与主权债务分析·综合运用

第 3/3 篇

「给 ONNX 模型喂对输入输出维度」

在 MT5 里加载训练好的 USDJPY 推理模型,第一道坎不是预测,而是把输入输出张量形状钉死。下面这段先校验输入:若 OnnxSetInputShape 返回 false,说明你本地模型的输入节点数量和代码里写的 {1,15} 对不上,直接弹窗报出 OnnxGetInputCount 的真实值,并 return(false) 中断初始化。 输出侧同样不能马虎,output_shape 写死成 {1,1} 表示单次推理只吐一个标量(例如下一根 K 线的回归值)。若 OnnxSetOutputShape 失败,同样报警并退出,避免后续拿空指针做矩阵运算把终端搞崩。

模型维度没问题后,真正喂数据前还得把标准化参数读进来。代码里硬编码了文件名 "usdjpy scaling factors.csv",用 FileOpen 以 FILE_READFILE_CSVFILE_ANSI 打开;若句柄不等于 INVALID_HANDLE 才算打开成功。

读文件时有个隐蔽点:counter 超过 100 就强制 break,这是防 csv 格式异常时死循环。真正落库的逻辑在 counter∈[17,31] 时填 mean_values[0..14],counter∈[33,47] 时填 std_values[0..14]——也就是说你的标准化因子必须严格排在 csv 的第 18 到 32 行(均值)和 34 到 48 行(标准差),否则数组下标会错位。外汇与贵金属杠杆高,模型推理仅是辅助,实盘前务必在策略测试器跑通这段 IO 逻辑。

MQL5 / C++
   Alert("Incorrect input shape, model has input shape ", OnnxGetInputCount(onnx_model));
   class="kw">return(false);
   }
class=class="str">"cmt">//--- Set the output shape
   class="type">ulong output_shape [] = {class="num">1,class="num">1};
class=class="str">"cmt">//--- Check if the output shape is valid
   if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape))
   {
      Alert("Incorrect output shape, model has output shape ", OnnxGetOutputCount(onnx_model));
      class="kw">return(false);
   }
class=class="str">"cmt">//--- Everything went fine
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load our scaling factors                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void load_scaling_factors(class="type">void)
   {
class=class="str">"cmt">//--- Read in the file
   class="type">class="kw">string file_name = "usdjpy scaling factors.csv";
class=class="str">"cmt">//--- Try open the file
   class="type">int result = FileOpen(file_name,FILE_READ|FILE_CSV|FILE_ANSI,","); class=class="str">"cmt">//Strings of ANSI type(one byte symbols). 
class=class="str">"cmt">//--- Check the result
   if(result != INVALID_HANDLE)
   {
      Print("Opened the file");
      class=class="str">"cmt">//--- Store the values of the file
      
      class="type">int counter = class="num">0;
      class="type">class="kw">string value = "";
      
      while(!FileIsEnding(result) && !IsStopped()) class=class="str">"cmt">//read the entire csv file to the end 
      {
      
         if (counter > class="num">100) class=class="str">"cmt">//if you aim to read class="num">10 values set a break point after class="num">10 elements have been read
            break; class=class="str">"cmt">//stop the reading progress
         
         value = FileReadString(result);
         Print("Trying to read class="type">class="kw">string: ",value," count value: ",counter);
         
         class=class="str">"cmt">//--- Check where we are
         if((counter >= class="num">17) && (counter < class="num">32))
            {
               mean_values[counter - class="num">17] = (class="type">class="kw">float) value;
            }  
         class=class="str">"cmt">//--- Check where we are
         if((counter >= class="num">33) && (counter < class="num">48))
            {
               std_values[counter - class="num">33] = (class="type">class="kw">float) value;
            }  
         class=class="str">"cmt">//--- Reading a new row
         if(FileIsLineEnding(result))
            {
               Print("row++");
            }

◍ 把多品种行情塞进ONNX模型前先过标准化

模型推理不是直接把报价丢进去就能跑。上面这段把美债10年、日债10年和美元日元三个品种的 OHLCV 共 15 个字段拼成 vectorf,再逐列减去均值除以标准差,否则不同量纲会把网络激活值冲垮。 标准化循环里 model_inputs[i] = ((model_inputs[i] - mean_values[i])/std_values[i]) 写死长度 15,意味着 mean_values 和 std_values 必须来自训练时同一套统计,否则预测倾向失真。开 MT5 后先确认这两个数组已从前一节的文件里读满 15 项再调 predict。 跑完 OnnxRun 后只用 model_output[0] 做判断:若它大于 USDJPY 当前收盘价,后面才进多分支。外汇与贵金属杠杆高,模型输出只是概率倾向,实盘仍需自设止损。

MQL5 / C++
class="type">void model_predict(class="type">void)
  {
  class=class="str">"cmt">//--- Fetch input values
    class="type">class="kw">string symbols[class="num">3] = {"UST10Y_U4","JGB10Y_U4","USDJPY"};
    vectorf model_inputs = {iOpen(symbols[class="num">0],PERIOD_CURRENT,class="num">0),iHigh(symbols[class="num">0],PERIOD_CURRENT,class="num">0),iLow(symbols[class="num">0],PERIOD_CURRENT,class="num">0),iClose(symbols[class="num">0],PERIOD_CURRENT,class="num">0),iTickVolume(symbols[class="num">0],PERIOD_CURRENT,class="num">0),
                    iOpen(symbols[class="num">1],PERIOD_CURRENT,class="num">0),iHigh(symbols[class="num">1],PERIOD_CURRENT,class="num">0),iLow(symbols[class="num">1],PERIOD_CURRENT,class="num">0),iClose(symbols[class="num">1],PERIOD_CURRENT,class="num">0),iTickVolume(symbols[class="num">1],PERIOD_CURRENT,class="num">0),
                    iOpen(symbols[class="num">2],PERIOD_CURRENT,class="num">0),iHigh(symbols[class="num">2],PERIOD_CURRENT,class="num">0),iLow(symbols[class="num">2],PERIOD_CURRENT,class="num">0),iClose(symbols[class="num">2],PERIOD_CURRENT,class="num">0),iTickVolume(symbols[class="num">2],PERIOD_CURRENT,class="num">0)
                 };
  class=class="str">"cmt">//--- Normalize and scale our inputs
  for(class="type">int i=class="num">0;i < class="num">15;i++)
    {
      model_inputs[i] = ((model_inputs[i] - mean_values[i])/std_values[i]);
    }
  class=class="str">"cmt">//--- Show the inputs
  Print("Model inputs: ",model_inputs);
  class=class="str">"cmt">//--- Fetch a forecast from our model
  OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_output);
  class=class="str">"cmt">//--- Give the user feedback
  Comment("Model forecast: ",model_output[class="num">0]);

  class=class="str">"cmt">//--- Store the prediction
  if(model_output[class="num">0] > iClose("USDJPY",PERIOD_CURRENT,class="num">0))
    {

把 ONNX 模型接进 EA 的生命周期

EA 的初始化函数 OnInit 里先调 load_onnx_file(),加载失败直接返回 INIT_FAILED,MT5 会拒绝启动策略。随后 load_scaling_factors() 读入归一化参数,再跑一次 model_predict() 做冒烟测试,确认模型能正常推理才返回 INIT_SUCCEEDED。 OnDeinit 负责释放资源:OnnxRelease(onnx_model) 清掉模型占用,ExpertRemove() 把 EA 从图表卸掉。不释放的话,反复加载不同 ONNX 文件可能撑爆内存句柄。 OnTick 是实盘心跳。每次报价先 model_predict() 拿最新输出;无持仓时把 state 归零,若模型输出大于 USDJPY 当前收盘价,就用 Trade.Buy 下 0.3 手,止损止盈各挂 2 点(按 SymbolInfoDouble 的 ASK 加减 2),备注写 USDJPY Bonds AI,state 置 1。外汇与贵金属杠杆高,0.3 手在 USDJPY 上若遇跳空可能触发远超 2 点损失的滑点,参数需按账户风控重设。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Load the ONNX file
   if(!load_onnx_file())
     {
class=class="str">"cmt">//--- We failed to load our onnx model
       class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- Load scaling factors
load_scaling_factors();
class=class="str">"cmt">//--- Test if our ONNX model works
model_predict();
class=class="str">"cmt">//--- Everything worked out
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- Release the resources we used for our onnx model
   OnnxRelease(onnx_model);
class=class="str">"cmt">//--- Release the expert advisor
   ExpertRemove();
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Obtain a forecast from our model
    model_predict();
class=class="str">"cmt">//--- Check if we have any positions
    if(PositionsTotal() == class="num">0)
      {
class=class="str">"cmt">//--- Reset the state of our system
        state = class="num">0;
class=class="str">"cmt">//--- Check for an entry
        if(model_output[class="num">0] > iClose("USDJPY",PERIOD_CURRENT,class="num">0))
          {
            Trade.Buy(class="num">0.3,"USDJPY",SymbolInfoDouble("USDJPY",SYMBOL_ASK),SymbolInfoDouble("USDJPY",SYMBOL_ASK)-class="num">2,SymbolInfoDouble("USDJPY",SYMBOL_ASK)+class="num">2,"USDJPY Bonds AI");
            state = class="num">1;
          }
      }
  }

「用模型输出压现价触发美日空单」

这段逻辑把 AI 模型的实时输出和 USDJPY 当前收盘价做了硬比较:只要 model_output[0] 低于 iClose("USDJPY",PERIOD_CURRENT,0),就视为偏空信号,直接以 0.3 手卖出。 卖单参数里,开仓价取 SYMBOL_BID,止损放在 ASK+2 点、止盈放在 ASK-2 点,注释写的品种是 USDJPY Bonds AI,实际下单品种仍是 USDJPY。外汇与贵金属杠杆高,这类 2 点窄止损在美日这种平均日波幅常超 50 点的品种上,可能被瞬时噪音扫掉,概率不低。 反转检查另起一段:当 state 与 prediction 不一致,系统弹 Alert 并平掉 USDJPY 仓位。state 被设成 2 代表已进入空头状态,下次预测值若翻转就可能触发被动平仓。

MQL5 / C++
if(model_output[class="num">0] < iClose("USDJPY",PERIOD_CURRENT,class="num">0))
  {
   Trade.Sell(class="num">0.3,"USDJPY",SymbolInfoDouble("USDJPY",SYMBOL_BID),SymbolInfoDouble("USDJPY",SYMBOL_ASK)+class="num">2,SymbolInfoDouble("USDJPY",SYMBOL_ASK)-class="num">2,"USDJPY Bonds AI");
   state = class="num">2;
  }
 }

class=class="str">"cmt">//--- Check for reversals
 if(state != prediction)
  {
   Alert("Reversal detected by the AI system!");
   Trade.PositionClose("USDJPY");
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

MQL5 / C++
if(model_output[class="num">0] < iClose("USDJPY",PERIOD_CURRENT,class="num">0))
  {
   Trade.Sell(class="num">0.3,"USDJPY",SymbolInfoDouble("USDJPY",SYMBOL_BID),SymbolInfoDouble("USDJPY",SYMBOL_ASK)+class="num">2,SymbolInfoDouble("USDJPY",SYMBOL_ASK)-class="num">2,"USDJPY Bonds AI");
   state = class="num">2;
  }
 }

class=class="str">"cmt">//--- Check for reversals
 if(state != prediction)
  {
   Alert("Reversal detected by the AI system!");
   Trade.PositionClose("USDJPY");
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 记住这一条就够了

把神经网络塞进经典策略未必划算。前面那套 USDJPY_M1_FLOAT.onnx 权重文件只有 0.33 KB,推理快但准确率上限受限于特征质量,作者也承认更简单的市价模型可能就够用。 外汇与贵金属属高风险品类,模型准确率波动会直接放大实盘回撤。若你真要上 AI 策略,先拿 MT5 跑通 USDJPY_Bonds.mq5 看样本外表现,再决定要不要堆特征。 特征没打磨清楚之前,普通报价驱动的轻量策略倾向更稳。少写复杂网络,多调一个参数,可能比训模型更值钱。

常见问题

不同品种量纲和波动幅度差太大,直接拼输入会让模型被高波动项主导。先按各自均值方差做 z-score 标准化,再拼成固定维度向量喂给模型。
在 OnInit 加载模型与会话,OnTick 里取行情拼输入并跑 Run,OnDeinit 释放会话。不要把加载放 OnTick,会拖慢执行。
可以。小布能按你设定的窗口和标准化方式,把美日、美债等行情自动拼成模型所需向量,并标出维度不匹配的报错点。
设阈值如小于 0.35 视为偏空,结合现价未持仓则下空单,并加滑点和点差过滤。外汇和贵金属属高风险,需先复盘验证。
先打印模型签名里的 input/output shape,再对一遍 EA 里拼的数组大小。记住:维度错模型直接抛异常,不会静默出假信号。