用Python重塑经典策略:移动平均线交叉·进阶篇
🐍

用Python重塑经典策略:移动平均线交叉·进阶篇

(2/3)·当经典双均线交叉遇上算法主导市场,它到底还剩多少预测力?本文用Python给出答案

含代码示例偏理论 第 2/3 篇
不少交易者仍把双均线交叉当万能信号,却从没验证过它在M15这种周期上的真实胜率。用Python拉一段EURUSD历史数据跑一遍回测,你会发现很多教科书结论在现代盘口并不成立。

把 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 和百分比回报率全被剔了,说明它们在这段样本里对交叉方向判别几乎没增量。 外汇和贵金属波动受消息与流动性冲击大,特征被留下不代表未来恒成立,换品种或周期可能完全翻盘,实盘前务必自己重跑选择器。

MQL5 / C++
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 月数据跑一遍验证成交逻辑。

MQL5 / C++
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 秒级别,仅适合低频率的状态巡查;若用于外汇或贵金属信号触发,滑点和漏报风险较高,建议仅作辅助验证而非交易依据。

MQL5 / C++
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。

MQL5 / C++
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">//+----------------------------------------------------------------------+
把回测诊断交给小布盯盘
这些定量检验与特征筛选流程,小布盯盘的AIGC模块已内置常用模板,打开对应品种页即可一键跑出交叉策略的历史表现,你只需判断要不要跟。

常见问题

M15既能保留日内波动结构,又不会像分钟级那样噪声过载;EURUSD流动性高、点差稳定,适合作为基准品种先验证策略假设。
若模型显示某条均线对交叉预测贡献极低,说明在该样本期内它已失去边际信息,但换周期或品种后可能重新相关,不宜绝对否定。
可以,小布盯盘内置的AIGC辅助能把Python验证过的交叉逻辑转成MQL5骨架,你再补风控参数即可,省去重复敲基础代码。
只说明在该段样本与特征集下假设关系不成立,可能需引入成交量、波动率等其他维度,或策略本身仅在特定行情阶段有效,概率上而非必然失效。
常见补充有ATR波动率、RSI背离、布林带宽度收敛等,它们可能提供交叉发生前的微观结构预警,但需各自做独立性检验。