在任何市场中获得优势(第三部分):Visa消费指数·综合运用
(3/3)· 把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 至今数据回放一遍。
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 端挂止损,而不是指望隔天才平。
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预测仅作概率参考,实盘前请在策略测试器跑历史校验。
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 周期上跑起这套消费数据驱动的信号。
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}(单值预测)。OnnxSetInputShape 和 OnnxSetOutputShape 任一返回 false 都得中止,否则推理时会按默认形状错位读取,结果不可信。外汇与贵金属杠杆高,模型推理仅是辅助信号,实盘前请在策略测试器用历史数据验证输出分布。
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_READ | FILE_CSV | FILE_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_READ | FILE_CSV | FILE_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 结构的。
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,说明那只特征没归一好或均值标准差过期。外汇与贵金属杠杆高、滑点大,模型输出只代表概率倾向,不能直接当下单指令。
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 策略测试器用历史数据验证。
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 策略测试器用历史数据验证触发频率与回撤表现。
((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 等更新。行情不会重复,能复用的只有验证动作本身。