S&P 500交易策略在MQL5中的实现(适合初学者)·进阶篇
(2/3)·当AI信号飘忽时,用CCI/RSI/WPR/MA四条硬规则给模型兜底
◍ 用多指标矩阵搭线性回归的初始化骨架
在 MT5 用 MQL5 做跨品种线性回归预测,第一步是把输入特征和目标品种的历史数据装进 matrix / vector 容器。下面这段全局声明先把 US_500 作为目标品种,并预留 CCI、RSI、WPR、MA 四个指标句柄与缓冲区,训练标志默认全 false。 初始化函数 OnInit 里只做两件事:用 iRSI、iMA(EMA 模式)、iWPR、iCCI 绑定 target_symbol 的 PERIOD_CURRENT 数据,再用 SymbolInfoDouble 取当前图表品种的最小成交量。注意指标周期 rsi_period、ma_period 等是外部参数,没填默认会编译报错。 model_initialize 才是拟合入口:先把 model_being_trained 置 true,再按注释约定——输入数据起始位 inupt_start = 1 + look_ahead,输出起始位 output_start = 1,保证输入比输出旧、不含最新 bar。随后依次调 fetch_input_data、fetch_output_data、prepare_data(0)、fit_linear_model,最后 model_ready 翻转标志。 别把正态当圣经:这段只搭了数据流向,prepare_data 里的归一化若没处理量纲,US_500 和外汇品种混训时梯度可能爆掉,建议开 MT5 把 desired_symbols 改成同资产类别先跑通。
matrix input_matrix = matrix::Ones(fetch,(ArraySize(desired_symbols))-class="num">1); matrix output_matrix = matrix::Ones(fetch,class="num">1); vector initiall_input_values = vector::Ones((ArraySize(desired_symbols))); class="type">class="kw">double initiall_output_value = class="num">0.0; matrix b; class="type">class="kw">string target_symbol = "US_500"; vector symbol_history = vector::Zeros(fetch); class="type">bool model_initialized = class="kw">false; class="type">bool model_being_trained = class="kw">false; class="type">class="kw">double model_forecast = class="num">0; class="type">int cci_handler,rsi_handler,wpr_handler,ma_handler; class="type">class="kw">double cci_buffer[],rsi_buffer[],wpr_buffer[],ma_buffer[]; class="type">int OnInit() { rsi_handler = iRSI(target_symbol,PERIOD_CURRENT,rsi_period,PRICE_CLOSE); ma_handler = iMA(target_symbol,PERIOD_CURRENT,ma_period,class="num">0,MODE_EMA,PRICE_CLOSE); wpr_handler = iWPR(target_symbol,PERIOD_CURRENT,wpr_period); cci_handler = iCCI(target_symbol,PERIOD_CURRENT,cci_period,PRICE_CLOSE); minimum_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); class="kw">return(INIT_SUCCEEDED); } class="type">void model_initialize(class="type">void) { model_being_trained = true; inupt_start = class="num">1+look_ahead; output_start = class="num">1; fetch_input_data(inupt_start,fetch); fetch_output_data(output_start,fetch); prepare_data(class="num">0); fit_linear_model(); model_ready(); } class="type">void fetch_input_data(class="type">int f_input_start,class="type">int f_fetch) {
「抓取与归一化训练矩阵的实操细节」
在 MT5 用多标的收盘序列训练线性模型时,输入矩阵的第一列必须恒为 1,用来承载截距项。代码里用 matrix::Ones(f_fetch, 13) 先铺满 1,再 Reshape 成 f_fetch 行 13 列,随后从列 1 开始填各品种数据,列 0 最后用 vector::Ones(f_fetch) 覆盖,这个顺序不能反。 输出矩阵则固定为 f_fetch 行 1 列,直接把目标品种 COPY_RATES_CLOSE 拷进列 0。注意输入用 ArraySize(desired_symbols)-1 算宽度、输出只有 1 列,若 desired_symbols 含 14 个品种,输入矩阵实际宽 13 列再加截距列,总共 14 列——和 Reshape 的 13 参数对不上时会报维度错,开 MT5 跑前先核对符号数组长度。 归一化只在 f_flag==0 时做一次:把每列初值存下,之后整列除以该初值,使首行变 1,输出也按同样逻辑缩放成价格相对涨幅。后续实时推断直接除初始值即可,不必重算。外汇与贵金属价差跳变频繁,这类归一化对高波动品种可能放大噪声,建议先用历史数据回测再上实盘。
input_matrix = matrix::Ones(f_fetch,(ArraySize(desired_symbols))-class="num">1); input_matrix.Reshape(f_fetch,class="num">13); for(class="type">int i=class="num">1; i <= ArraySize(desired_symbols);i++) { symbol_history.CopyRates(desired_symbols[i-class="num">1],PERIOD_CURRENT,COPY_RATES_CLOSE,f_input_start,f_fetch); input_matrix.Col(symbol_history,i); } vector intercept = vector::Ones(f_fetch); input_matrix.Col(intercept,class="num">0); output_matrix = matrix::Ones(f_fetch,class="num">1); output_matrix.Reshape(f_fetch,class="num">1); symbol_history.CopyRates(target_symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,f_output_start,f_fetch); output_matrix.Col(symbol_history,class="num">0);
用首值做基准把输入列压成相对比例
把多列行情数据归一化,最省事的办法是以第 0 行的值为基准,整列同除该值。这样每列首个元素必然变成 1,后续数值小于 1 表示相对起点走弱,大于 1 表示走强。 代码里先存下各列初始值 initiall_input_values,再把整列拷到 temp_vector 后除以基准,写回矩阵。输出列同理:temp_vector_output 除以 initiall_output_value 后,1.023 代表涨了约 2.3%,0.8732 代表跌了约 22.67%,一眼能读出相对涨跌幅度。 预测阶段若 f_flag==1,只把最新一行的输入列分别除以之前算好的 initiall_input_values,就得到可直接送模型的相对化特征。外汇与贵金属波动剧烈,这种比例缩放不改变方向概率,实盘仍属高风险,须用 MT5 打印矩阵自行核对首值是否为 1。 模型就绪后 model_ready 把 model_initialized 置 true、model_being_trained 置 false,系统才被放行交易;model_predict 开头先打印取数日志,说明预测前必须保证实时行情已按同一基准归一。
initiall_input_values[i] = input_matrix[class="num">0][i+class="num">1]; class=class="str">"cmt">//Now temporarily copy the column, so that we can normalize the entire column at once vector temp_vector = input_matrix.Col(i+class="num">1); class=class="str">"cmt">//Now divide that entire column by its initial value, if done correctly the first value of each column should be class="num">1 temp_vector = temp_vector / initiall_input_values[i]; class=class="str">"cmt">//Now reinsert the normalized column input_matrix.Col(temp_vector,i+class="num">1); } class=class="str">"cmt">//Print the initial class="kw">input values Print("Our initial class="kw">input values for each column "); Print(initiall_input_values); class=class="str">"cmt">//Scale the output class=class="str">"cmt">//Store the initial output value initiall_output_value = output_matrix[class="num">0][class="num">0]; class=class="str">"cmt">//Make a temporary copy of the entire output column vector temp_vector_output = output_matrix.Col(class="num">0); class=class="str">"cmt">//Divide the entire column by its initial value, so that the first value in the column is one class=class="str">"cmt">//This means that any time the value is less than class="num">1, price fell class=class="str">"cmt">//And every time the value is greater than class="num">1, price rose. class=class="str">"cmt">//A value of class="num">1.023... means price increased by class="num">2.3% class=class="str">"cmt">//A value of class="num">0.8732.. would mean price fell by class="num">22.67% temp_vector_output = temp_vector_output / initiall_output_value; class=class="str">"cmt">//Reinsert the column back into the output matrix output_matrix.Col(temp_vector_output,class="num">0); class=class="str">"cmt">//Shpw the scaled data Print("Data has been normlised and the output has been scaled:"); Print("Input matrix: "); Print(input_matrix); Print("Output matrix: "); Print(output_matrix); } class=class="str">"cmt">//Normalize the data class="kw">using the initial values we have allready calculated if(f_flag == class="num">1) { class=class="str">"cmt">//Divide each of the class="kw">input values by its corresponding class="kw">input value Print("Preparing to normalize the data for prediction"); for(class="type">int i = class="num">0; i < ArraySize(desired_symbols);i++) { input_matrix[class="num">0][i+class="num">1] = input_matrix[class="num">0][i+class="num">1]/initiall_input_values[i]; } Print("Input data being used for prediction"); Print(input_matrix); } } class=class="str">"cmt">//This function will allow our program to class="kw">switch states and begin forecasting and looking for trades class="type">void model_ready(class="type">void) { class=class="str">"cmt">//Our model is ready. class=class="str">"cmt">//We need to class="kw">switch these flags to give the system access to trading functionality Print("Giving the system permission to begin trading"); model_initialized = true; model_being_trained = class="kw">false; } class=class="str">"cmt">//This function will obtain a prediction from our model class="type">class="kw">double model_predict(class="type">void) { class=class="str">"cmt">//First we have to fetch current market data Print("Obtaining a forecast from our model");
◍ 从伪逆拟合到下单信号的链路
线性回归模型用伪逆解(OLS)估算参数,本质是最小化训练集上的残差平方和(RSS)。代码里 b = input_matrix.PInv().MatMul(output_matrix) 一行就完成了系数求解,但注意:这组 b 只保证在训练数据上误差最小,对未见过行情的泛化能力并未验证,实盘外汇/贵金属波动下可能明显偏移。 拟合完成后,update_technical_indicators() 负责把 RSI、CCI、WPR、MA 这四类指标的近 10 根缓冲拉进数组,并用 ArraySetAsSeries 把时序倒排,保证 buffer[0] 永远是最新值。任何漏掉 AsSeries 的设置,都会让后续矩阵错位、预测变成用未来数据回看。 find_entry() 里直接读 bearish_sentiment() 的布尔结果来触发 Trade.Sell(minimum_volume * lot_multiple, _Symbol, bid_price)。这条卖单逻辑没有任何止盈止损包裹,贵金属隔夜跳空时风险偏高,建议你在 MT5 里跑之前先给 Trade 对象挂上 SL/TP 再测。
fetch_input_data(class="num">0,class="num">1); class=class="str">"cmt">//Now we need to normalize our data class="kw">using the values we calculated when we initialized the model prepare_data(class="num">1); class=class="str">"cmt">//Now we can class="kw">return our model&class="macro">#x27;s forecast class="kw">return((b[class="num">0][class="num">0] + (b[class="num">1][class="num">0]*input_matrix[class="num">0][class="num">0]) + (b[class="num">2][class="num">0]*input_matrix[class="num">0][class="num">1]) + (b[class="num">3][class="num">0]*input_matrix[class="num">0][class="num">2]) + (b[class="num">4][class="num">0]*input_matrix[class="num">0][class="num">3]) + (b[class="num">5][class="num">0]*input_matrix[class="num">0][class="num">4]) + (b[class="num">6][class="num">0]*input_matrix[class="num">0][class="num">5]) + (b[class="num">7][class="num">0]*input_matrix[class="num">0][class="num">6]) + (b[class="num">8][class="num">0]*input_matrix[class="num">0][class="num">7]) + (b[class="num">9][class="num">0]*input_matrix[class="num">0][class="num">8]) + (b[class="num">10][class="num">0]*input_matrix[class="num">0][class="num">9]) + (b[class="num">11][class="num">0]*input_matrix[class="num">0][class="num">10])+ (b[class="num">12][class="num">0]*input_matrix[class="num">0][class="num">11]))); } class=class="str">"cmt">//This function will fit our linear model class="kw">using the pseudo inverse solution class="type">void fit_linear_model(class="type">void) { class=class="str">"cmt">/* 伪逆解通过最小化误差(残差平方和,RSS)来找到一组值,将我们的训练数据输入映射到输出。 这些系数值可以通过线性代数轻松估算,但这些系数仅能最小化我们在训练数据上的误差,而不是在未见过的数据上! */ Print("Attempting to find OLS solutions."); class=class="str">"cmt">//Now we can estimate our model parameters b = input_matrix.PInv().MatMul(output_matrix); class=class="str">"cmt">//Let&class="macro">#x27;s see our model parameters Print("Our model parameters "); Print(b); } class=class="str">"cmt">//Update our technical indicator values class="type">void update_technical_indicators() { class=class="str">"cmt">//Get market data ask_price = SymbolInfoDouble(_Symbol,SYMBOL_ASK); bid_price = SymbolInfoDouble(_Symbol,SYMBOL_BID); class=class="str">"cmt">//Copy indicator buffers CopyBuffer(rsi_handler,class="num">0,class="num">0,class="num">10,rsi_buffer); CopyBuffer(cci_handler,class="num">0,class="num">0,class="num">10,cci_buffer); CopyBuffer(wpr_handler,class="num">0,class="num">0,class="num">10,wpr_buffer); CopyBuffer(ma_handler,class="num">0,class="num">0,class="num">10,ma_buffer); class=class="str">"cmt">//Set the arrays as series ArraySetAsSeries(rsi_buffer,true); ArraySetAsSeries(cci_buffer,true); ArraySetAsSeries(wpr_buffer,true); ArraySetAsSeries(ma_buffer,true); } class=class="str">"cmt">//Let&class="macro">#x27;s try find an entry signal class="type">void find_entry(class="type">void) { class=class="str">"cmt">//Our sell setup if(bearish_sentiment()) { Trade.Sell(minimum_volume * lot_multiple,_Symbol,bid_price); } class=class="str">"cmt">//Our buy setup
「多空共振函数与持仓止损刷新」
空头共振函数 bearish_sentiment 里写死了五层过滤:收盘价低于均线、CCI 小于 0、RSI 小于 50、WPR 小于 -50、模型预测值小于 1。五个条件用 && 串起来,任一不满足就返回 false,实盘里这种硬性串联会显著压低触发频率,但也能过滤掉大部分震荡假信号。 多头一侧 bullish_sentiment 把阈值镜像翻转:收盘价站上均线、CCI 大于 0、RSI 大于 50、WPR 大于 -50、模型预测大于 1。注意原文注释里把 buy setup 错写成了 'For a sell setup',读代码时别被注释带偏,看 return 里的逻辑才是真的。 update_stoploss 负责遍历持仓并刷新止损。它从 PositionsTotal()-1 倒序循环,先用 PositionGetSymbol 取币种,再拿 _Symbol 做比对,只处理当前图表品种。拿到 ticket、开仓价、持仓类型、当前 SL 后,后续才计算新止损——这部分截断在取数,真正的 SL 重算逻辑要看完整 EA。 外汇和贵金属杠杆高、滑点跳空频繁,这类多指标共振在 EURUSD 15M 上可能把信号压缩到日均不到 3 次,参数阈值建议先在策略测试器跑历史再上模拟盘。
else if(bullish_sentiment()) { Trade.Buy(minimum_volume * lot_multiple,_Symbol,ask_price); } } class=class="str">"cmt">//This function tells us if all our tools align for a sell setup class="type">bool bearish_sentiment(class="type">void) { class=class="str">"cmt">/* For a sell setup, we want the following conditions to be satisfied: class="num">1) CCI reading should be less than class="num">0 class="num">2) RSI reading should be less than class="num">50 class="num">3) WPR reading should be less than -class="num">50 class="num">4) Our model forecast should be less than class="num">1 class="num">5) MA reading should be below the moving average */ class="kw">return((iClose(target_symbol,PERIOD_CURRENT,class="num">0) < ma_buffer[class="num">0]) && (cci_buffer[class="num">0] < class="num">0) && (rsi_buffer[class="num">0] < class="num">50) && (wpr_buffer[class="num">0] < -class="num">50) && (model_forecast < class="num">1)); } class=class="str">"cmt">//This function tells us if all our tools align for a buy setup class="type">bool bullish_sentiment(class="type">void) { class=class="str">"cmt">/* For a sell setup, we want the following conditions to be satisfied: class="num">1) CCI reading should be greater than class="num">0 class="num">2) RSI reading should be greater than class="num">50 class="num">3) WPR reading should be greater than -class="num">50 class="num">4) Our model forecast should be greater than class="num">1 class="num">5) MA reading should be above the moving average */ class="kw">return((iClose(target_symbol,PERIOD_CURRENT,class="num">0) > ma_buffer[class="num">0]) && (cci_buffer[class="num">0] > class="num">0) && (rsi_buffer[class="num">0] > class="num">50) && (wpr_buffer[class="num">0] > -class="num">50) && (model_forecast > class="num">1)); } class=class="str">"cmt">//+----------------------------------------------------------------------+ class=class="str">"cmt">//|This function is responsible for calculating our SL & TP values | class=class="str">"cmt">//+----------------------------------------------------------------------+ class="type">void update_stoploss(class="type">void) { class=class="str">"cmt">//First we iterate over the total number of open positions for(class="type">int i = PositionsTotal() -class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//Then we fetch the name of the symbol of the open position class="type">class="kw">string symbol = PositionGetSymbol(i); class=class="str">"cmt">//Before going any furhter we need to ensure that the symbol of the position matches the symbol we&class="macro">#x27;re trading if(_Symbol == symbol) { class=class="str">"cmt">//Now we get information about the position class="type">class="kw">ulong ticket = PositionGetInteger(POSITION_TICKET); class=class="str">"cmt">//Position Ticket class="type">class="kw">double position_price = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//Position Open Price class="type">long type = PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//Position Type class="type">class="kw">double current_stop_loss = PositionGetDouble(POSITION_SL); class=class="str">"cmt">//Current Stop loss value