使用Python和MQL5进行多品种分析(第三部分):三角汇率·综合运用
「银价跨周期比值与对数特征的提取写法」
在 MT5 里做 XAGUSD 的特征工程,常见做法是把当前 K 线与上一根 K 线的 OHLC 做比值,再补一层对数变换,用来弱化价格量纲、突出相对波动。下面这段片段就在一根循环里连续塞了 14 个维度:4 个原始比值、3 个开盘价对前一根高/低/收的比值、1 个高低比值,以及 4 个对数比值,最后还加了正弦和余弦两个非线性项。 外汇与贵金属杠杆高、跳空频繁,这类比值在重大数据行情里可能因分母突变而失真,实盘前务必用历史回放核对。打开 MT5 的 MetaEditor,把符号写死为 "XAGUSD"、周期取 PERIOD_CURRENT,即可直接编译观察输出文件里的数值分布。 代码里 MathLog10 包裹的比值,和直接除法的比值并存,意味着同一组价格关系你拿到了线性与对数两种尺度;若后续接机器学习模型,这两种表达可能触发不同的特征重要性,值得手动比对。
(iOpen("XAGUSD",PERIOD_CURRENT,i) / iOpen("XAGUSD",PERIOD_CURRENT,i+class="num">1)), (iHigh("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+class="num">1)), (iLow("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+class="num">1)), (iClose("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+class="num">1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+class="num">1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+class="num">1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+class="num">1)), (iHigh("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+class="num">1)), MathLog10(iOpen("XAGUSD",PERIOD_CURRENT,i) / iOpen("XAGUSD",PERIOD_CURRENT,i+class="num">1)), MathLog10(iHigh("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+class="num">1)), MathLog10(iLow("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+class="num">1)), MathLog10(iClose("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+class="num">1)), (MathSin(iHigh("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i))), (MathCos(iOpen("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i))) ); } } class=class="str">"cmt">//--- Close the file FileClose(file_handle); } class=class="str">"cmt">//+------------------------------------------------------------------+
用Python切出无泄漏的训练集并导出三市场模型
做统计模型前先把数据泄漏堵死。我们跑的是H1周期,标注窗口取LOOK_AHEAD=24,也就是用未来24根小时线(约一个交易日)的收盘价减当前收盘价当Target。如果训练集混入了回测段(本例回测从2023-11-01起),模型等于提前看了答案,对外汇和贵金属这种高波动品种会给出虚假置信度。
代码里用 data.iloc[:-((24*365)-918),:] 把与回测重叠的尾部切掉,只留2023-10-31及之前的数据。这一步直接决定后面三个ONNX模型在MT5里是辅助决策还是制造噪音。
把XAGUSD和XAGEUR的增长曲线叠在一起看,红绿线长期不重合、会出现阶段性背离再修正,说明两个白银报价间存在概率性套利空间;若完全无套利,两线应从头到尾重叠。系统对三个模型同时看多的走势加权更高,倾向过滤掉单一市场假突破。
下面这段是可直接跑的构建脚本,核心是分别用四个OHLC字段拟合GradientBoostingRegressor再存成ONNX:
#Import libraries we need
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
#Clean up the data
LOOK_AHEAD = 24
data = pd.read_csv("../XAGEUR XAGUSD EURUSD Triangular Exchange Rates.csv")
data["Target"] = data["XAGUSD Close"].shift(-LOOK_AHEAD) - data["XAGUSD Close"]
data.dropna(inplace=True)
data.reset_index(inplace=True,drop=True)
#Drop the dates corresponding to our backtest
_ = data.iloc[-((24 * 365) - 918):,:]
#Keep the dates before our backtest
data = data.iloc[:-((24 * 365) - 918),:]
data
plt.title("Comparing XAGUSD & XAGEUR Growth")
plt.plot((data['XAGUSD Close'] / data.loc[0,"XAGUSD Close"]) / (data['XAGUSD Close'].max() - data['XAGUSD Close'].min()),color="red")
plt.plot((data['XAGEUR Close'] / data.loc[0,"XAGEUR Close"]) / (data['XAGEUR Close'].max() - data['XAGEUR Close'].min()),color="green")
plt.ylabel("Commodity Growth")
plt.xlabel("Time")
plt.legend(["XAGUSD","XAGEUR"])
plt.grid()
X = data.iloc[:,1:-1].columns
y = "Target"
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorTypea
from sklearn.ensemble import GradientBoostingRegressor
model = GradientBoostingRegressor()
model.fit(data.loc[:,[\"XAGUSD Open\",\"XAGUSD High\",\"XAGUSD Low\",\"XAGUSD Close\"]],data.loc[:,y])
initial_types = [("float_input",FloatTensorType([1,4]))]
xagusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)
onnx.save(xagusd_model_proto,"../XAGUSD State Model.onnx")
model = GradientBoostingRegressor()
model.fit(data.loc[:,[\"XAGEUR Open\",\"XAGEUR High\",\"XAGEUR Low\",\"XAGEUR Close\"]],data.loc[:,"XAGEUR Target"])
initial_types = [("float_input",FloatTensorType([1,4]))]
xageur_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)
onnx.save(xageur_model_proto,"../XAGEUR State Model.onnx")
model = GradientBoostingRegressor()
model.fit(data.loc[:,[\"EURUSD Open\",\"EURUSD High\",\"EURUSD Low\",\"EURUSD Close\"]],data.loc[:,"EURUSD Target"])
initial_types = [("float_input",FloatTensorType([1,4]))]
eurusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)
onnx.save(eurusd_model_proto,"../EURUSD State Model.onnx")
逐行拆一下关键行:LOOK_AHEAD=24 定义标注跨度;shift(-24) 把Target移到未来24根;dropna 清掉末尾无未来的行;切片 -((24*365)-918) 精确砍掉约918根(回测起始前移量)的重叠样本;三个GradientBoostingRegressor各吃自己市场的OHLC四列,输出ONNX供MT5调用。
开MT5前,先确认三个ONNX已落在EA同目录,否则三模型加权逻辑会退化成单模型信号,假突破过滤能力可能明显下降。
class="macro">#Import libraries we need class="kw">import pandas as pd class="kw">import numpy as np class="kw">import seaborn as sns class="kw">import matplotlib.pyplot as plt class="macro">#Clean up the data LOOK_AHEAD = class="num">24 data = pd.read_csv("../XAGEUR XAGUSD EURUSD Triangular Exchange Rates.csv") data["Target"] = data["XAGUSD Close"].shift(-LOOK_AHEAD) - data["XAGUSD Close"] data.dropna(inplace=True) data.reset_index(inplace=True,drop=True) class="macro">#Drop the dates corresponding to our backtest _ = data.iloc[-((class="num">24 * class="num">365) - class="num">918):,:] class="macro">#Keep the dates before our backtest data = data.iloc[:-((class="num">24 * class="num">365) - class="num">918),:] data plt.title("Comparing XAGUSD & XAGEUR Growth") plt.plot((data[&class="macro">#x27;XAGUSD Close&class="macro">#x27;] / data.loc[class="num">0,"XAGUSD Close"]) / (data[&class="macro">#x27;XAGUSD Close&class="macro">#x27;].max() - data[&class="macro">#x27;XAGUSD Close&class="macro">#x27;].min()),class="type">class="kw">color="red") plt.plot((data[&class="macro">#x27;XAGEUR Close&class="macro">#x27;] / data.loc[class="num">0,"XAGEUR Close"]) / (data[&class="macro">#x27;XAGEUR Close&class="macro">#x27;].max() - data[&class="macro">#x27;XAGEUR Close&class="macro">#x27;].min()),class="type">class="kw">color="green") plt.ylabel("Commodity Growth") plt.xlabel("Time") plt.legend(["XAGUSD","XAGEUR"]) plt.grid() X = data.iloc[:,class="num">1:-class="num">1].columns y = "Target" class="kw">import onnx from skl2onnx class="kw">import convert_sklearn from skl2onnx.common.data_types class="kw">import FloatTensorTypea from sklearn.ensemble class="kw">import GradientBoostingRegressor model = GradientBoostingRegressor() model.fit(data.loc[:,["XAGUSD Open","XAGUSD High","XAGUSD Low","XAGUSD Close"]],data.loc[:,y]) initial_types = [("float_input",FloatTensorType([class="num">1,class="num">4]))] xagusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12) onnx.save(xagusd_model_proto,"../XAGUSD State Model.onnx") model = GradientBoostingRegressor() model.fit(data.loc[:,["XAGEUR Open","XAGEUR High","XAGEUR Low","XAGEUR Close"]],data.loc[:,"XAGEUR Target"]) initial_types = [("float_input",FloatTensorType([class="num">1,class="num">4]))] xageur_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12) onnx.save(xageur_model_proto,"../XAGEUR State Model.onnx") model = GradientBoostingRegressor() model.fit(data.loc[:,["EURUSD Open","EURUSD High","EURUSD Low","EURUSD Close"]],data.loc[:,"EURUSD Target"]) initial_types = [("float_input",FloatTensorType([class="num">1,class="num">4]))] eurusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12) onnx.save(eurusd_model_proto,"../EURUSD State Model.onnx")
◍ 把ONNX模型和双MA交叉塞进EA骨架
改进版策略的落地思路很直接:在原有EA里挂上ONNX模型做状态预测,再叠一套对开盘价和收盘价分别计价的同周期移动平均线交叉,用来给模型信号做二次确认。两条MA周期相同,本例宏定义为8;当开盘价MA处在收盘价MA上方,解读为偏空,反之偏多。 全局层要先声明模型缓冲与指标句柄。下面代码里用 #resource 把三个品种的ONNX文件映射进内存,并预留了XAGUSD的双MA句柄与输入输出向量。注意 sl_width 按 _Point 的300倍设,贵金属点值小,这个宽度只是示例参数,实盘需按品种波动重调。 #resource "\\Files\\XAGUSD State Model.onnx" as uchar xagusd_onnx_buffer[] 这行把白银模型读为字节数组;同理EURUSD、XAGEUR各一份。 vectorf model_output = vectorf::Zeros(1); 给模型单输出留了长度1的浮向量。 退出时要释放资源,否则MT5终端反复加载会漏内存。OnDeinit里依次 OnnxRelease 三个模型、IndicatorRelease 两个MA句柄,并打印注销信息,这是多品种EA的基本洁癖。 初始化函数 setup() 里用 SymbolSelect 把涉及品种加入市场观察,指标与模型若加载失败应中断并报错——回测和实盘都依赖这一步,跳过验证可能拿到静默假信号。 确认逻辑是:先从模型取预测方向,再比MA交叉方向,两者一致才视为高概率机会并加倍手数,否则保守手数。白银回测中,原版夏普0.14、改进版1.85;平均每笔亏损115→109美元,盈利188→213美元,总盈利395→1449美元,且交易次数更少。外汇与贵金属杠杆高,这类回测不预示实盘,需用非训练时段数据在MT5自测验证。
class="macro">#define XAGUSD_MA_PERIOD class="num">8 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| System resources | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\Files\XAGUSD State Model.onnx" as class="type">uchar xagusd_onnx_buffer[] class="macro">#resource "\Files\XAGEUR State Model.onnx" as class="type">uchar xageur_onnx_buffer[] class="macro">#resource "\Files\EURUSD State Model.onnx" as class="type">uchar eurusd_onnx_buffer[] class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+------------------------------------------------------------------+ vector eurusd,xagusd,xageur; class="type">class="kw">double eurusd_growth,xagusd_growth,xageur_growth,bid,ask; class="type">class="kw">double sl_width = class="num">3e2 * _Point; class="type">int xagusd_f_ma_handler,xagusd_s_ma_handler; class="type">class="kw">double xagusd_f[],xagusd_s[]; vectorf model_output = vectorf::Zeros(class="num">1); class="type">long onnx_model; vectorf xageur_model_output = vectorf::Zeros(class="num">1); class="type">long xageur_onnx_model; vectorf eurusd_model_output = vectorf::Zeros(class="num">1); class="type">long eurusd_onnx_model; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- OnnxRelease(onnx_model); OnnxRelease(xageur_onnx_model); OnnxRelease(eurusd_onnx_model); IndicatorRelease(xagusd_f_ma_handler); IndicatorRelease(xagusd_s_ma_handler); Print("System deinitialized"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Setup our technical indicators and select the symbols we need | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool setup(class="type">void) { class=class="str">"cmt">//--- Select the symbols we need SymbolSelect(SYMBOL_ONE,true); SymbolSelect(SYMBOL_TWO,true);
「把银价双均线塞进 ONNX 前的句柄校验」
多品种套利类 EA 在初始化阶段最怕静默失败。下面这段代码先把 XAGUSD 的第三符号拉进市场观察,再挂两条同周期同长度的 SMA——一条吃开盘价、一条吃收盘价,用来捕捉日内微结构偏差。 xagusd_f_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,0,MODE_SMA,PRICE_OPEN); xagusd_s_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,0,MODE_SMA,PRICE_CLOSE); 任意一条返回 INVALID_HANDLE 就直接 Comment 报错并 return(false),避免后面拿空句柄去跑推理把账户拖崩。 三个 ONNX 模型(XAGUSD / XAGEUR / EURUSD)都用 OnnxCreateFromBuffer 从内存 buffer 载入,ONNX_DEFAULT 走默认执行后端。只要有一个模型句柄非法,立刻在图表上打出 GetLastError() 具体码并终止初始化——外汇与贵金属杠杆高,模型没加载全就开跑,信号错位概率显著放大。 输入张量固定成 {1,4}、输出 {1,1},分别对应 4 维特征进、1 维预测出。OnnxSetInputShape / OnnxSetOutputShape 任一步返回 false 都要拦住,否则推理时维度对不上会直接抛错。开 MT5 把这段贴进 OnInit,故意改坏一个 buffer 名,能看到图表左上角精确报出是哪一对货币对模型建失败。
SymbolSelect(SYMBOL_THREE,true); class=class="str">"cmt">//--- Setup the moving averages xagusd_f_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,class="num">0,MODE_SMA,PRICE_OPEN); xagusd_s_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,class="num">0,MODE_SMA,PRICE_CLOSE); if((xagusd_f_ma_handler == INVALID_HANDLE) || (xagusd_s_ma_handler == INVALID_HANDLE)) { Comment("Failed to load our technical indicators correctly. ", GetLastError()); class="kw">return(false); } class=class="str">"cmt">//--- Setup our statistical models onnx_model = OnnxCreateFromBuffer(xagusd_onnx_buffer,ONNX_DEFAULT); xageur_onnx_model = OnnxCreateFromBuffer(xageur_onnx_buffer,ONNX_DEFAULT); eurusd_onnx_model = OnnxCreateFromBuffer(eurusd_onnx_buffer,ONNX_DEFAULT); if(onnx_model == INVALID_HANDLE) { Comment("Failed to create our XAGUSD ONNX model correctly. ",GetLastError()); class="kw">return(false); } if(xageur_onnx_model == INVALID_HANDLE) { Comment("Failed to create our XAGEUR ONNX model correctly. ",GetLastError()); class="kw">return(false); } if(eurusd_onnx_model == INVALID_HANDLE) { Comment("Failed to create our EURUSD ONNX model correctly. ",GetLastError()); class="kw">return(false); } class="type">ulong input_shape[] = {class="num">1,class="num">4}; class="type">ulong output_shape[] = {class="num">1,class="num">1}; if(!(OnnxSetInputShape(onnx_model,class="num">0,input_shape))) { Comment("Failed to specify XAGUSD model input shape. ",GetLastError()); class="kw">return(false); } if(!(OnnxSetInputShape(xageur_onnx_model,class="num">0,input_shape))) { Comment("Failed to specify XAGEUR model input shape. ",GetLastError()); class="kw">return(false); } if(!(OnnxSetInputShape(eurusd_onnx_model,class="num">0,input_shape))) { Comment("Failed to specify EURUSD model input shape. ",GetLastError()); class="kw">return(false); } if(!(OnnxSetOutputShape(onnx_model,class="num">0,output_shape))) { Comment("Failed to specify XAGUSD model output shape. ",GetLastError()); class="kw">return(false); } if(!(OnnxSetOutputShape(xageur_onnx_model,class="num">0,output_shape))) { Comment("Failed to specify XAGEUR model output shape. ",GetLastError()); class="kw">return(false); }
用 ONNX 预测结果驱动双档仓位
这段逻辑把跨品种增长率过滤和 ONNX 模型输出绑在一起,决定下多大手数。EURUSD、XAGEUR、XAGUSD 三者增长率需同时满足方向门槛(如 eu_growth<1 且 xaueur_growth>1 且 xagusd_growth<1 才考虑卖),模型输出符号再决定是保守 1 倍 VOLUME 还是激进 2 倍 VOLUME。 模型推理本身很轻:取前一根 1 周期 K 线的开盘与收盘转 float 塞进输入向量,调 OnnxRun 后把 model_output[0] 打印出来看符号。若输出小于 0 且银价现货低于期货,才触发加倍卖出;否则只做标准卖出。 外汇与贵金属杠杆高、跨品种相关性会断裂,这类多条件共振只是提高概率,不代表胜率保障。实盘前建议在 MT5 策略测试器里把 VOLUME 和 sl_width 调小,先跑 2023 年 EURUSD 的 M1 数据看 model_output 分布是否稳定。
if(!(OnnxSetOutputShape(eurusd_onnx_model,class="num">0,output_shape))) { Comment("Failed to specify EURUSD model output shape. ",GetLastError()); class="kw">return(false); } Print("System initialized succefully"); class=class="str">"cmt">//--- If we have gotten this far, everything went fine. class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Fetch a prediction from our model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void model_predict(class="type">void) { vectorf model_inputs = { (class="type">class="kw">float) iOpen(SYMBOL_ONE,TF_1,class="num">1), (class="type">class="kw">float) iClose(SYMBOL_ONE,TF_1,class="num">1)}; OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output); Print(StringFormat("Model forecast: %d",model_output)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Find setup | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void find_setup(class="type">void) { model_predict(); class=class="str">"cmt">//--- Check if the current market setup matches our expectations for selling if((eurusd_growth < class="num">1) && (xageur_growth > class="num">1) && (xagusd_growth < class="num">1)) { if(xagusd_s[class="num">0] < xagusd_f[class="num">0]) { if(model_output[class="num">0] < class="num">0) { class=class="str">"cmt">//--- If all our systems align, we have a high probability trade setup Trade.Sell(VOLUME * class="num">2,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } class=class="str">"cmt">//--- Otherwise, we should trade conservatively Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } } class=class="str">"cmt">//--- Check if the current market setup matches our expectations for buying if((eurusd_growth > class="num">1) && (xageur_growth < class="num">1) && (xagusd_growth > class="num">1)) { if(xagusd_s[class="num">0] > xagusd_f[class="num">0]) { if(model_output[class="num">0] > class="num">0) { Trade.Buy(VOLUME * class="num">2,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } } }
◍ 别急着下结论
这套三角套利思路真正落地的关键,是把领域认知塞进统计模型里,而不是只跑别人给的 EA。基准版 Baseline_Model.mq5 仅 7.05 KB,第二版膨胀到 12.44 KB,多出来的 5 KB 左右基本是过滤与执行逻辑的加固,实盘前建议先 diff 这两份源码。 欧元兑美元、白银兑欧元、白银兑美元三个状态模型以 ONNX 格式给出,体积分别在 53.38 KB、53.3 KB、52.31 KB,误差级差异说明输入特征尺度接近;Triangular_Exchange_Rates.ipynb 有 109.79 KB,里面是构造市场强度的 Jupyter 流程,可本地重跑验证相关性假设。 外汇与贵金属杠杆高、滑点凶,这类跨市场策略在流动性裂口时可能瞬间失效。先开 MT5 把 ZIP 里的 mq5 拖进策略测试器,用历史数据跑一遍再谈参数微调。