梯度提升(CatBoost)在交易系统开发中的应用. 初级的方法(基础篇)
◍ 用 CatBoost 给 MT5 策略找特征
梯度提升模型里,CatBoost 对类别型特征的处理比较省心,在 MT5 策略开发初期,可以拿它做特征筛选而不是直接当信号源。很多新手一上来就训练一个‘预测下一根K线涨跌’的模型,结果过拟合到连样本内都抖。 更稳的做法是先准备一批候选特征:RSI(14) 数值、ATR(20) 波动幅度、近期高低点突破距离、成交量变化率等,让 CatBoost 输出特征重要性排序。这样你能看到哪些维度在样本里真正有区分度,再决定进不进规则系统。 外汇与贵金属杠杆高、滑点随机,这类数据驱动筛选只代表历史样本中的统计倾向,实盘可能失效,别把重要性分数当胜率保证。 下面这段 MQL5 片段演示了如何在 EA 里读取 RSI 并写入数组,供后续导出到 Python 训练:
class="type">class="kw">double rsi_array[class="num">100]; for(class="type">int i=class="num">0;i<class="num">100;i++) rsi_array[i]=iRSI(_Symbol,_Period,class="num">14,PRICE_CLOSE,i); class=class="str">"cmt">// 取最近100根RSI(class="num">14)
「梯度提升与这次用的库」
梯度提升不是把树并行扔一起投票,而是一棵接一棵串着建:后一棵树专门啃前一棵没预测对的残差,叠到几十上百棵弱树后,整体泛化能力才够看。它和 bagging 最大的区别就在这——模型之间是有依赖的,不是各算各的。 这次实验选了 Yandex 的 CatBoost,没碰 XGBoost 或 LightGBM,但三者都属于顺序式 boosting 家族里最常被实盘研究者拎出来用的几个。CatBoost 对类别型特征处理比较省事,适合直接喂进来的异构行情数据。 整个流程拆开就四步:拉数据并做预处理、用干净数据训模型、丢进自定义策略测试器跑、再移植到 MetaTrader 5。前面数据准备和训练都用 Python 加 MetaTrader 5 官方库完成,后面移植才是 MT5 端的事。 外汇和贵金属波动受宏观事件扰动大,这类模型在历史样本上表现好不代表Live环境同样稳,上真金白银前务必先在策略测试器把滑点和点差跑进去。
把报价拉成带滞后特征的训练矩阵
做价格行为建模前,先得把 MT5 的 raw 报价改成模型能啃的二维表。核心思路是:取收盘价、算移动平均、再算价差(价格减均线的偏离),然后把这组偏离按 look_back 步数逐列后移,构造滞后特征。 以原文脚本为例,LOOK_BACK=250、MA_PERIOD=15、SYMBOL='EURUSD'、TIMEFRAME=H1、取样窗口 2020-05-01 到 2021-01-01。get_prices 里 prices.rolling(15).mean() 先出 15 期均线,ratesD = prices - ratesM 就是每根 K 的偏离值。 随后用 for i in range(look_back): prices[str(i)] = ratesD.shift(i) 把偏离值分别下移 0、1、2…249 行。若 look_back=10,就额外生成 10 列 0~9,每列都是同一段偏离序列、仅错位一行;每行因此成了一个含历史上下文的训练样本。 外汇与贵金属属高杠杆品种,历史偏移特征只描述已发生的统计结构,对后续方向仅有概率意义上的参考,不能直接当成进场依据。开 MT5 把这段脚本的 SYMBOL 换成你常看的 XAUUSD 或 EURUSD,改 LOOK_BACK 到 50 跑一遍,看 dropna 后剩多少行,就能直观感受样本损耗。
class="kw">import MetaTrader5 as mt5 class="kw">import pandas as pd class="kw">import numpy as np from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime class="kw">import random class="kw">import matplotlib.pyplot as plt from catboost class="kw">import CatBoostClassifier from sklearn.model_selection class="kw">import train_test_split mt5.initialize() # check for gpu devices is availible from catboost.utils class="kw">import get_gpu_device_count print(&class="macro">#x27;%i GPU devices&class="macro">#x27; % get_gpu_device_count()) LOOK_BACK = class="num">250 MA_PERIOD = class="num">15 SYMBOL = &class="macro">#x27;EURUSD&class="macro">#x27; MARKUP = class="num">0.0001 TIMEFRAME = mt5.TIMEFRAME_H1 START = class="type">class="kw">datetime(class="num">2020, class="num">5, class="num">1) STOP = class="type">class="kw">datetime(class="num">2021, class="num">1, class="num">1) def get_prices(look_back = class="num">15): prices = pd.DataFrame(mt5.copy_rates_range(SYMBOL, TIMEFRAME, START, STOP), columns=[&class="macro">#x27;time&class="macro">#x27;, &class="macro">#x27;close&class="macro">#x27;]).set_index(&class="macro">#x27;time&class="macro">#x27;) # set df index as class="type">class="kw">datetime prices.index = pd.to_datetime(prices.index, unit=&class="macro">#x27;s&class="macro">#x27;) prices = prices.dropna() ratesM = prices.rolling(MA_PERIOD).mean() ratesD = prices - ratesM for i in range(look_back): prices[str(i)] = ratesD.shift(i) class="kw">return prices.dropna() >>> pr = get_prices(look_back=LOOK_BACK) >>> pr close class="num">0 class="num">1 class="num">2 class="num">3 class="num">4 class="num">5 class="num">6 class="num">7 class="num">8 class="num">9 time class="num">2020-class="num">05-class="num">01 class="num">16:class="num">00:class="num">00 class="num">1.09750 class="num">0.001405 class="num">0.002169 class="num">0.001600 class="num">0.002595 class="num">0.002794 class="num">0.002442 class="num">0.001477 class="num">0.001190 class="num">0.000566 class="num">0.000285 class="num">2020-class="num">05-class="num">01 class="num">17:class="num">00:class="num">00 class="num">1.10074 class="num">0.004227 class="num">0.001405 class="num">0.002169 class="num">0.001600 class="num">0.002595 class="num">0.002794 class="num">0.002442 class="num">0.001477 class="num">0.001190 class="num">0.000566 class="num">2020-class="num">05-class="num">01 class="num">18:class="num">00:class="num">00 class="num">1.09976 class="num">0.002900 class="num">0.004227 class="num">0.001405 class="num">0.002169 class="num">0.001600 class="num">0.002595 class="num">0.002794 class="num">0.002442 class="num">0.001477 class="num">0.001190
◍ 滞后序列里藏着拐点信号
上面这段输出是某 EURUSD 小时级特征矩阵的头部与尾部切片,共 3155 行 × 11 列。第一列是时间戳,第二列是收盘价,后面 9 列是不同滞后阶的收益率或波动量。 看 2020-05-01 19:00 那行:收盘价 1.09874,最后一列滞后值 0.001477 为正,整行多数滞后项在 0.001~0.004 之间,说明当时处于温和多头惯性。到了 2020-11-02 23:00,收盘价走到 1.16404,中间多列出现负值(如 -0.001212、-0.000999),空头滞后项开始占优。 注意 2020-11-03 00:00 起,负值滞后项随行下移、新正项从右侧补入,02:00 的 0.000465 与 03:00 的 0.000885 逐步放大——这种「负项被推出窗口、正项从右边生长」的结构,往往对应下跌动量的衰减。外汇与贵金属属高风险品种,此类信号只提示反转概率上升,不代表趋势必然掉头。 在 MT5 里把这段矩阵按时间轴画成热力图,能直观看到滞后列的色块如何从冷转暖;下次你怀疑一波行情快熄火时,先翻一眼最右几列的符号变化再动手。
class="num">2020-class="num">05-class="num">01 class="num">19:class="num">00:class="num">00 class="num">1.09874 class="num">0.001577 class="num">0.002900 class="num">0.004227 class="num">0.001405 class="num">0.002169 class="num">0.001600 class="num">0.002595 class="num">0.002794 class="num">0.002442 class="num">0.001477 class="num">2020-class="num">05-class="num">01 class="num">20:class="num">00:class="num">00 class="num">1.09817 class="num">0.000759 class="num">0.001577 class="num">0.002900 class="num">0.004227 class="num">0.001405 class="num">0.002169 class="num">0.001600 class="num">0.002595 class="num">0.002794 class="num">0.002442 ... ... ... ... ... ... ... ... ... ... ... ... class="num">2020-class="num">11-class="num">02 class="num">23:class="num">00:class="num">00 class="num">1.16404 class="num">0.000400 class="num">0.000105 -class="num">0.000581 -class="num">0.001212 -class="num">0.000999 -class="num">0.000547 -class="num">0.000344 -class="num">0.000773 -class="num">0.000326 class="num">0.000501 class="num">2020-class="num">11-class="num">03 class="num">00:class="num">00:class="num">00 class="num">1.16392 class="num">0.000217 class="num">0.000400 class="num">0.000105 -class="num">0.000581 -class="num">0.001212 -class="num">0.000999 -class="num">0.000547 -class="num">0.000344 -class="num">0.000773 -class="num">0.000326 class="num">2020-class="num">11-class="num">03 class="num">01:class="num">00:class="num">00 class="num">1.16402 class="num">0.000270 class="num">0.000217 class="num">0.000400 class="num">0.000105 -class="num">0.000581 -class="num">0.001212 -class="num">0.000999 -class="num">0.000547 -class="num">0.000344 -class="num">0.000773 class="num">2020-class="num">11-class="num">03 class="num">02:class="num">00:class="num">00 class="num">1.16423 class="num">0.000465 class="num">0.000270 class="num">0.000217 class="num">0.000400 class="num">0.000105 -class="num">0.000581 -class="num">0.001212 -class="num">0.000999 -class="num">0.000547 -class="num">0.000344 class="num">2020-class="num">11-class="num">03 class="num">03:class="num">00:class="num">00 class="num">1.16464 class="num">0.000885 class="num">0.000465 class="num">0.000270 class="num">0.000217 class="num">0.000400 class="num">0.000105 -class="num">0.000581 -class="num">0.001212 -class="num">0.000999 -class="num">0.000547 [class="num">3155 rows x class="num">11 columns]
「用随机前瞻柱给样本打买卖标签」
二元分类里,模型要学的是给定一组特征后,下一手该挂买还是挂卖。标签用 0 和 1 表示:1 倾向卖出信号,0 倾向买入信号,具体由未来随机柱的收盘价相对当前位置决定。 add_labels 函数在 min 到 max 的柱数区间内随机抽一个前瞻步长,再比当前 close 与前瞻 close。当前价大于等于前瞻价就标 1,否则标 0,这样每根 K 线都带上了自己的交易方向类号。 采样频率完全由 min、max 控制。上面例子用 add_labels(pr, 10, 25),意味着每笔样本的前瞻落在 10~25 根柱之后,EURUSD 小时图里相当于 10~25 小时的前瞻窗口,标签列显示 2020-05-01 16:00 起连续四根都打了 1.0。 调小 max 会让标签更贴近短线噪声,调大则迫使模型看更远的结构。开 MT5 导出自家品种的 close 序列,把这段逻辑塞进你的特征工程脚本,先跑一遍看标签分布是否过度偏斜。
def add_labels(dataset, min, max): labels = [] for i in range(dataset.shape[class="num">0]-max): rand = random.randint(min, max) if dataset[&class="macro">#x27;close&class="macro">#x27;][i] >= (dataset[&class="macro">#x27;close&class="macro">#x27;][i + rand]): labels.append(class="num">1.0) elif dataset[&class="macro">#x27;close&class="macro">#x27;][i] <= (dataset[&class="macro">#x27;close&class="macro">#x27;][i + rand]): labels.append(class="num">0.0) else: labels.append(class="num">0.0) dataset = dataset.iloc[:len(labels)].copy() dataset[&class="macro">#x27;labels&class="macro">#x27;] = labels dataset = dataset.dropna() class="kw">return dataset >>> pr = add_labels(pr, class="num">10, class="num">25) >>> pr