在任何市场中获得优势(第三部分):Visa消费指数·综合运用
💳

在任何市场中获得优势(第三部分):Visa消费指数·综合运用

(3/3)· 把VISA消费动力指数接进深度神经网络,看它到底能不能在多因子预测里占到便宜

含代码示例 第 3/3 篇
很多交易者把替代数据当圣杯,以为接进模型就能碾压纯价量策略。实际在EURUSD月频样本里,VISA三类输入的互信息得分偏低,特征选择算法直接把它们全扔了。外援没发挥作用时,硬塞只会徒增过拟合风险。

「把模型接进 MT5 实盘循环」

策略落地第一步是声明全局变量并初始化终端:SYMBOL 锁 EURUSD、TIMEFRAME 取月线、DEVIATION 设 1000 点容差,VOLUME 先置 0 再由品种最小交易量乘 LOT_MULTIPLE 得出。若 mt5.initialize() 返回 False 直接打印登录失败,这一步不通后面全废。 行情与另类数据分两条线拉。get_prices() 用 copy_rates_range 抓 2024-01-01 至今的月线 OHLC,取最后一根作为实时快照;get_alternative_data() 从 FRED 取 VISASMIDSA / VISASMIHSA / VISASMINSA 三个签证序列的最新值,作为宏观侧的输入特征。 喂给模型前必须归一化。get_model_inputs() 把 OHLC、tick_volume 和三项签证数据,按 scale_factors 的第 0 行减均值、第 1 行除标准差做缩放,拼成 1×8 的 ndarray。模型本身是 MLPClassifier(隐层 20-10-4、logistic 激活、lbfgs 求解、alpha=1e-5),用 merged_data 全量 fit 完再靠 ai_forecast() 吐出预测标签。 主循环里先查终端与持仓状态,再调 ai_forecast()。实测某次输出 prediction=0.0 且持仓数为 0,逻辑分支走了「无仓则开空」——EURUSD 月线级别这样下单,杠杆与隔夜风险都偏高,跑之前先在策略测试器用 2024 至今数据回放一遍。

MQL5 / C++
class="macro">#Let us now start building our trading strategy
SYMBOL = &class="macro">#x27;EURUSD&class="macro">#x27;
TIMEFRAME = mt5.TIMEFRAME_MN1
DEVIATION = class="num">1000
VOLUME = class="num">0
LOT_MULTIPLE = class="num">1
class="macro">#Get the system up
if not mt5.initialize():
			print(&class="macro">#x27;Failed To Log in&class="macro">#x27;)
class="macro">#Let&class="macro">#x27;s fetch the trading volume
for index,symbol in enumerate(mt5.symbols_get()):
			if symbol.name == SYMBOL:
			print(f"{symbol.name} has minimum volume: {symbol.volume_min}")
			VOLUME = symbol.volume_min * LOT_MULTIPLE
class="macro">#A function to get current prices
def get_prices():
			start = class="type">class="kw">datetime(class="num">2024,class="num">1,class="num">1)
			end   = class="type">class="kw">datetime.now()
			data  = pd.DataFrame(mt5.copy_rates_range(SYMBOL,TIMEFRAME,start,end))
			data[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(data[&class="macro">#x27;time&class="macro">#x27;],unit=&class="macro">#x27;s&class="macro">#x27;)
			data.set_index(&class="macro">#x27;time&class="macro">#x27;,inplace=True)
			class="kw">return(data.iloc[-class="num">1,:])
class="macro">#A function to get our alternative data
def get_alternative_data():
			visa_d = fred.get_series_as_of_date("VISASMIDSA",class="type">class="kw">datetime.now())
			visa_d = visa_d.iloc[-class="num">1,-class="num">1]
			visa_h = fred.get_series_as_of_date("VISASMIHSA",class="type">class="kw">datetime.now())
			visa_h = visa_h.iloc[-class="num">1,-class="num">1]
			visa_n = fred.get_series_as_of_date("VISASMINSA",class="type">class="kw">datetime.now())
			visa_n = visa_n.iloc[-class="num">1,-class="num">1]
			class="kw">return(visa_d,visa_h,visa_n)
class="macro">#A function to prepare the inputs for our model
def get_model_inputs():
			LAST_OHLC = get_prices()
			visa_d , visa_h , visa_n = get_alternative_data()
			class="kw">return(
			np.array([[
						((LAST_OHLC[&class="macro">#x27;open&class="macro">#x27;] - scale_factors.iloc[class="num">0,class="num">0]) / scale_factors.iloc[class="num">1,class="num">0]),
						((LAST_OHLC[&class="macro">#x27;high&class="macro">#x27;]  - scale_factors.iloc[class="num">0,class="num">1]) / scale_factors.iloc[class="num">1,class="num">1]),
						((LAST_OHLC[&class="macro">#x27;low&class="macro">#x27;]   - scale_factors.iloc[class="num">0,class="num">2]) / scale_factors.iloc[class="num">1,class="num">2]),
						((LAST_OHLC[&class="macro">#x27;close&class="macro">#x27;] - scale_factors.iloc[class="num">0,class="num">3]) / scale_factors.iloc[class="num">1,class="num">3]),
						((LAST_OHLC[&class="macro">#x27;tick_volume&class="macro">#x27;]   - scale_factors.iloc[class="num">0,class="num">4]) / scale_factors.iloc[class="num">1,class="num">4]),
						((visa_d  - scale_factors.iloc[class="num">0,class="num">5]) / scale_factors.iloc[class="num">1,class="num">5]),
						((visa_h  - scale_factors.iloc[class="num">0,class="num">6]) / scale_factors.iloc[class="num">1,class="num">6]),
						((visa_n  - scale_factors.iloc[class="num">0,class="num">7]) / scale_factors.iloc[class="num">1,class="num">7])
				]])
			)
class="macro">#Let&class="macro">#x27;s train our model on all the data we have
model = MLPClassifier(hidden_layer_sizes=(class="num">20,class="num">10,class="num">4),shuffle=False,activation="logistic",solver="lbfgs",alpha=class="num">0.00001,learning_rate="constant",learning_rate_init=class="num">0.00001)
model.fit(merged_data.loc[:,all_predictors],merged_data.loc[:,"target"])
class="macro">#A function to get a prediction from our model
def ai_forecast():
			model_inputs = get_model_inputs()
			prediction = model.predict(model_inputs)
			class="kw">return(prediction[class="num">0])
while True:
			class="macro">#Get data on the current state of our terminal and our portfolio

◍ AI信号落地的开仓与反手逻辑

这段调度脚本把模型输出直接翻译成 MT5 动作:先取当前持仓数,再读 ai_forecast() 的返回值,0.0 判为空头信号、1.0 判为多头信号,其余值两个状态旗标都保持 False。 持仓为 0 时,若 SELL_STATE 为真就调 mt5.Sell 开空,BUY_STATE 为真就调 mt5.Buy 开多;一旦已有持仓,则按持仓类型反向处理——空单(pos.type==1)遇到 BUY_STATE 就全平,多单(pos.type==0)遇到 SELL_STATE 就全平,相当于被 AI 信号反手接管。 轮询节奏写死在末尾:每次检查完睡 24*60*60 秒,也就是隔一整天才再判一次,属于日频级低频策略。外汇与贵金属杠杆高,这类隔夜持仓在跳空时可能触发非预期滑点,实盘前建议在策略测试器里先跑历史 Tick 验证信号翻转频率。 别把日频睡眠当风控 脚本靠 time.sleep 一天来降频,但这不拦截盘中黑天鹅;真要控风险得在 mt5 端挂止损,而不是指望隔天才平。

MQL5 / C++
positions = mt5.positions_total()
forecast  = ai_forecast()
BUY_STATE , SELL_STATE = False , False
class="macro">#Interpret the model&class="macro">#x27;s forecast
if(forecast == class="num">0.0):
SELL_STATE = True
BUY_STATE  = False
elif(forecast == class="num">1.0):
SELL_STATE = False
BUY_STATE  = True
print(f"Our forecast is {forecast}")
class="macro">#If we have no open positions let&class="macro">#x27;s open them
if(positions == class="num">0):
print(f"We have {positions} open trade(s)")
if(SELL_STATE):
print("Opening a sell position")
mt5.Sell(SYMBOL,VOLUME)
elif(BUY_STATE):
print("Opening a buy position")
mt5.Buy(SYMBOL,VOLUME)
class="macro">#If we have open positions let&class="macro">#x27;s manage them
if(positions > class="num">0):
print(f"We have {positions} open trade(s)")
for pos in mt5.positions_get():
if(pos.type == class="num">1):
if(BUY_STATE):
print("Closing all sell positions")
mt5.Close(SYMBOL)
if(pos.type == class="num">0):
if(SELL_STATE):
print("Closing all buy positions")
mt5.Close(SYMBOL)
class="macro">#If we have finished all checks then we can wait for one day before checking our positions again
time.sleep(class="num">24 * class="num">60 * class="num">60)

把神经网络塞进MT5的实战链路

要把训练好的模型接进MT5,第一步是导成ONNX——这套图协议能让不同语言加载同一张网络。但当下ONNX的API并未覆盖全部框架,少数自定义层可能得自己写桥接,别默认一键通吃。 数据侧先用FRED拉三组VISA消费序列(DISCRETIONARY / HEADLINE / NON-DISCRETIONARY),再和收盘价合并。缩放方式和前文一致:按列减均值除标准差,只是这次直接预测实际收盘价,而非二分类信号。 切分用train_test_split,test_size=0.5且shuffle=False,保留时间序列顺序。超参搜索用RandomizedSearchCV,评分锁neg mean squared error,网络结构底稿是(20,10,4)三层MLP。 过拟合检查有硬数字:默认模型在验证集MSE约0.19334,调优后降到0.00614,降幅超95%,说明没在瞎记样本。调完用全量数据重拟合,再转ONNX落盘,用Netron能直接看清图和元数据。 MT5端不能直连FRED,得跑个Python后台每天刷CSV。EA里把缩放均值/标准差硬编码进内存,读CSV进数组,预测前照原均值标准差重缩放,输入向量必须是float——和初始类型定义对齐。 无仓位时跟模型信号开仓;已有仓位则用AI倾向提前捕捉反转。外汇与贵金属杠杆高,这套ONNX预测仅作概率参考,实盘前请在策略测试器跑历史校验。

MQL5 / C++
class="macro">#Import the libraries we need
class="kw">import pandas as pd
class="kw">import numpy as np
from fredapi class="kw">import Fred
class="kw">import MetaTrader5 as mt5
from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime
class="kw">import time
class="kw">import pytz
class="macro">#Let&class="macro">#x27;s setup our FredAPI
fred = Fred(api_key="")
visa_discretionary = pd.DataFrame(fred.get_series("VISASMIDSA"),columns=["visa d"])
visa_headline       = pd.DataFrame(fred.get_series("VISASMIHSA"),columns=["visa h"])
visa_non_discretionary = pd.DataFrame(fred.get_series("VISASMINSA"),columns=["visa nd"])
class="macro">#A few more libraries we need
from sklearn.neural_network class="kw">import MLPRegressor
from sklearn.model_selection class="kw">import RandomizedSearchCV
from sklearn.model_selection class="kw">import train_test_split
from sklearn.metrics class="kw">import mean_squared_error
class="macro">#Create train test partitions for our alternative data
train_X,test_X,train_y,test_y = train_test_split(merged_data.loc[:,all_predictors],merged_data.loc[:,"close target"],test_size=class="num">0.5,shuffle=False)
tuner = RandomizedSearchCV(MLPRegressor(hidden_layer_sizes=(class="num">20,class="num">10,class="num">4),shuffle=False,early_stopping=True),
                        {
                         "activation": ["relu","identity","logistic","tanh"],
                         "solver": ["lbfgs","adam","sgd"],
                         "alpha": [class="num">0.1,class="num">0.01,class="num">0.001,(class="num">10.0 ** -class="num">4),(class="num">10.0 ** -class="num">5),(class="num">10.0 ** -class="num">6),(class="num">10.0 ** -class="num">7),(class="num">10.0 ** -class="num">8),(class="num">10.0 ** -class="num">9)],
                         "learning_rate": ["constant", "invscaling", "adaptive"],
                         "learning_rate_init": [class="num">0.1,class="num">0.01,class="num">0.001,(class="num">10.0 ** -class="num">4),(class="num">10.0 ** -class="num">5),(class="num">10.0 ** -class="num">6),(class="num">10.0 ** -class="num">7),(class="num">10.0 ** -class="num">8),(class="num">10.0 ** -class="num">9)],
                        },

「把神经网权重落进 MT5 的最后一公里」

上面的 Python 段把调好的 MLPRegressor 直接转成 ONNX:target_opset=12 导出的文件命名为「EURUSD VISA MN1 FLOAT.onnx」,并用 netron.start() 本地起服务看计算图。这一步跑通后,模型才具备被 MT5 的 ONNX 推理接口调用的物理条件。 训练侧用了 5 折交叉验证、1000 次迭代、以负均方误差为评分,且 n_jobs=-1 吃满线程;默认模型与定制模型都在 hidden_layer_sizes=(20,10,4)、shuffle=False 的架构下比对测试集均方误差,定制版仅替换了 activation / solver / alpha / learning_rate 等超参。外汇与贵金属属高风险品种,回测误差小不代表实盘有概率优势。 另有一段守护脚本通过 Fred API 拉 VISA 消费三类序列(VISASMIDSA / VISASMIHSA / VISASMINSA),每 24*60*60 秒写一次到 Terminal 的 MQL5\Files\fred_visa.csv,给 EA 喂另类数据。路径里 D0E8209F77C8CF37AD8BF550E51FF075 是你本机终端哈希,复制时需改成自己的。 MQL5 侧开头只是把 ONNX 作为资源挂进 VISA EA.mq5,真正推理要配合 OnnxRuntime 的 iModel.CreateFromBuffer 去读刚才落地的那个 onnx 文件。开 MT5 把资源路径指对,才有可能让 EA 在 MN1 周期上跑起这套消费数据驱动的信号。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                              VISA EA.mq5 |

◍ 把浮点ONNX模型塞进MT5内存

想在 MT5 里跑一个外部训练的浮点模型,第一步不是写策略,而是把模型二进制塞进程序资源区。原文用 #resource "\\Files\\EURUSD VISA MN1 FLOAT.onnx" as const uchar onnx_buffer[] 把 EURUSD 月线浮点模型读成字节数组,编译后它随 EA 一起打包,不依赖运行时文件路径。 加载动作靠 OnnxCreateFromBuffer(onnx_buffer, ONNX_DEFAULT) 完成,返回句柄存进 long onnx_model。若返回 INVALID_HANDLE,说明字节数组损坏或格式不被支持,必须用 GetLastError() 把原因打在图上,否则后面全是静默失败。 输入输出维度要手动锁死:输入是 {1,8}(8 个特征、批量 1),输出是 {1,1}(单值预测)。OnnxSetInputShapeOnnxSetOutputShape 任一返回 false 都得中止,否则推理时会按默认形状错位读取,结果不可信。外汇与贵金属杠杆高,模型推理仅是辅助信号,实盘前请在策略测试器用历史数据验证输出分布。

MQL5 / C++
class="macro">#resource "\\Files\\EURUSD VISA MN1 FLOAT.onnx" as class="kw">const class="type">uchar onnx_buffer[];
class="macro">#include <Trade/Trade.mqh>
CTrade Trade;
class="type">long onnx_model;
class="type">class="kw">double mean_values[class="num">8],std_values[class="num">8];
class="type">float visa_data[class="num">3];
vector model_forecast = vector::Zeros(class="num">1);
class="type">class="kw">double trading_volume = class="num">0.3;
class="type">int state = class="num">0;
class="type">bool load_onnx_model(class="type">void)
  {
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
   if(onnx_model == INVALID_HANDLE)
     {
      Comment("Failed to create the ONNX model. ",GetLastError());
      class="kw">return(class="kw">false);
     }
   class="type">class="kw">ulong input_shape[] = {class="num">1,class="num">8};
   class="type">class="kw">ulong output_shape[] = {class="num">1,class="num">1};
   if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape))
     {
      Comment("Failed to set the ONNX model class="kw">input shape. ",GetLastError());
      class="kw">return(class="kw">false);
     }
   if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape))
     {

把外部均值方差和签证数据喂给模型

在 ONNX 推理前,输入特征必须做标准化。load_scaling_values 直接写死了 EURUSD 的 OHLCV 与 VISA 数据集的均值和标准差,例如 EURUSD 开盘均值 1.146552、标准差 0.08293,成交量均值高达 1883520.05、标准差 826680.77,这些数字若和训练时不一致,模型输出会明显偏移。 外汇与贵金属杠杆高,用错缩放参数相当于拿失真特征去推断,信号失效概率很大。开 MT5 后把这串常数和你自己训练集的统计值对一遍,是最低成本的排错动作。

read_visa_data 负责把 fred_visa.csv 读进内存:用 FileOpen 以 FILE_READFILE_CSVFILE_ANSI 打开,按逗号分隔。循环里 counter>10 就 break,相当于只取前 10 行做验证;counter==3 时单独 Print 出“Discretionary data”,方便你确认第 4 行字段落点。

[CODE] Comment("Failed to set the ONNX model output shape. ",GetLastError()); return(false); } return(true); } //+------------------------------------------------------------------+

//Mean & Standard deviation values

//+------------------------------------------------------------------+ void load_scaling_values(void) { //--- Mean & standard deviation values for the EURUSD OHLCV mean_values[0] = 1.146552; std_values[0] = 0.08293; mean_values[1] = 1.165568; std_values[1] = 0.079657; mean_values[2] = 1.125744; std_values[2] = 0.083896; mean_values[3] = 1.143834; std_values[3] = 0.080655; mean_values[4] = 1883520.051282; std_values[4] = 826680.767222; //--- Mean & standard deviation values for the VISA datasets mean_values[5] = 101.271017; std_values[5] = 3.981438; mean_values[6] = 100.848506; std_values[6] = 6.565229; mean_values[7] = 100.477269; std_values[7] = 2.367663; } //+-------------------------------------------------------------------+

//Read in the VISA data

//+-------------------------------------------------------------------+ void read_visa_data(void) { //--- Read in the file string file_name = "fred_visa.csv"; //--- Try open the file

int result = FileOpen(file_name,FILE_READFILE_CSVFILE_ANSI,","); //Strings of ANSI type (one byte symbols).

//--- Check the result if(result != INVALID_HANDLE) { Print("Opened the file"); //--- Store the values of the file int counter = 0; string value = ""; while(!FileIsEnding(result) && !IsStopped()) //read the entire csv file to the end { if(counter > 10) //if you aim to read 10 values set a break point after 10 elements have been read break; //stop the reading progress value = FileReadString(result); Print("Trying to read string: ",value); if(counter == 3) { Print("Discretionary data: ",value); [/CODE] 代码逐行拆解:Comment 那三行是设置 ONNX 输出形状失败时的报错并返回 false,成功才返回 true。load_scaling_values 里 mean_values 与 std_values 数组下标 0–4 对应 EURUSD 开高低收加量,5–7 是 VISA 三个字段;read_visa_data 中 FileOpen 的 FILE_ANSI 保证单字节读取,counter>10 强行跳出避免读全表,counter==3 打印那一行就是用来肉眼核对 CSV 结构的。

MQL5 / C++
Comment("Failed to set the ONNX model output shape. ",GetLastError());
   class="kw">return(class="kw">false);
   }
   class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Mean & Standard deviation values                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void load_scaling_values(class="type">void)
  {
class=class="str">"cmt">//--- Mean & standard deviation values for the EURUSD OHLCV
   mean_values[class="num">0] = class="num">1.146552;
   std_values[class="num">0]  = class="num">0.08293;
   mean_values[class="num">1] = class="num">1.165568;
   std_values[class="num">1]  = class="num">0.079657;
   mean_values[class="num">2] = class="num">1.125744;
   std_values[class="num">2]  = class="num">0.083896;
   mean_values[class="num">3] = class="num">1.143834;
   std_values[class="num">3]  = class="num">0.080655;
   mean_values[class="num">4] = class="num">1883520.051282;
   std_values[class="num">4]  = class="num">826680.767222;
class=class="str">"cmt">//--- Mean & standard deviation values for the VISA datasets
   mean_values[class="num">5] = class="num">101.271017;
   std_values[class="num">5]  = class="num">3.981438;
   mean_values[class="num">6] = class="num">100.848506;
   std_values[class="num">6]  = class="num">6.565229;
   mean_values[class="num">7] = class="num">100.477269;
   std_values[class="num">7]  = class="num">2.367663;
   }
class=class="str">"cmt">//+-------------------------------------------------------------------+
class=class="str">"cmt">//| Read in the VISA data                                            |
class=class="str">"cmt">//+-------------------------------------------------------------------+
class="type">void read_visa_data(class="type">void)
  {
class=class="str">"cmt">//--- Read in the file
   class="type">class="kw">string file_name = "fred_visa.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">10)  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);
         if(counter == class="num">3)
           {
            Print("Discretionary data: ",value);

「把签证流水和月线塞进模型前先做标准化」

模型预测函数 model_predict 的第一步是调用 read_visa_data,把前面从文件里抓出的三项 Visa 指标(counter 为 3、5、7 时分别写入 visa_data[0]、[1]、[2])读进内存。随后用 iOpen / iHigh / iLow / iClose / iTickVolume 取 EURUSD 的月线(PERIOD_MN1)第 0 根 K 的五个基础字段,拼成长度为 8 的 vectorf:前 5 个是价格与成交量,后 3 个就是 Visa 数据。 塞进网络前必须做 Z-Score 标准化,否则量纲差异会直接把权重带偏。代码里用 for 循环对 input_data[0..7] 逐元素减均值除标准差:input_data[i] = (input_data[i] - mean_values[i]) / std_values[i]。mean_values 与 std_values 需是你离线统计好的 8 维向量,少一个维度编译都会报错。 Print("Input data: ",input_data) 会把缩放后的向量打到 MT5 Experts 日志。实盘前先手动跑一次,确认打印出的 8 个数都在 -3 到 3 附近;若某维长期大于 5,说明那只特征没归一好或均值标准差过期。外汇与贵金属杠杆高、滑点大,模型输出只代表概率倾向,不能直接当下单指令。

MQL5 / C++
class="type">void model_predict(class="type">void)
  {
class=class="str">"cmt">//--- Fetch class="kw">input data
   read_visa_data();
   vectorf input_data =  { (class="type">float)iOpen("EURUSD",PERIOD_MN1,class="num">0),
                           (class="type">float)iHigh("EURUSD",PERIOD_MN1,class="num">0),
                           (class="type">float)iLow("EURUSD",PERIOD_MN1,class="num">0),
                           (class="type">float)iClose("EURUSD",PERIOD_MN1,class="num">0),
                           (class="type">float)iTickVolume("EURUSD",PERIOD_MN1,class="num">0),
                           (class="type">float)visa_data[class="num">0],
                           (class="type">float)visa_data[class="num">1],
                           (class="type">float)visa_data[class="num">2]
                         };
class=class="str">"cmt">//--- Scale the data
   for(class="type">int i =class="num">0; i < class="num">8;i++)
    {
      input_data[i] = (class="type">float)((input_data[i] - mean_values[i])/std_values[i]);
    }
class=class="str">"cmt">//--- Show the class="kw">input data
   Print("Input data: ",input_data);

◍ 把 ONNX 预测接到开仓逻辑上

EA 的生命周期从 OnInit 开始:先 load_onnx_model() 载入模型文件,失败直接返回 INIT_FAILED;接着 read_visa_data() 读 VISA 特征、load_scaling_values() 读归一化参数,全部就绪才返回 INIT_SUCCEEDED。 OnDeinit 里只做两件事:OnnxRelease(onnx_model) 释放模型资源,ExpertRemove() 退出 EA。不手动释放的话,反复加载策略可能拖慢终端。 OnTick 是真正跑信号的地方。每次 tick 先 model_predict() 拿预测值,用 Comment 把 model_forecast[0] 打到图表左上角,方便你肉眼对照。 当 PositionsTotal()==0 无持仓时,若预测值低于当前收盘价就 Trade.Sell 做空、高于就 Trade.Buy 做多,并把 state 标记为 1 或 2。外汇与贵金属杠杆高,这套信号仅作概率参考,实盘前务必在 MT5 策略测试器用历史数据验证。

MQL5 / C++
class=class="str">"cmt">//--- Obtain a forecast
  OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT|ONNX_DEFAULT,input_data,model_forecast);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Load the ONNX file
   if(!load_onnx_model())
     {
     class=class="str">"cmt">//--- We failed to load the ONNX model
      class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- Read the VISA data
   read_visa_data();
class=class="str">"cmt">//--- Load scaling values
   load_scaling_values();
class=class="str">"cmt">//--- We were successful
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- Free up the resources we don&class="macro">#x27;t need
   OnnxRelease(onnx_model);
   ExpertRemove();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Get a prediction from our model
   model_predict();
   Comment("Model forecast: ",model_forecast[class="num">0]);
class=class="str">"cmt">//--- Check if we have any positions
   if(PositionsTotal() == class="num">0)
     {
     class=class="str">"cmt">//--- Note that we have no trades open
      state = class="num">0;
     class=class="str">"cmt">//--- Find an entry and take note
      if(model_forecast[class="num">0] < iClose(_Symbol,PERIOD_CURRENT,class="num">0))
       {
        Trade.Sell(trading_volume,_Symbol,SymbolInfoDouble(_Symbol,SYMBOL_BID),class="num">0,class="num">0,"Gain an Edge VISA");
        state = class="num">1;
       }
      if(model_forecast[class="num">0] > iClose(_Symbol,PERIOD_CURRENT,class="num">0))
       {
        Trade.Buy(trading_volume,_Symbol,SymbolInfoDouble(_Symbol,SYMBOL_ASK),class="num">0,class="num">0,"Gain an Edge VISA");
        state = class="num">2;
       }
     }
class=class="str">"cmt">//--- If we have positions open, check for reversals
   if(PositionsTotal() > class="num">0)
     {
      if(((state == class="num">1) && (model_forecast[class="num">0] > iClose(_Symbol,PERIOD_CURRENT,class="num">0))) ||

状态2下的反转平仓触发

当模型内部状态标记为 2 且实时预测值低于当前周期收盘价时,系统判定为潜在反转信号。此时会弹出警报并立即对当前品种平仓,属于硬止损之外的模型驱动退出。 这段代码是前述状态机逻辑的收口部分:state==2 代表此前已处于某种趋势衰竭观测态,model_forecast[0] 为模型对最新价的预测输出。若预测值小于 iClose(_Symbol,PERIOD_CURRENT,0) 实际收盘价,说明价格已背离模型预期,反转概率上升。 外汇与贵金属市场跳空频繁,这类基于模型预测的下单退出机制在高波动时段可能滑点放大,实盘前务必在 MT5 策略测试器用历史数据验证触发频率与回撤表现。

MQL5 / C++
      ((state == class="num">2) && (model_forecast[class="num">0] < iClose(_Symbol,PERIOD_CURRENT,class="num">0))))
      {
       Alert("Reversal detected, closing positions now");
       Trade.PositionClose(_Symbol);
      }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「把这条线请下神坛」

前面几节把外部数据的筛选、信息量度量和过拟合控制拆开讲过,落到实盘前得泼盆冷水:没有哪套数据集能永久给你优势。附带工程包里 VISA_EA.mq5 仅 8.45 KB,EURUSD_VISA_MN1_FLOAT.onnx 模型 2.79 KB,说明即便跑通了月度浮点模型,部署成本也极低,但低门槛不等于高胜率。 外汇与贵金属自带高杠杆和高波动风险,任何从历史数据提炼的「边缘」都可能随 regime 切换消失。打开 MT5 把 ZIP 里的 EA 和 ONNX 拖进策略测试器,用 2020 年后的行情重跑,比盲信作者结论更有用。 真要延续这条研究线,下一步该是自己换一组宏观序列重训模型,而不是守着别人的 notebook 等更新。行情不会重复,能复用的只有验证动作本身。

把数据拉取交给小布盯盘
FRED API密钥管理、VISA SMI序列的定时抓取与对齐,这些重复劳动小布盯盘的AIGC已内置,打开对应品种页即可看到预处理好的替代数据层,你只管调模型和看信号。

常见问题

SMI由VISA用自有卡网络匿名汇总美国境内借记与信用卡支出,更贴近实时消费行为;传统零售销售多为普查或抽样滞后发布,颗粒度与频次都不同。
原文用随机搜索1000次迭代调5个参数并做验证集监控,倾向能压住过拟合,但样本极少时结论本就脆弱,准确率可能随月份漂移。
个人注册可拿免费密钥调公开序列如VISA SMI;调用频次有限额,批量历史回填建议在低峰期分次请求。
小布盯盘内置了替代数据接入与品种页诊断,可展示VISA层与价量因子的相关性概览,但模型训练仍需你在MT5端用MQL5实现,它不替你写网络权重。
不一定。本系列价值在帮你用最低成本排除无效数据;省下的调研时间本身就是优势,外汇贵金属高风险,少做无用功也是风控。