基于暴力算法的 CatBoost 模型高级重采样与选择·综合运用
(3/3)·简单随机抽样埋下的类别失衡与自相关坑,本篇用重采样一次性填平
「多周期均线与噪声注入的重构落点」
训练脚本现在支持传入一组不同平均周期的移动均线,例如 [15, 55, 150, 250],多 MA 组合对训练结果通常有正向影响。回测起点也可配置,TSTART_DATE 设为 2015-01-01 后能从更早样本开始评估模型。 随机抽样函数新增 add_noize 参数,允许向原始数据集注入噪声。引入 0.1–0.2 级别的标签误差,虽会加大回撤、混杂交易,但可能让模型在新数据上更稳健。 点差现在被显式计入:未覆盖点差的交易标为 2.0,随后因无信息量从数据集删除。tester 函数改为返回 R^2 分数,配合主成分分析的可视化辅助函数,能帮你直观看清特征分布。 下面这段是标签与回测的核心重构,注意 fill_arrays 之后特征向量已纳入全部 MA 周期。
MA_PERIODS = [class="num">15, class="num">55, class="num">150, class="num">250] TSTART_DATE = class="type">class="kw">datetime(class="num">2015, class="num">1, class="num">1) def add_labels(dataset, min, max, add_noize = class="num">0.1): labels = [] for i in range(dataset.shape[class="num">0]-max): rand = random.randint(min, max) curr_pr = dataset[&class="macro">#x27;close&class="macro">#x27;][i] future_pr = dataset[&class="macro">#x27;close&class="macro">#x27;][i + rand] if future_pr + MARKUP < curr_pr: labels.append(class="num">1.0) elif future_pr - MARKUP > curr_pr: labels.append(class="num">0.0) else: labels.append(class="num">2.0) dataset = dataset.iloc[:len(labels)].copy() dataset[&class="macro">#x27;labels&class="macro">#x27;] = labels dataset = dataset.dropna() dataset = dataset.drop(dataset[dataset.labels == class="num">2].index).reset_index(drop=True) if add_noize==class="num">0: class="kw">return dataset # add noize to samples noize_b = dataset[dataset.labels == class="num">0][&class="macro">#x27;labels&class="macro">#x27;].sample(frac = add_noize) noize_s = dataset[dataset.labels == class="num">1][&class="macro">#x27;labels&class="macro">#x27;].sample(frac = add_noize) noize_b = noize_b+class="num">1 noize_s = noize_s-class="num">1 dataset.update(noize_b) dataset.update(noize_s) class="kw">return dataset def tester(dataset, markup = class="num">0.0, plot = False): last_deal = class="type">int(class="num">2) last_price = class="num">0.0 report = [class="num">0.0] for i in range(dataset.shape[class="num">0]): pred = dataset[&class="macro">#x27;labels&class="macro">#x27;][i] if last_deal == class="num">2: last_price = dataset[&class="macro">#x27;close&class="macro">#x27;][i] last_deal = class="num">0 if pred <= class="num">0.5 else class="num">1 class="kw">continue if last_deal == class="num">0 and pred > class="num">0.5: last_deal = class="num">1 report.append(report[-class="num">1] - markup + (dataset[&class="macro">#x27;close&class="macro">#x27;][i] - last_price)) last_price = dataset[&class="macro">#x27;close&class="macro">#x27;][i] class="kw">continue if last_deal == class="num">1 and pred < class="num">0.5: last_deal = class="num">0 report.append(report[-class="num">1] - markup + (last_price - dataset[&class="macro">#x27;close&class="macro">#x27;][i])) last_price = dataset[&class="macro">#x27;close&class="macro">#x27;][i] y = np.array(report).reshape(-class="num">1,class="num">1) X = np.arange(len(report)).reshape(-class="num">1,class="num">1) lr = LinearRegression() lr.fit(X,y) l = lr.coef_ if l >= class="num">0:
◍ 把训练好的模型导出成 MT5 可跑的 C 头文件
上面这段 Python 负责把 CatBoost 模型固化成 C++ 头文件(catmodel.h),再拼出一段 MQL5 骨架代码,让 MT5 直接调用。核心动作就两个:模型用 save_model 以 cpp 格式落地,随后用字符串拼接生成 OnInit 与特征填充函数。 LOOK_BACK 与 MA_PERIODS 是外部常量,前者决定回看根数,后者是一组均线周期。代码里 ArrayResize(hnd, len(MA_PERIODS)) 按周期数量开句柄数组,循环里对每个周期调 iMA 拿 SMA 收盘价句柄,计数从尾到头递减避免错位。 fill_arays 里用 CopyClose 取最近 look_back 根收盘价,再对每个均线句柄 CopyBuffer 取同样长度数值,逐根算 pr[f]-ma[f] 作为特征,最后 ArrayInsert 纵向拼进 features 并设为序列数组。外汇与贵金属波动剧烈,这套特征构造只是概率性信号源,实盘前务必在 MT5 策略测试器用历史数据验证过拟合风险。 导出后把 catmodel.h 丢进 MQL5/Include,EA 里 include 后即可在 OnTick 调模型推理。建议先拿 EURUSD 的 H1 数据跑一遍,观察信号延迟是否超过 2 根 K 线再决定跟单逻辑。
l = class="num">1 else: l = -class="num">1 if(plot): plt.plot(report) plt.show() class="kw">return lr.score(X,y) * l def pca_plot(data): from sklearn.decomposition class="kw">import PCA pca = PCA(n_components = class="num">5) components = pd.DataFrame(pca.fit_transform(data[data.columns[class="num">1:-class="num">1]])) components[&class="macro">#x27;labels&class="macro">#x27;] = data[&class="macro">#x27;labels&class="macro">#x27;].reset_index(drop = True) class="kw">import seaborn as sns g = sns.PairGrid(components, hue="labels", height=class="num">1.2) g.map_diag(sns.histplot) g.map_offdiag(sns.scatterplot) g.add_legend() plt.show() def export_model_to_MQL_code(model): model.save_model(&class="macro">#x27;catmodel.h&class="macro">#x27;, format="cpp", export_parameters=None, pool=None) # add variables code = &class="macro">#x27;class="type">int &class="macro">#x27; + &class="macro">#x27;loock_back = &class="macro">#x27; + str(LOOK_BACK) + &class="macro">#x27;;\n&class="macro">#x27; code += &class="macro">#x27;class="type">int hnd[];\n&class="macro">#x27; code += &class="macro">#x27;class="type">int OnInit() {\n&class="macro">#x27; code += &class="macro">#x27;ArrayResize(hnd,&class="macro">#x27; + str(len(MA_PERIODS)) + &class="macro">#x27;);\n&class="macro">#x27; count = len(MA_PERIODS) - class="num">1 for i in MA_PERIODS: code += &class="macro">#x27;hnd[&class="macro">#x27; + str(count) + &class="macro">#x27;]&class="macro">#x27; + &class="macro">#x27; =&class="macro">#x27; + &class="macro">#x27; iMA(NULL,PERIOD_CURRENT,&class="macro">#x27; + str(i) + &class="macro">#x27;,class="num">0,MODE_SMA,PRICE_CLOSE);\n&class="macro">#x27; count -= class="num">1 code += &class="macro">#x27;class="kw">return(INIT_SUCCEEDED);\n&class="macro">#x27; code += &class="macro">#x27;}\n\n&class="macro">#x27; # get features code += &class="macro">#x27;class="type">void fill_arays(class="type">int look_back, class="type">class="kw">double &features[]) {\n&class="macro">#x27; code += &class="macro">#x27; class="type">class="kw">double ma[], pr[], ret[];\n&class="macro">#x27; code += &class="macro">#x27; ArrayResize(ret,&class="macro">#x27; + str(LOOK_BACK) +&class="macro">#x27;);\n&class="macro">#x27; code += &class="macro">#x27; CopyClose(NULL,PERIOD_CURRENT,class="num">1,look_back,pr);\n&class="macro">#x27; code += &class="macro">#x27; for(class="type">int i=class="num">0;i<&class="macro">#x27; + str(len(MA_PERIODS)) +&class="macro">#x27;;i++) {\n&class="macro">#x27; code += &class="macro">#x27; CopyBuffer(hnd[&class="macro">#x27; + &class="macro">#x27;i&class="macro">#x27; + &class="macro">#x27;], class="num">0, class="num">1, look_back, ma);\n&class="macro">#x27; code += &class="macro">#x27; for(class="type">int f=class="num">0;f<&class="macro">#x27; + str(LOOK_BACK) +&class="macro">#x27;;f++)\n&class="macro">#x27; code += &class="macro">#x27; ret[f] = pr[f] - ma[f];\n&class="macro">#x27; code += &class="macro">#x27; ArrayInsert(features, ret, ArraySize(features), class="num">0, WHOLE_ARRAY); }\n&class="macro">#x27; code += &class="macro">#x27; ArraySetAsSeries(features, true);\n&class="macro">#x27; code += &class="macro">#x27;}\n\n&class="macro">#x27;
画得少,看得清
用混合高斯模型对原始行情做重采样,本质是用最省力的方式把特征空间拉正,再喂给 CatBoost 分类器。实测里,这种迭代重采样路线在 4.38 KB 的 cat_trader.mq5 配套逻辑下,能把新数据上的分类表现拉起来,但外汇与贵金属杠杆高、滑点跳空频繁,模型过拟合过去只是概率优势而非确定性。 你不需要从头写 Python 训练栈,MQL5 端加载 157.06 KB 的 cat_model.mqh 就能直接跑推理;改 train 起止时间若回测变烂,大概率是样本区间动量结构变了,而不是代码废了。 真正划算的突破,是花最少精力把「初级模型」推到「能上实盘观察」的边界。剩下的,交给 MT5 逐根 K 线去验证。