用Python重塑经典策略:移动平均线交叉·进阶篇
(2/3)·当经典双均线交叉遇上算法主导市场,它到底还剩多少预测力?本文用Python给出答案
把 LDA 与逻辑回归的预测摆进箱线图
把上一阶段算出的分类分数直接画成箱线图,能直观看出 LDA 的改进幅度。相比基线,LDA 在箱线图里的分布收得更窄,逻辑回归只有轻微但能肉眼分辨的提升。 预测均线交叉时,LDA 给出的分数在箱线图里始终成团、几乎不散开,说明预测稳定、残差可能带平稳性,模型抓到了可用关系。外汇与贵金属行情受事件扰动大,这种聚集只是样本内现象,实盘仍属高风险。 混淆矩阵(真实类在左、预测类在下)暴露了偏向:上涨走势有 47% 被误判为下跌,而真正下跌只有 25% 被误判为上涨。结论很直接——这套 LDA 在辨跌上比辨涨更靠谱,样本内胜率不对称。 学习曲线能拆出过拟合与欠拟合。LDA 的蓝(训练)橙(验证)两线有可见但不大的缺口,说明轻微过拟合、程度不重;小神经网络则两头准确率都低,且在训练样本逼近 10000 前验证表现随数据增多反而变糟,之后才边际改善,是典型的欠拟合节奏。
◍ 特征消除
在大多数机器学习项目中,并非所有输入都直接与目标变量相关,这是不常见的。通常,只有可用输入的一个子集与预测目标相关。消除不相关输入有几个优点,如: 在模型训练和特征工程过程中提高计算效率。 如果移除的特征包含噪声,则可以提高模型准确性。 接下来,我们需要确定移动平均线之间是否存在有意义的关系。我们将使用特征消除算法来验证这种假设关系。如果这些算法未能从输入列表中消除移动平均线,则表明存在有意义的关系。相反,如果它们成功移除了这些特征,则表明移动平均线与移动平均线交叉之间不存在显著关系。 我们将采用一种称为向后选择的特征选择技术。该方法首先使用所有可用输入拟合线性模型,然后测量模型的准确性。随后,每次移除一个特征,并记录对模型准确性的影响。在每一步中,消除导致准确性下降最小的特征,直到没有特征剩余。在这个阶段,算法会自动选择它已识别的最重要特征,并建议使用它们。 值得提及的是特征消除的一个显著缺点是,当数据集中存在噪声和不重要的列时,重要列可能看起来不提供信息。因此,由于系统中的噪声,向后选择算法可能会无意中消除一个重要特征,因为它看起来不提供信息。 现在,让我们继续查看计算机认为哪些列是重要的。我们首先导入一个名为mlxtend的库,它包含向后选择算法的实现。 然后,我们在数据集上应用该算法。让我们特别注意我们传递的三个参数: "k_features=" 指示算法选择多少列。我们可以通过传递一个从1开始到数据集中总列数的区间,来指示算法仅选择它认为必要的列。 "forward=" 指示算法是否应使用向前选择或向后选择,我们希望使用向后选择,因此将此参数设置为"False"。 "n_jobs=" 指示算法是否应并行执行计算,我们传递"-1"以允许算法使用所有可用内核,这将显著减少花费的时间。 [Parallel(n_jobs=-1)]:使用后向LokyBackend,启用8个并发工作进程。 [Parallel(n_jobs=-1)]:完成八分之三 | 耗时: 8.0s 剩余: 13.3s [Parallel(n_jobs=-1)]:完成八分之八 | 耗时: 8.0s 剩余: 0.0s [Parallel(n_jobs=-1)]:完成八分之八 | 耗时: 8.0s 完成 一旦该过程完成,我们就可以使用以下命令来获取算法认为重要的输入特征列表。 ('open', 'high', 'close', 'SMA_5', 'SMA_50') 正如我们所见,后向选择算法将我们的两个移动平均线纳入了其重要特征列表。这对我们来说是个极好的消息,因为它证实了我们的交易策略并非仅仅源于一个虚假的回归结果。 [CODE] from mlxtend.feature_selection import SequentialFeatureSelector backward_feature_selector = SequentialFeatureSelector(LinearDiscriminantAnalysis(), &nbs
「给双均线模型塞进五类辅助指标」
双均线关系确认后,下一步不是直接训模型,而是先堆特征。机器学习里选输入更像手艺活——你很难在跑之前断定哪个指标真有用,只能把觉得合理的都加进去,再用后向选择看留存。 这次在 EURUSD 的同一段行情里,除了原有的 SMA_5 与 SMA_50,我们额外拉了 MACD(看趋势确认)、Awesome Oscillator(看动量翻转)、Aroon(抓新趋势起点)、CCI(判超买超卖)、以及百分比回报率(看价格正负增长)。 后向选择跑完,算法从全部候选里留下这 7 个:('close', 'tick_volume', 'spread', 'SMA_5', 'SMA_50', 'MACDh_12_26_9', 'AO_5_34')。注意 Aroon、CCI 和百分比回报率全被剔了,说明它们在这段样本里对交叉方向判别几乎没增量。 外汇和贵金属波动受消息与流动性冲击大,特征被留下不代表未来恒成立,换品种或周期可能完全翻盘,实盘前务必自己重跑选择器。
class="macro">#Fetch market data market_data = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,date_start,date_end)) market_data["time"] = pd.to_datetime(market_data["time"],unit=&class="macro">#x27;s&class="macro">#x27;) class="macro">#Add simple moving average technical indicator market_data.ta.sma(length=class="num">5,append=True) class="macro">#Add simple moving average technical indicator market_data.ta.sma(length=class="num">50,append=True) class="macro">#Add macd market_data.ta.macd(append=True) class="macro">#Add awesome oscilator market_data.ta.ao(append=True) class="macro">#Add aroon market_data.ta.aroon(append=True) class="macro">#Add chaikins comodity index market_data.ta.cci(append=True) class="macro">#Add percent class="kw">return market_data.ta.percent_return(append=True) class="macro">#Delete missing rows market_data.dropna(inplace=True) class="macro">#Add the target market_data["target"] = class="num">0 market_data.loc[market_data["SMA_5"].shift(-look_ahead) > market_data["SMA_50"].shift(-look_ahead),"target"] = class="num">1 market_data.loc[market_data["SMA_5"].shift(-look_ahead) < market_data["SMA_50"].shift(-look_ahead),"target"] = class="num">0 class="macro">#The last rows do not have answers market_data = market_data[:-look_ahead] market_data backward_feature_selector = SequentialFeatureSelector(LinearDiscriminantAnalysis(), k_features=(class="num">1,market_data.loc[:,predictors].shape[class="num">1]), forward=False, verbose=class="num">2, scoring="accuracy", cv=class="num">5 ).fit(market_data.iloc[:,class="num">1:-class="num">1],market_data.loc[:,"target"]) backward_feature_selector.k_feature_names_
把 LDA 模型塞进 MT5 实时信号循环
整合策略的核心,是用 LDA 线性判别模型对接 MT5 实时行情:先用训练数据里筛选出的 7 个特征列(close、tick_volume、spread、SMA_5、SMA_50、MACDh_12_26_9、AO_5_34)拟合模型,再让程序从终端拉数据、算指标、出预测。 预测信号本身只有两类——模型输出 1 解读为 buy,输出 0 解读为 sell。回测样例中,2024-06-25 14:35:37 这条实时推演给出的就是 sell 倾向,说明当时短周期结构偏弱。 get_prices() 从 2024-06-01 起拉 EURUSD 的 copy_rates_range,补完 SMA(5/50)、AO、MACD 后只留最后 2 行作为模型输入;ai_signal() 取 forecast[1] 作为最新一根 K 线的判断。主循环 while True 里每次重新取价预测,有反向持仓先平仓再开仓。 外汇与贵金属杠杆高、滑点跳空频繁,这类信号仅代表统计倾向,实盘前务必在策略测试器用 2024 年 6 月数据跑一遍验证成交逻辑。
predictors = [&class="macro">#x27;close&class="macro">#x27;,&class="macro">#x27;tick_volume&class="macro">#x27;,&class="macro">#x27;spread&class="macro">#x27;,&class="macro">#x27;SMA_5&class="macro">#x27;,&class="macro">#x27;SMA_50&class="macro">#x27;,&class="macro">#x27;MACDh_12_26_9&class="macro">#x27;,&class="macro">#x27;AO_5_34&class="macro">#x27;] model = LinearDiscriminantAnalysis() model.fit(market_data.loc[:,predictors],market_data.loc[:,"target"]) def get_prices(): start = class="type">class="kw">datetime(class="num">2024,class="num">6,class="num">1) end = class="type">class="kw">datetime.now() data = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,start,end)) class="macro">#Add simple moving average technical indicator data.ta.sma(length=class="num">5,append=True) data.ta.sma(length=class="num">50,append=True) class="macro">#Add awesome oscilator data.ta.ao(append=True) class="macro">#Add macd data.ta.macd(append=True) class="macro">#Delete missing rows data.dropna(inplace=True) 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) data = data.loc[:,[&class="macro">#x27;close&class="macro">#x27;,&class="macro">#x27;tick_volume&class="macro">#x27;,&class="macro">#x27;spread&class="macro">#x27;,&class="macro">#x27;SMA_5&class="macro">#x27;,&class="macro">#x27;SMA_50&class="macro">#x27;,&class="macro">#x27;MACDh_12_26_9&class="macro">#x27;,&class="macro">#x27;AO_5_34&class="macro">#x27;]] data = data.iloc[-class="num">2:,:] class="kw">return(data) class="macro">#Get signals LDA model def ai_signal(input_data,_model): class="macro">#Get a forecast forecast = _model.predict(input_data) class="kw">return forecast[class="num">1] class="macro">#if __name__ == &class="macro">#x27;__main__&class="macro">#x27;: class="macro">#We&class="macro">#x27;ll use an infinite loop to keep the program running class="kw">while True: class="macro">#Fetching model prediction signal = ai_signal(get_prices(),model) class="macro">#Decoding model prediction into an action if signal == class="num">1: direction = &class="macro">#x27;buy&class="macro">#x27; elif signal == class="num">0: direction = &class="macro">#x27;sell&class="macro">#x27; print(f&class="macro">#x27;AI Forecast: {direction}&class="macro">#x27;) class="macro">#Opening A Buy Trade class="macro">#But first we need to ensure there are no opposite trades open on the same symbol if direction == &class="macro">#x27;buy&class="macro">#x27;: class="macro">#Close any sell positions for pos in mt5.positions_get(): if pos.type == class="num">1: class="macro">#This is an open sell order, and we need to close it close_order(pos.ticket) if not mt5.positions_totoal(): class="macro">#We have no open positions mt5.Buy(symbol,volume) class="macro">#Opening A Sell Trade elif direction == &class="macro">#x27;sell&class="macro">#x27;: class="macro">#Close any buy positions for pos in mt5.positions_get(): if pos.type == class="num">0: class="macro">#This is an open buy order, and we need to close it close_order(pos.ticket) if not mt5.positions_get(): class="macro">#We have no open positions mt5.sell(symbol,volume)
◍ 用 Python 轮询盯盘时间的笨办法
在 MT5 之外做辅助监控时,有人会用 Python 脚本周期性打印当前时间并休眠,以便肉眼确认脚本还活着。 下面这段代码片段每 60 秒在控制台输出一次本地时间并空一行:先打印 datetime.now() 得到的当前时刻,再打印分隔线,随后 time.sleep(60) 挂起进程一分钟。 这种轮询方式延迟固定为 60 秒级别,仅适合低频率的状态巡查;若用于外汇或贵金属信号触发,滑点和漏报风险较高,建议仅作辅助验证而非交易依据。
print(&class="macro">#x27;time: &class="macro">#x27;, class="type">class="kw">datetime.now()) print(&class="macro">#x27;------- &class="macro">#x27;) time.sleep(class="num">60)
「在 MT5 里徒手搭一个分类器 EA」
把模型写进 MQL5 而不是导出 ONNX,最直接的好处是跳过「一个市场一套模型、一个周期再套一份」的重复劳动。同一段 EA 挂在 EURUSD 或 XAUUSD、切到 M5 还是 H1,理论上都能直接跑,限制只在你给的历史样本长度。 下面这段全局声明是整套程序的骨架:三个系数 b_nort、b_one、b_two 初始全为 0,后续靠误差反推迭代;sl_width 默认 0.4 是 ATR 倍数,用来动态算止损止盈,而不是写死 100 点。lot_multiple=20、positions=2 是给终端用户调的仓位与并发手数参数。 [CODE 逐行拆解] //Global variables int ma_5,ma_50; // 短期与长期 EMA 的指标句柄 double bid, ask; // 当前买卖价 double min_volume; // 最小成交量限制 double ma_50_reading[],ma_5_reading[]; // 存两类均线的缓冲数组 int size; // 训练样本量 double current_prediction; // 模型最新预测值 int state = -1; // 持仓状态标记 matrix ohlc; // 收盘价矩阵 vector target; // 标签向量:1 为多 0 为空 double b_nort = 0; // 截距系数初值 double b_one = 0; // 系数1初值 double b_two = 0; // 系数2初值 long min_distance,atr_stop; // 止损距离相关 //Inputs int input lot_multiple = 20; // 手数放大倍数 int input positions = 2; // 最大并发仓位 double input sl_width = 0.4; // ATR 止损倍数 //Libraries #include <Trade\Trade.mqh> // 引入交易库 CTrade Trade; // 交易对象实例 get_training_data 函数抓 100 根 K 线(size=100),用 iMA 取周期 5 与 50 的 EMA,再把「短均 > 长均」标为 1、「短均 < 长均」标为 0 写进 target。这一步标签直接决定了后面预测值 1=买入信号、0=卖出信号的含义。 训练放在 OnInit 里触发,实盘预测出来后由独立函数解释:prediction=1 倾向开多,=0 倾向开空;同时每仓挂 ATR 动态止损与止盈。外汇与贵金属杠杆高、滑点跳空频繁,这种动态止损也只降低风险而非消除,回测盈利不代表实盘概率同权。 把上面代码贴进 MT5 EA 编辑器能直接编译通过,先不改参数挂模拟盘,看 Print 里 'Done getting training data.' 是否按时吐出,再决定是否调 lot_multiple。
class=class="str">"cmt">//Global variables class="type">int ma_5,ma_50; class="type">class="kw">double bid, ask; class="type">class="kw">double min_volume; class="type">class="kw">double ma_50_reading[],ma_5_reading[]; class="type">int size; class="type">class="kw">double current_prediction; class="type">int state = -class="num">1; matrix ohlc; vector target; class="type">class="kw">double b_nort = class="num">0; class="type">class="kw">double b_one = class="num">0; class="type">class="kw">double b_two = class="num">0; class="type">long min_distance,atr_stop; class=class="str">"cmt">//Inputs class="type">int input lot_multiple = class="num">20; class="type">int input positions = class="num">2; class="type">class="kw">double input sl_width = class="num">0.4; class=class="str">"cmt">//Libraries class="macro">#include <Trade\Trade.mqh> CTrade Trade; class=class="str">"cmt">//+----------------------------------------------------------------------+ class=class="str">"cmt">//|This function is responsible for getting our training data ready | class=class="str">"cmt">//+----------------------------------------------------------------------+ class="type">void get_training_data(class="type">void) { class=class="str">"cmt">//How much data are we going to use? size = class="num">100; class=class="str">"cmt">//Copy price data ohlc.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,class="num">1,size); class=class="str">"cmt">//Get indicator data ma_50 = iMA(_Symbol,PERIOD_CURRENT,class="num">50,class="num">0,MODE_EMA,PRICE_CLOSE); ma_5 = iMA(_Symbol,PERIOD_CURRENT,class="num">5,class="num">0,MODE_EMA,PRICE_CLOSE); CopyBuffer(ma_50,class="num">0,class="num">0,size,ma_50_reading); CopyBuffer(ma_5,class="num">0,class="num">0,size,ma_5_reading); ArraySetAsSeries(ma_50_reading,true); ArraySetAsSeries(ma_5_reading,true); class=class="str">"cmt">//Label the target target = vector::Zeros(size); for(class="type">int i = class="num">0; i < size; i++) { if(ma_5_reading[i] > ma_50_reading[i]) { target[i] = class="num">1; } else if(ma_5_reading[i] < ma_50_reading[i]) { target[i] = class="num">0; } } class=class="str">"cmt">//Feedback Print("Done getting training data."); } class=class="str">"cmt">//+----------------------------------------------------------------------+ class=class="str">"cmt">//|This function is responsible for making predictions using our model | class=class="str">"cmt">//+----------------------------------------------------------------------+