重构经典策略:原油·进阶篇
(2/3)·经典价差策略在算法盘里失灵?本篇用机器学习重构WTI-布伦特价差预测模型
「EA 骨架与回测前的参数约定」
把跨市场回归 EA 搭起来,第一步是引入交易与尾随止损库,再用四个输入参数框住风险与仓位逻辑:lot_multiple 决定每笔手数是最小交易量的几倍,profit_target 设统一平仓盈利点,max_loss 控账户总回撤上限,position_size 规定同方向开仓数量。这些量直接决定你在 MT5 里跑这个 EA 时的暴露程度,外汇与贵金属类品种杠杆高,回撤触发后可能迅速离场。 为了在两个标的间对齐历史,代码里用 consumption=0.01 只取 1% 可用 K 线,再分别算 UK Brent Oil 与 WTI_OIL 的 iBars 乘该比例,取较小值赋给 max_bars。look_ahead 被设为 max_bars/4,也就是用四分之一的柱数做前瞻预测步长,剩下部分用于拟合。 模型侧,A 是 fetch×6 的零矩阵存输入(含价差与截距),y 是 fetch×1 的输出列(布伦特收盘价),wti_price 是对应长度的向量。OnInit 只查布伦特最小交易量,OnTick 里先刷新买卖价,未训练就拟合、已训练就查持仓并按预测方向开仓或检查止盈止损。 回测可直接用 MT5 策略测试器加载,图例显示历史收益率曲线来自该 EA 对布伦特与 WTI 价差回归后的信号。注意这仅是历史样本表现,未来同向价差结构可能断裂,实盘前务必用更小 consumption 验证过拟合程度。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Brent EA.mq5 |</span> <span class="comment">class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. |</span> <span class="comment">class=class="str">"cmt">//| [MQL5官方文档] |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2024, MetaQuotes Ltd."</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档] <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span> <span class="class="type">class="kw">string">"class="num">1.00"</span> <span class="comment">class=class="str">"cmt">//Libraries</span> <span class="preprocessor">class="macro">#include </span><Trade\Trade.mqh> CTrade ExtTrade; <span class="preprocessor">class="macro">#include </span><TrailingStop\ATRTrailingStop3.mqh> ATRTrailingStop ExtATRTrailingStop; <span class="comment">class=class="str">"cmt">//Inputs</span> <span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> lot_multiple = <span class="number">class="num">1.0</span>; <span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> profit_target = <span class="number">class="num">10</span>; <span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> max_loss = <span class="number">class="num">20</span>; <span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> position_size = <span class="number">class="num">2</span>; <span class="comment">class=class="str">"cmt">//Set this value between class="num">0 and class="num">1 to control how much data is used</span> <span class="keyword">class="type">class="kw">double</span> consumption = <span class="number">class="num">0.01</span>; <span class="comment">class=class="str">"cmt">//We want to know which symbol has the least number of bars.</span> <span class="keyword">class="type">class="kw">double</span> brent_bars = (<span class="keyword">class="type">class="kw">double</span>) <span class="functions">NormalizeDouble</span>((<span class="functions">iBars</span>(<span class="class="type">class="kw">string">"UK Brent Oil"</span>,<span class="macro">PERIOD_CURRENT</span>) * consumption),<span class="number">class="num">0</span>); <span class="keyword">class="type">class="kw">double</span> wti_bars = (<span class="keyword">class="type">class="kw">double</span>) <span class="functions">NormalizeDouble</span>((<span class="functions">iBars</span>(<span class="class="type">class="kw">string">"WTI_OIL"</span>,<span class="macro">PERIOD_CURRENT</span>) * consumption),<span class="number">class="num">0</span>); <span class="comment">class=class="str">"cmt">//Select the lowest</span> <span class="keyword">class="type">class="kw">double</span> max_bars = (brent_bars < wti_bars) ? brent_bars : wti_bars; <span class="comment">class=class="str">"cmt">//How far into the future are we forecasting</span> <span class="keyword">class="type">class="kw">double</span> look_ahead = <span class="functions">NormalizeDouble</span>((max_bars / <span class="number">class="num">4</span>),<span class="number">class="num">0</span>); <span class="comment">class=class="str">"cmt">//How many bars should we fetch? </span> <span class="keyword">class="type">int</span> fetch = (<span class="keyword">class="type">int</span>) (max_bars - look_ahead) - <span class="number">class="num">1</span>; <span class="comment">class=class="str">"cmt">//Matrix A stores our inputs. y is the output. x is the coefficients.</span> <span class="keyword">matrix</span> A = <span class="keyword">matrix</span>::Zeros(fetch,<span class="number">class="num">6</span>); <span class="keyword">matrix</span> y = <span class="keyword">matrix</span>::Zeros(fetch,<span class="number">class="num">1</span>); <span class="keyword">vector</span> wti_price = <span class="keyword">vector</span>::Zeros(fetch);
◍ 布伦特模型下的持仓与下单骨架
这段逻辑把 UK Brent Oil 和 WTI_OIL 两个品种分开存字符串,用 vector 和 matrix 给回归预测留了占位:brent_price 长度由 fetch 决定,x 是 6 行 1 列的零矩阵,intercept 是全 1 向量。外汇与贵金属杠杆高,直接用真仓跑前务必在策略测试器里先验证。 OnInit 里只做两件事:atr_multiple 大于 0 时初始化 ATR trailing stop,再读 brent 品种的最小成交量 min_volume,返回 INIT_SUCCEEDED。 OnTick 是总调度:未初始化就调 InitializeModel,已初始化且无持仓就跑 ModelForecast 再 InterpretForecast;有持仓则进 ManageTrades。ManageTrades 只看账户浮盈是否突破 profit_target 或浮亏低于 -max_loss,触发就 CloseAll,属于硬止损止盈。 InterpretForecast 把 forecast 和当前 K 线收盘价比:高于就 check_buy,低于就 check_sell。check_buy 在空仓时按 position_size 次数循环,每次下 lot_multiple * min_volume 手多单,说明手数是最小单位的整数倍,避免券商拒单。
vector brent_price = vector::Zeros(fetch); vector spread; vector intercept = vector::Ones(fetch); matrix x = matrix::Zeros(class="num">6,class="num">1); class="type">class="kw">double forecast = class="num">0; class="type">class="kw">double ask = class="num">0; class="type">class="kw">double bid = class="num">0; class="type">class="kw">double min_volume = class="num">0; class="type">class="kw">string brent = "UK Brent Oil"; class="type">class="kw">string wti = "WTI_OIL"; class="type">bool model_initialized = false; class="type">int OnInit() { class=class="str">"cmt">//Initialise trailing stops if(atr_multiple > class="num">0) ExtATRTrailingStop.Init(atr_multiple); min_volume = SymbolInfoDouble(brent,SYMBOL_VOLUME_MIN); class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- ask = SymbolInfoDouble(brent,SYMBOL_ASK); bid = SymbolInfoDouble(brent,SYMBOL_BID); if(model_initialized) { if(PositionsTotal() == class="num">0) { forecast = class="num">0; forecast = ModelForecast(); InterpretForecast(); } else { ManageTrades(); } } else { model_initialized = InitializeModel(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ManageTrades() { if(AccountInfoDouble(ACCOUNT_PROFIT) > profit_target) CloseAll(); if(AccountInfoDouble(ACCOUNT_PROFIT) < (-class="num">1 * max_loss)) CloseAll(); } class="type">void InterpretForecast() { if(forecast != class="num">0) { if(forecast > iClose(_Symbol,PERIOD_CURRENT,class="num">0)) { check_buy(); } else if(forecast < iClose(_Symbol,PERIOD_CURRENT,class="num">0)) { check_sell(); } } } class="type">void check_buy() { if(PositionsTotal() == class="num">0) { for(class="type">int i = class="num">0; i < position_size; i++) { ExtTrade.Buy(lot_multiple * min_volume,brent,ask,class="num">0,class="num">0,"BUY"); } } } class="type">void check_sell() {
无持仓时按倍数铺空单与定向平仓
这段代码暴露了一个典型的网格式建仓逻辑:当账户里 Brent 相关持仓数为 0 时,循环 position_size 次,以 lot_multiple * min_volume 的手数在 bid 价挂空单。position_size 若设成 5、lot_multiple 设成 2,就意味着一口气打出 5 张加倍最小手的空单,原油这种高波动品种上杠杆暴露会很快放大,属于高风险操作。 CloseAll 与 close_buy / close_sell 三个函数把平仓拆得很细:CloseAll 只清 Brent 品种的全部持仓,close_buy 和 close_sell 则分别按 POSITION_TYPE_BUY / SELL 精准剔除某一方向。实盘里如果同时跑多品种 EA,这种按符号过滤的写法能避免误平其他品种。 InitializeModel 里用 SymbolSelect 确认 brent 和 wti 可见后,才通过 CopyRates 拉取收盘价序列,其中 y 取 1 到 fetch 根、A 和 brent_price 偏移 look_ahead 根起步。把 look_ahead 调大,模型输入就相对输出右移,回测里常用来模拟预测滞后,但实盘滑点可能让这种错位更明显。 别把平仓函数当保险丝 close_buy 只关多单、close_sell 只关空单,若你手动反手后忘了调函数,脚本可能只平掉一半仓位,留着反向单裸奔。
if(PositionsTotal() == class="num">0) { for(class="type">int i = class="num">0; i < position_size; i++) { ExtTrade.Sell(lot_multiple * min_volume,brent,bid,class="num">0,class="num">0,"SELL"); } } class="type">void CloseAll(class="type">void) { for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--) { if(PositionSelectByTicket(PositionGetTicket(i))) { if(PositionGetSymbol(i) == brent) { class="type">ulong ticket; ticket = PositionGetTicket(i); ExtTrade.PositionClose(ticket); } } } } class="type">void close_buy() { class="type">ulong ticket; class="type">int type; if(PositionsTotal() > class="num">0) { for(class="type">int i = class="num">0; i < PositionsTotal(); i++) { ticket = PositionGetTicket(i); type = (class="type">int)PositionGetInteger(POSITION_TYPE); if(type == POSITION_TYPE_BUY) { ExtTrade.PositionClose(ticket); } } } } class="type">void close_sell() { class="type">ulong ticket; class="type">int type; if(PositionsTotal() > class="num">0) { for(class="type">int i = class="num">0; i < PositionsTotal(); i++) { ticket = PositionGetTicket(i); type = (class="type">int)PositionGetInteger(POSITION_TYPE); if(type == POSITION_TYPE_SELL) { ExtTrade.PositionClose(ticket); } } } } class="type">bool InitializeModel() { class=class="str">"cmt">//Try select the symbols if(SymbolSelect(brent,true) && SymbolSelect(wti,true)) { Print("Symbols Available. Bars: ",max_bars," Fetch: ",fetch," Look ahead: ",look_ahead); class=class="str">"cmt">//Get historical data on Brent , our model output y.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,class="num">1,fetch); class=class="str">"cmt">//model class="kw">input A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(class="num">1 + look_ahead),fetch); brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(class="num">1+look_ahead),fetch);
「布伦特与WTI价差怎么喂进回归模型」
做跨品种套利或相关性分析时,Brent 对 WTI 的价差是最直接的输入特征。上面这段逻辑先把两个品种的收盘价按当前周期抓取,用 brent_price 减 wti_price 得到 spread,再打印出来方便在 MT5 终端实时盯。 矩阵 A 被重塑为 3 行 fetch 列:第 0 行放 Brent 收盘价,第 1 行塞 spread,第 2 行补一列 intercept(全 1 截距项)。转置后和标签 y 对齐,若 A 或 y 列数为 0 就说明历史拷贝失败,直接返回 false 并把维度打印出来排错。 拟合阶段走的是 x = A.PInv().MatMul(y),即伪逆乘以目标向量的最小二乘解,跑完把系数 x 打印出来。预测函数 ModelForecast 只抓最新 1 根 K 线,重算 spread 后用手算点积 (A[0][0]*x[0][0])+(A[1][0]*x[1][0])+(A[2][0]*x[2][0]) 得出单点预报值。外汇与贵金属类价差策略杠杆高、跳空频繁,回测吻合不代表实盘概率占优,开 MT5 用真实品种名替换 brent / wti 变量再验证。
wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,(class="num">1+look_ahead),fetch); class=class="str">"cmt">//Calculate the spread spread = brent_price - wti_price; Print("The Current Spread: ",spread); A.Reshape(class="num">3,fetch); class=class="str">"cmt">//Add the spread to the class="kw">input matrix A.Row(spread,class="num">1); class=class="str">"cmt">//Add a column for the intercept A.Row(intercept,class="num">2); class=class="str">"cmt">//Reshape the matrices A = A.Transpose(); y = y.Transpose(); class=class="str">"cmt">//Inspect the matrices if((A.Cols() == class="num">0 || y.Cols() == class="num">0)) { Print("Error occured when copying historical data"); Print("A rows: ",A.Rows()," y rows: ",y.Rows()," A columns: ",A.Cols()," y cols: ",y.Cols()); Print("A"); Print(A); Print("y"); Print(y); class="kw">return(false); } else { Print("No errors occured when copying historical data"); x = A.PInv().MatMul(y); Print("Finished Fitting The Model"); Print(x); class="kw">return(true); } } Print("Faield to select symbols"); class="kw">return(false); } class="type">class="kw">double ModelForecast() { if(model_initialized) { class=class="str">"cmt">//model class="kw">input A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,class="num">0,class="num">1); brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,class="num">0,class="num">1); wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,class="num">0,class="num">1); class=class="str">"cmt">//Calculate the spread spread = brent_price - wti_price; Print("The Spread: ",spread); A.Reshape(class="num">3,fetch); class=class="str">"cmt">//Add the spread to the class="kw">input matrix A.Row(spread,class="num">1); class=class="str">"cmt">//Add a column for the intercept A.Row(intercept,class="num">2); class=class="str">"cmt">//Reshape the matrices A = A.Transpose(); class="type">class="kw">double _forecast = (A[class="num">0][class="num">0]*x[class="num">0][class="num">0]) + (A[class="num">1][class="num">0]*x[class="num">1][class="num">0]) + (A[class="num">2][class="num">0]*x[class="num">2][class="num">0]); class="kw">return(_forecast); } class="kw">return(class="num">0); }
◍ 布伦特与WTI价差回归的矩阵搭建
这段 EA 头文件把跨品种套利的数据骨架先立起来了:同时抓取 UK Brent Oil 与 WTI_OIL 的当前周期 BAR 数,乘以 consumption=0.01 的采样率,取两者较小值作为 max_bars,避免长历史对齐时一方数据不足。 look_ahead 直接取 max_bars 的四分之一并取整,意味着模型用约 75% 的重叠样本去推剩余 25% 的价差方向;fetch 再减 1 作为矩阵行数,实盘里若 Brent 当前有 4000 根 BAR,采样后 max_bars=40,look_ahead=10,fetch 即为 29。 A 是 29×6 的零矩阵,y 是 29×1 的输出列,x 是 6×1 的系数向量,intercept 全 1 充当偏置项——典型的多元线性回归布局,用 WTI 价、Brent 价、价差及滞后项凑出 6 个特征。 OnInit 里仅在 atr_multiple>0 时加载 ATR trailing(默认 5.0 倍),并读出 Brent 最小交易量 min_volume,外汇与原油价差策略杠杆敏感,实盘前务必在 MT5 品种规格里核对 SYMBOL_VOLUME_MIN,否则市价单可能直接拒单。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Brent EA.mq5 | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | 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=class="str">"cmt">//Libraries class="macro">#include <Trade\Trade.mqh> CTrade ExtTrade; class="macro">#include <TrailingStop\ATRTrailingStop3.mqh> ATRTrailingStop ExtATRTrailingStop; class=class="str">"cmt">//Inputs class="kw">input class="type">class="kw">double atr_multiple = class="num">5.0; class="kw">input class="type">class="kw">double lot_multiple = class="num">1.0; class="kw">input class="type">class="kw">double profit_target = class="num">10; class="kw">input class="type">class="kw">double max_loss = class="num">20; class="kw">input class="type">int position_size = class="num">2; class=class="str">"cmt">//Set this value between class="num">0 and class="num">1 to control how much data is used class="type">class="kw">double consumption = class="num">0.01; class=class="str">"cmt">//We want to know which symbol has the least number of bars. class="type">class="kw">double brent_bars = (class="type">class="kw">double) NormalizeDouble((iBars("UK Brent Oil",PERIOD_CURRENT) * consumption),class="num">0); class="type">class="kw">double wti_bars = (class="type">class="kw">double) NormalizeDouble((iBars("WTI_OIL",PERIOD_CURRENT) * consumption),class="num">0); class=class="str">"cmt">//Select the lowest class="type">class="kw">double max_bars = (brent_bars < wti_bars) ? brent_bars : wti_bars; class=class="str">"cmt">//How far into the future are we forecasting class="type">class="kw">double look_ahead = NormalizeDouble((max_bars / class="num">4),class="num">0); class=class="str">"cmt">//How many bars should we fetch? class="type">int fetch = (class="type">int)(max_bars - look_ahead) - class="num">1; class=class="str">"cmt">//Matrix A stores our inputs. y is the output. x is the coefficients. matrix A = matrix::Zeros(fetch,class="num">6); matrix y = matrix::Zeros(fetch,class="num">1); vector wti_price = vector::Zeros(fetch); vector brent_price = vector::Zeros(fetch); vector spread; vector intercept = vector::Ones(fetch); matrix x = matrix::Zeros(class="num">6,class="num">1); class="type">class="kw">double forecast = class="num">0; class="type">class="kw">double ask = class="num">0; class="type">class="kw">double bid = class="num">0; class="type">class="kw">double min_volume = class="num">0; class="type">class="kw">string brent = "UK Brent Oil"; class="type">class="kw">string wti = "WTI_OIL"; class="type">bool model_initialized = false; class="type">int OnInit() { class=class="str">"cmt">//Initialise trailing stops if(atr_multiple > class="num">0) ExtATRTrailingStop.Init(atr_multiple); min_volume = SymbolInfoDouble(brent,SYMBOL_VOLUME_MIN); class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+
主循环与风控退场的接线方式
EA 的生命周期里,OnDeinit 留空不代表没用——它只是不在这里做清理,真正干活的是 OnTick。每次报价进来先抓 brent 的卖价和买价:ask = SymbolInfoDouble(brent, SYMBOL_ASK) 与 bid = SymbolInfoDouble(brent, SYMBOL_BID),这两行决定了后面所有价差判断的基准。 模型没初始化就先 InitializeModel(),初始化成功后看仓位:PositionsTotal()==0 就跑 ModelForecast() 拿预测值再 InterpretForecast();有仓就交给 ManageTrades() 管。这个分支让「无仓预测、有仓管理」严格分开,避免重复开单。 ManageTrades 的风控很直白:账户浮盈超 profit_target 或浮亏低于 -1*max_loss 就 CloseAll()。外汇与贵金属杠杆高,这种硬止损能在单日黑天鹅里把本金回撤锁死,但参数设太窄会被噪音洗出场。 InterpretForecast 把 forecast 和当前 K 线收盘价比:预测价高于 iClose(_Symbol, PERIOD_CURRENT, 0) 走 check_buy,低于就 check_sell。注意它比的是当前品种收盘而非 brent,跨品种信号映射到本品种执行时这块最容易出偏差,上机前建议打印 forecast 与 close 的差值日志。
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">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- ask = SymbolInfoDouble(brent,SYMBOL_ASK); bid = SymbolInfoDouble(brent,SYMBOL_BID); if(model_initialized) { if(PositionsTotal() == class="num">0) { forecast = class="num">0; forecast = ModelForecast(); InterpretForecast(); } else { ManageTrades(); } } else { model_initialized = InitializeModel(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|This function closes trades if we reach our profit or loss limit | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ManageTrades() { if(AccountInfoDouble(ACCOUNT_PROFIT) > profit_target) CloseAll(); if(AccountInfoDouble(ACCOUNT_PROFIT) < (-class="num">1 * max_loss)) CloseAll(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|This function judges if our model is giving a class="type">long or class="type">short signal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void InterpretForecast() { if(forecast != class="num">0) { if(forecast > iClose(_Symbol,PERIOD_CURRENT,class="num">0)) { check_buy(); } else if(forecast < iClose(_Symbol,PERIOD_CURRENT,class="num">0)) { check_sell(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|This function checks if we can open buy positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void check_buy() {