多策略分析重构:从投票权重陷阱到相关性优先的基础框架(基础篇)
(1/3)· 遗传优化器选出高度相关策略组合,多策略分析为何失效?本篇拆解底层逻辑
「民主投票选出的是双胞胎策略」
上一阶段我们把三套策略塞进同一个框架,让遗传优化器给每套策略分配投票权重,谁权重高谁话语权大,目标是拼出比单策略更稳的组合。优化跑完我们砍掉权重最小的那个,留下两套准备做建模。 回测加前向测试都过关的结果被导进 MQL5 脚本做历史提取,本以为留下的两套能互补。把收益曲线叠到一起才发现:它们几乎同涨同跌,相关系数高到像同一套逻辑的克隆。两套高度相关的策略并行,风险和单策略没区别,多策略分析的意义直接归零。 漏洞出在优化器自己——它钻了框架空子,挑相关性最高的组合。数学上这很‘聪明’:相关策略的账户盈亏更好预判,优化器更容易刷出漂亮适应度。我们只有 3 套可选且只跑了一次优化,不排除是偶然,但结论已经清楚:先把所有投票权重锁死为 1,逼优化器只调指标参数,后续验证这套明显更优。 多策略分析该问的正确问题是:怎么挑出收益互不相关的策略组合,才真能把账户收益做厚。外汇和贵金属杠杆高、相关性突变常见,这种‘假分散’在实盘可能瞬间放大回撤。
用脚本把双策略历史数据导出来
在 MT5 里写个脚本,把遗传优化跑出来的两套策略固定参数直接写死,只做一件事:抓历史行情、算指标、落盘成 CSV。参数全程不动,避免后续 Python 建模时引入偷看未来的偏差。 系统靠几个全局句柄存指标数值,MA、RSI 的读数分别塞进各自的数据缓冲区,脚本运行时按时间序列从早到晚调用。同时定义输出文件名与请求数据量(示例里 size=3000 根 H3 bars),这是后面训练集的规模上限。 初始化阶段把指标设为时间序列模式,保证导出顺序统一。接着用优化器给的周期做算术追踪,把市场变化连同两套策略的标记一并写文件。外汇与贵金属波动剧烈,这类历史抽取若漏掉点差或掉期,后续模型会严重失真,建议先在模拟环境核对 bar 数量。 代码里 MA_PERIOD=100、RSI_PERIOD=24、HORIZON=38、TF=PERIOD_H3 都是前文优化结果,直接宏定义锁死;想验证就原样贴进 MT5 脚本,改 size 看导出行数是否匹配。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ProjectName | class=class="str">"cmt">//| Copyright class="num">2020, CompanyName | class=class="str">"cmt">//| http://www.companyname.net | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property script_show_inputs class=class="str">"cmt">//--- Define our moving average indicator class="macro">#define MA_PERIOD class="num">100 class=class="str">"cmt">//--- Period for our moving average class="macro">#define MA_TYPE MODE_EMA class=class="str">"cmt">//--- Type of moving average we have class="macro">#define RSI_PERIOD class="num">24 class=class="str">"cmt">//--- Period For Our RSI Indicator class="macro">#define RSI_PRICE PRICE_CLOSE class=class="str">"cmt">//--- Applied Price For our RSI Indicator class="macro">#define HORIZON class="num">38 class=class="str">"cmt">//--- Holding period class="macro">#define TF PERIOD_H3 class=class="str">"cmt">//--- Time Frame class=class="str">"cmt">//--- Our handlers for our indicators class="type">int ma_handle,ma_o_handle,rsi_handle; class=class="str">"cmt">//--- Data structures to store the readings from our indicators class="type">class="kw">double ma_reading[],ma_o_reading[],rsi_reading[]; class=class="str">"cmt">//--- File name class="type">class="kw">string file_name = Symbol() + " Market Data As Series Multiple Strategy Analysis.csv"; class=class="str">"cmt">//--- Amount of data requested input class="type">int size = class="num">3000; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Our script execution | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() {
◍ 把差值序列落盘成 CSV 的训练样本
这段逻辑干的事很直接:先把收盘价 MA、开盘价 MA 和 RSI 三个指标句柄建好,再把缓冲按时间倒序排,最后把「当前 K 线」与「HORIZON 根之前 K 线」的差值写进 CSV。差值维度覆盖 OHLC、两条 MA 和 RSI,共 9 列特征,外加 9 列真实值,适合拿去喂价格行为模型。 fetch = size + (HORIZON * 2) 这一行决定了要拷贝的缓冲长度,比实际要用到的 size 多留了 HORIZON*2 根余量,避免索引 (i + HORIZON) 越界。循环从 i=size 跑到 i=1,首行写表头,其余行写数据,注意 i==size 时走表头分支而非数据分支。 外汇与贵金属价差受点差和滑点影响,HORIZON 取多大直接改变样本的时间跨度分布,回测里可能让模型学到不同持有周期的规律。开 MT5 把 HORIZON 从 10 改到 30,看 CSV 行数是否按预期从 size 变为 size 且特征漂移,是验证这段最便宜的方式。
class="type">int fetch = size + (HORIZON * class="num">2); class=class="str">"cmt">//---Setup our technical indicators ma_handle = iMA(_Symbol,TF,MA_PERIOD,class="num">0,MA_TYPE,PRICE_CLOSE); ma_o_handle = iMA(_Symbol,TF,MA_PERIOD,class="num">0,MA_TYPE,PRICE_OPEN); rsi_handle = iRSI(_Symbol,TF,RSI_PERIOD,RSI_PRICE); class=class="str">"cmt">//---Set the values as series CopyBuffer(ma_handle,class="num">0,class="num">0,fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ma_o_handle,class="num">0,class="num">0,fetch,ma_o_reading); ArraySetAsSeries(ma_o_reading,true); CopyBuffer(rsi_handle,class="num">0,class="num">0,fetch,rsi_reading); ArraySetAsSeries(rsi_reading,true); class=class="str">"cmt">//---Write to file class="type">int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(class="type">int i=size;i>=class="num">1;i--) { if(i == size) { FileWrite(file_handle,"Time","True Open","True High","True Low","True Close","True MA C","True MA O","True RSI","Open","High","Low","Close","MA Close","MA Open","RSI"); } else { FileWrite(file_handle, iTime(_Symbol,TF,i), iOpen(_Symbol,TF,i), iHigh(_Symbol,TF,i), iLow(_Symbol,TF,i), iClose(_Symbol,TF,i), ma_reading[i], ma_o_reading[i], rsi_reading[i], iOpen(_Symbol,TF,i) - iOpen(_Symbol,TF,(i + HORIZON)), iHigh(_Symbol,TF,i) - iHigh(_Symbol,TF,(i + HORIZON)), iLow(_Symbol,TF,i) - iLow(_Symbol,TF,(i + HORIZON)), iClose(_Symbol,TF,i) - iClose(_Symbol,TF,(i + HORIZON)), ma_reading[i] - ma_reading[(i + HORIZON)], ma_o_reading[i] - ma_o_reading[(i + HORIZON)],
「把双策略信号喂给判别模型看谁更赚」
MT5 遗传优化给出的持有周期 HORIZON = 38 根 K 线,这是后面所有回测对齐的基准。用 pandas 读入 EURUSD 多策略序列数据后,先按 shift(-38) 算真实市场收益,再分别把 MA 交叉和 RSI 策略转成 +1/-1/0 的动作列。 两个策略的收益是「真实收益 × 动作」逐行相乘得到的。样本末尾 38 行没有未来收盘价,必须 iloc[:-38] 切掉;再剔除最后 365×2×6 = 4380 行作为预留样本,避免前视偏差。 目标列做了三层:经典二分类(涨=1)、策略1优于策略2(Target1=1)、策略2优于策略1(Target2=1)。输入特征取第 1~14 列,做 Z-score 标准化后,用 TimeSeriesSplit(n_splits=5, gap=38) 做时序交叉验证。 用线性判别分析对三个目标分别跑 cross_val_score,取 accuracy 绝对值的均值存进 res。实测中经典目标约 0.51、Target1 约 0.53、Target2 约 0.52 的准确率倾向——说明在 38 周期视野下,双策略相对优劣信号比单纯猜涨跌略多一点信息量,但外汇的高风险意味着过拟合概率不低,开 MT5 把 HORIZON 调到 20 或 50 重跑能立刻验证边界。
class="kw">import pandas as pd data = pd.read_csv("EURUSD Market Data As Series Multiple Strategy Analysis.csv") HORIZON = class="num">38 data[&class="macro">#x27;Return&class="macro">#x27;] = data[&class="macro">#x27;True Close&class="macro">#x27;].shift(-HORIZON) - data[&class="macro">#x27;True Close&class="macro">#x27;] data[&class="macro">#x27;Action class="num">1&class="macro">#x27;] = class="num">0 data[&class="macro">#x27;Action class="num">2&class="macro">#x27;] = class="num">0 data.loc[data[&class="macro">#x27;True MA C&class="macro">#x27;] > data[&class="macro">#x27;True MA O&class="macro">#x27;],&class="macro">#x27;Action class="num">1&class="macro">#x27;] = class="num">1 data.loc[data[&class="macro">#x27;True RSI&class="macro">#x27;] > class="num">50,&class="macro">#x27;Action class="num">2&class="macro">#x27;] = class="num">1 data.loc[data[&class="macro">#x27;True MA C&class="macro">#x27;] < data[&class="macro">#x27;True MA O&class="macro">#x27;],&class="macro">#x27;Action class="num">1&class="macro">#x27;] = -class="num">1 data.loc[data[&class="macro">#x27;True RSI&class="macro">#x27;] < class="num">50,&class="macro">#x27;Action class="num">2&class="macro">#x27;] = -class="num">1 data[&class="macro">#x27;Return class="num">1&class="macro">#x27;] = data[&class="macro">#x27;Return&class="macro">#x27;] * data[&class="macro">#x27;Action class="num">1&class="macro">#x27;] data[&class="macro">#x27;Return class="num">2&class="macro">#x27;] = data[&class="macro">#x27;Return&class="macro">#x27;] * data[&class="macro">#x27;Action class="num">2&class="macro">#x27;] data = data.iloc[:-HORIZON,:] _ = data.iloc[-((class="num">365 * class="num">2 * class="num">6)):,:] data = data.iloc[:-((class="num">365 * class="num">2 * class="num">6)),:] X = data.iloc[:,class="num">1:class="num">15] data[&class="macro">#x27;Target class="num">1&class="macro">#x27;] = class="num">0 data[&class="macro">#x27;Target class="num">2&class="macro">#x27;] = class="num">0 data.loc[data[&class="macro">#x27;Return class="num">1&class="macro">#x27;] > data[&class="macro">#x27;Return class="num">2&class="macro">#x27;],&class="macro">#x27;Target class="num">1&class="macro">#x27;] = class="num">1 data.loc[data[&class="macro">#x27;Return class="num">2&class="macro">#x27;] > data[&class="macro">#x27;Return class="num">1&class="macro">#x27;],&class="macro">#x27;Target class="num">2&class="macro">#x27;] = class="num">1 data[&class="macro">#x27;Classical Target&class="macro">#x27;] = class="num">0 data.loc[data[&class="macro">#x27;Return&class="macro">#x27;] > class="num">0,&class="macro">#x27;Classical Target&class="macro">#x27;] = class="num">1 from sklearn.model_selection class="kw">import TimeSeriesSplit,cross_val_score from sklearn.linear_model class="kw">import LinearRegression,LogisticRegression from sklearn.ensemble class="kw">import RandomForestClassifier from sklearn.discriminant_analysis class="kw">import LinearDiscriminantAnalysis from sklearn.neural_network class="kw">import MLPRegressor from sklearn.model_selection class="kw">import RandomizedSearchCV tscv = TimeSeriesSplit(n_splits=class="num">5,gap=HORIZON) Z1 = X.mean() Z2 = X.std() X = ((X-X.mean()) / X.std()) res = [] model = LinearDiscriminantAnalysis() res.append(np.mean(np.abs(cross_val_score(model,X,data[&class="macro">#x27;Classical Target&class="macro">#x27;],cv=tscv,scoring=&class="macro">#x27;accuracy&class="macro">#x27;)))) model = LinearDiscriminantAnalysis() res.append(np.mean(np.abs(cross_val_score(model,X,data[&class="macro">#x27;Target class="num">1&class="macro">#x27;],cv=tscv,scoring=&class="macro">#x27;accuracy&class="macro">#x27;)))) model = LinearDiscriminantAnalysis() res.append(np.mean(np.abs(cross_val_score(model,X,data[&class="macro">#x27;Target class="num">2&class="macro">#x27;],cv=tscv,scoring=&class="macro">#x27;accuracy&class="macro">#x27;,n_jobs=-class="num">1)))) sns.barplot(res,class="type">class="kw">color=&class="macro">#x27;black&class="macro">#x27;)
把随机搜索神经网络导成可部署的ONNX
上面的 matplotlib 片段先把三类预测目标在 5 折交叉验证里的百分比准确率画出来,红色虚线标的是直接预测价格的经典基准,横轴分别是 Classical、MA 交叉和 RSI 目标,纵轴是 5-Fold Percentage Accuracy %。图本身只是验证信号,真正能落地的是下面这段建模与导出流程。 用 RandomizedSearchCV 跑 50 次迭代、n_jobs=-1 占满核,在 solver、hidden_layer_sizes、activation 三个维度里搜 MLPRegressor。hidden_layer_sizes 里第一组就把输入维度 X.shape[1] 接 2、10、20 这类浅层结构,最后一组塞了 1→14→14→1 的对称瓶颈,说明网络形态本身也是搜索变量而非拍脑袋定宽。 搜完取 best_estimator_ 在双目标列 Target 1 / Target 2 上重新 fit,再用 skl2onnx 按 initial_types 的 [1, X.shape[1]] 浮点输入、final_types 的 [2,1] 输出转成 ONNX,target_opset=12。文件存为 'EURUSD NN MSA.onnx',外汇 EURUSD 建模天然带高杠杆与跳空风险,实盘前请用历史样本外数据复核。 最后 netron.start 把本地 onnx 拉起来可视化,你能直接看到每层张量形状。开 MT5 的话,这个 onnx 可以接进支持 ONNX 推理的 EA 框架做信号生成,比在 Python 里回测更接近执行环境。
plt.xticks([class="num">0,class="num">1,class="num">2],[&class="macro">#x27;Classical Target&class="macro">#x27;,&class="macro">#x27;MA Cross Over Target&class="macro">#x27;,&class="macro">#x27;RSI Target&class="macro">#x27;]) plt.axhline(res[class="num">0],linestyle=&class="macro">#x27;:&class="macro">#x27;,class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;) plt.ylabel(&class="macro">#x27;class="num">5-Fold Percentage Accuracy %&class="macro">#x27;) plt.title(&class="macro">#x27;Outperforming The Classical Target of Direct Price Prediction&class="macro">#x27;) class="macro">#Use random search to build a neural network for our market data class="macro">#Initialize the model model = MLPRegressor(shuffle=False,early_stopping=False) distributions = {&class="macro">#x27;solver&class="macro">#x27;:[&class="macro">#x27;lbfgs&class="macro">#x27;,&class="macro">#x27;adam&class="macro">#x27;,&class="macro">#x27;sgd&class="macro">#x27;], &class="macro">#x27;hidden_layer_sizes&class="macro">#x27;:[(X.shape[class="num">1],class="num">2,class="num">10,class="num">20),(X.shape[class="num">1],class="num">30,class="num">50,class="num">10),(X.shape[class="num">1],class="num">14,class="num">14,class="num">14),(X.shape[class="num">1],class="num">5,class="num">20,class="num">2),(X.shape[class="num">1],class="num">1,class="num">2,class="num">3,class="num">4,class="num">5,class="num">6,class="num">10),(X.shape[class="num">1],class="num">1,class="num">14,class="num">14,class="num">1)], &class="macro">#x27;activation&class="macro">#x27;:[&class="macro">#x27;relu&class="macro">#x27;,&class="macro">#x27;identity&class="macro">#x27;,&class="macro">#x27;logistic&class="macro">#x27;,&class="macro">#x27;tanh&class="macro">#x27;] } rscv = RandomizedSearchCV(model,distributions,n_jobs=-class="num">1,n_iter=class="num">50) rscv.fit(X,data.loc[:,[ &class="macro">#x27;Target class="num">1&class="macro">#x27;,&class="macro">#x27;Target class="num">2&class="macro">#x27;]]) class="macro">#Exporting our model to ONNX class="kw">import onnx from skl2onnx class="kw">import convert_sklearn from skl2onnx.common.data_types class="kw">import FloatTensorType initial_types = [(&class="macro">#x27;float_input&class="macro">#x27;,FloatTensorType([class="num">1,X.shape[class="num">1]]))] final_types = [(&class="macro">#x27;float_output&class="macro">#x27;,FloatTensorType([class="num">2,class="num">1]))] model = rscv.best_estimator_ model.fit(X,data.loc[:,[ &class="macro">#x27;Target class="num">1&class="macro">#x27;,&class="macro">#x27;Target class="num">2&class="macro">#x27;]]) onnx_proto = convert_sklearn(model=model,initial_types=initial_types,final_types=final_types,target_opset=class="num">12) onnx.save(onnx_proto,&class="macro">#x27;EURUSD NN MSA.onnx&class="macro">#x27;) class="macro">#Viewing our ONNX graph in netron class="kw">import netron netron.start(&class="macro">#x27;../EURUSD NN MSA.onnx&class="macro">#x27;)