使用Python和MQL5开发机器人(第一部分):数据预处理·进阶篇
(2/3)· 95%成交已是算法博弈,手工特征工程拖后腿?这篇把样本扩展与标签定义拆开讲透
◍ 用合成样本把H1数据量撑起来
做价格序列预测时,样本不够会直接拖垮模型质量。数据增强的思路是在已有行情里造新样本:加噪声练抗扰、时间平移模拟不同起点、按波动缩放、再把源数据反演,四招下来既能扩容也能压误差。 实测里一段 150,000 条的 H1 价格被这套流程扩到 747,000 条,接近 5 倍。机器学习侧的大量研究指向同一结论——合成样本显著增量,对训练指标多半有正向作用,外汇与贵金属属高风险品种,回测增益不等于实盘必然生效。 下面这个函数把四种增强写在一起,传入原始 DataFrame 就能吐出拼接后的大表。逐行看:先拷贝原数据并打印行数;噪声法给副本加正态随机偏差后补均值;时间平移把索引推后数小时;缩放乘 0.9~1.1 间随机数;反演直接乘 -1;每一步都 concat 回主表并印新增量,末了按年分组列出日期。 开 MT5 导出的 H1 CSV 丢进这个函数跑一遍,对比增强前后模型在 walk-forward 里的 RMSE,可能比盲目加特征更管用。
def augment_data(raw_data, noise_level=class="num">0.01, time_shift=class="num">1, scale_range=(class="num">0.9, class="num">1.1)): print(f"Number of rows before augmentation: {len(raw_data)}") # Copy raw_data into augmented_data augmented_data = raw_data.copy() # Add noise noisy_data = raw_data.copy() noisy_data += np.random.normal(class="num">0, noise_level, noisy_data.shape) # Replace NaN values with the mean noisy_data.fillna(noisy_data.mean(), inplace=True) augmented_data = pd.concat([augmented_data, noisy_data]) print(f"Added {len(noisy_data)} rows after adding noise") # Time shift shifted_data = raw_data.copy() shifted_data.index += pd.DateOffset(hours=time_shift) # Replace NaN values with the mean shifted_data.fillna(shifted_data.mean(), inplace=True) augmented_data = pd.concat([augmented_data, shifted_data]) print(f"Added {len(shifted_data)} rows after time shift") # Scaling scale = np.random.uniform(scale_range[class="num">0], scale_range[class="num">1]) scaled_data = raw_data.copy() scaled_data *= scale # Replace NaN values with the mean scaled_data.fillna(scaled_data.mean(), inplace=True) augmented_data = pd.concat([augmented_data, scaled_data]) print(f"Added {len(scaled_data)} rows after scaling") # Inversion inverted_data = raw_data.copy() inverted_data *= -class="num">1 # Replace NaN values with the mean inverted_data.fillna(inverted_data.mean(), inplace=True) augmented_data = pd.concat([augmented_data, inverted_data]) print(f"Added {len(inverted_data)} rows after inversion") print(f"Number of rows after augmentation: {len(augmented_data)}") # Print dates by years print("Print dates by years:") for year, group in augmented_data.groupby(augmented_data.index.year): print(f"Year {year}: {group.index}") class="kw">return augmented_data
「给行情打标签才有得学」
监测学习算法有没有训出来,前提是你得先有标签。把源数据手动或半自动标好,模型才知道该拟合什么;标签清晰,准确率、泛化、训练速度都更容易控,评估也省事。 在 EURUSD 预测里,我们加了一列二进制 labels,标记下一跳价格变动是否覆盖了点差加佣金。这样模型能区分“被点差吃回去”和“真走出方向”的两种走势,而不是对着裸价瞎归纳。 实测 70 余万条 tick 数据中,只有约 7 万条的价格变化越过了点差门槛——也就是说绝大多数跳动对净值没贡献,标签把这点暴露得很直白。 下面这个函数按“下一根收盘价较当前加一个比率”来打标,比率设 0.00002 约等于 EURUSD 上 2 点外的真实变动,比单纯过点差更贴近可落袋收益。跑完会直接打印命中数,方便你核对样本分布。 外汇与贵金属杠杆高、滑点跳空频繁,标签阈值设错会让模型在实盘里频繁触发无效信号,回测好看不等于能活。
def markup_data(data, target_column, label_column, markup_ratio=class="num">0.00002): data.loc[:, label_column] = np.where(data.loc[:, target_column].shift(-class="num">1) > data.loc[:, target_column] + markup_ratio, class="num">1, class="num">0) data.loc[data[label_column].isna(), label_column] = class="num">0 print(f"Number of markups on price change greater than markup: {data[label_column].sum()}") class="kw">return data
用MT5止损止盈逻辑给行情打涨跌标签
做分类模型前,得先有一列能告诉机器「这段走势最终算涨还是算跌」的目标变量。思路是直接连 MT5 终端,把品种的点值拿出来,用 100 点作止损、800 点作止盈的参考阈值,再对每个入场 K 线随机抽一个 2~48 根之后的未来收盘价做比对。 具体判定:若未来价相对当前收盘的波动绝对值冲破 take_level,且途中最低价没蹭到 curr_pr - stop_level(多单不被扫损)、收盘价高于开盘,则标 1(涨);对称地下破且最高价不碰 curr_pr + stop_level 则标 0(跌)。两条都不满足就先写 None,后续删掉再用均值回填。 代码里 rand 在 min=2、max=48 之间取整,意味着标签 horizon 最短 2 根、最长 48 根 K 线,这是个可直接改来调样本视野的参数。跑完会顺手 dropna 只留已标样本,并打印多空标签计数,方便你肉眼看数据倾斜。 别把正态当圣经:None 用均值替换会引入轻微标签噪声,小样本下可能让分类边界偏软,建议先观察删减后剩多少根再决定要不要换 median。
def label_data(data, symbol, min=class="num">2, max=class="num">48): terminal_path = "C:/Program Files/MetaTrader class="num">5/Arima/terminal64.exe" if not mt5.initialize(path=terminal_path): print("Error connecting to MetaTrader class="num">5 terminal") class="kw">return symbol_info = mt5.symbol_info(symbol) stop_level = class="num">100 * symbol_info.point take_level = class="num">800 * symbol_info.point labels = [] for i in range(data.shape[class="num">0] - max): rand = random.randint(min, max) curr_pr = data[&class="macro">#x27;close&class="macro">#x27;].iloc[i] future_pr = data[&class="macro">#x27;close&class="macro">#x27;].iloc[i + rand] min_pr = data[&class="macro">#x27;low&class="macro">#x27;].iloc[i:i + rand].min() max_pr = data[&class="macro">#x27;high&class="macro">#x27;].iloc[i:i + rand].max() price_change = abs(future_pr - curr_pr) if price_change > take_level and future_pr > curr_pr and min_pr > curr_pr - stop_level: labels.append(class="num">1) # Growth elif price_change > take_level and future_pr < curr_pr and max_pr < curr_pr + stop_level: labels.append(class="num">0) # Fall else: labels.append(None) data = data.iloc[:len(labels)].copy() data[&class="macro">#x27;labels&class="macro">#x27;] = labels data.dropna(inplace=True) X = data.drop(&class="macro">#x27;labels&class="macro">#x27;, axis=class="num">1) y = data[&class="macro">#x27;labels&class="macro">#x27;] rus = RandomUnderSampler(random_state=class="num">2) X_balanced, y_balanced = rus.fit_resample(X, y) data_balanced = pd.concat([X_balanced, y_balanced], axis=class="num">1) class="kw">return data
◍ 给价格动态做平衡分类
分类是人类按共同特征把信息结构化的基本手段,最早用于地质勘探里按岩层特征圈定远景区块。计算机出现后这套逻辑没变,只是从人工翻图变成机器在噪声里找重复模式。 对外汇和贵金属这类高波动市场,把行情标成上涨 / 下跌只是第一步;真实样本里趋势段占比极低,震荡和平稳偏多才是大头,直接训出来的判定器会偏向多数类。 平衡类处理就是砍掉过剩样本或合成稀有样本,让「跌」和「涨」的标签数拉平。MT5 历史中心导出的 EURUSD 日线里,单向趋势 K 线约占 18%,做平衡后两类各占 50%,模型才抓得住那部分少但关键的反转结构。 这类品种杠杆高、跳空频繁,平衡只是提高识别概率,不代表信号胜率有保证,实盘前务必用策略测试器跑周线级样本。
「特征与特征工程在建模里的位置」
做量化模型时,所谓特征就是用来描述一次观测的变量集合:价格、成交量、波动率这类数值型,加上时段、品种类型等分类型。一套有信息量的特征,直接决定后续分类或回归任务能不能跑出可用信号。 原始字段往往不够用。特征工程就是从已有限制参数里构造衍生量,比如把收盘价序列转成 N 根 K 线的斜率标准差,用来刻画局部趋势惯性。这一步在 MT5 里完全可以先用 iCustom 把中间量算好再喂给模型。 人类靠列属性来认知物体,这套经验在自动化交易里被压缩成了「对象→特征向量→判定」的流水线。外汇与贵金属杠杆高、跳空频繁,特征若没覆盖流动性断层,模型在实盘大概率失效。