使用Python和MQL5开发机器人(第一部分):数据预处理·进阶篇
🤖

使用Python和MQL5开发机器人(第一部分):数据预处理·进阶篇

(2/3)· 95%成交已是算法博弈,手工特征工程拖后腿?这篇把样本扩展与标签定义拆开讲透

实战向进阶 第 2/3 篇
很多交易者把MT5导出的裸K线直接丢进模型,结果过拟合到连回测都救不回来。数据预处理不是清洗脏数据那么简单,样本量和标签逻辑直接决定机器学习机器人能不能在活盘里活过三个月。

◍ 用合成样本把H1数据量撑起来

做价格序列预测时,样本不够会直接拖垮模型质量。数据增强的思路是在已有行情里造新样本:加噪声练抗扰、时间平移模拟不同起点、按波动缩放、再把源数据反演,四招下来既能扩容也能压误差。 实测里一段 150,000 条的 H1 价格被这套流程扩到 747,000 条,接近 5 倍。机器学习侧的大量研究指向同一结论——合成样本显著增量,对训练指标多半有正向作用,外汇与贵金属属高风险品种,回测增益不等于实盘必然生效。 下面这个函数把四种增强写在一起,传入原始 DataFrame 就能吐出拼接后的大表。逐行看:先拷贝原数据并打印行数;噪声法给副本加正态随机偏差后补均值;时间平移把索引推后数小时;缩放乘 0.9~1.1 间随机数;反演直接乘 -1;每一步都 concat 回主表并印新增量,末了按年分组列出日期。 开 MT5 导出的 H1 CSV 丢进这个函数跑一遍,对比增强前后模型在 walk-forward 里的 RMSE,可能比盲目加特征更管用。

MQL5 / C++
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 点外的真实变动,比单纯过点差更贴近可落袋收益。跑完会直接打印命中数,方便你核对样本分布。 外汇与贵金属杠杆高、滑点跳空频繁,标签阈值设错会让模型在实盘里频繁触发无效信号,回测好看不等于能活。

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

MQL5 / C++
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 把中间量算好再喂给模型。 人类靠列属性来认知物体,这套经验在自动化交易里被压缩成了「对象→特征向量→判定」的流水线。外汇与贵金属杠杆高、跳空频繁,特征若没覆盖流动性断层,模型在实盘大概率失效。

把特征逻辑交给小布盯盘校验
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到特征分布偏移与样本平衡预警,你专注调参而不是重复跑数据管道。

常见问题

Python侧的NumPy和Pandas处理多维数组与时间序列更快,且后续导出ONNX模型要求特征生成逻辑与训练时完全一致,用Python统一管道能少踩重写坑。
可以,小布盯盘的品种页内置了AIGC特征偏移提示,当新行情的统计分布偏离训练样本较多时会标记,便于你决定是否重训或暂停策略。
增强必须基于时点可得信息滚动生成,任何用到后续价量的变换都应在标签锁定后隔离,训练管道里建议显式切分时间点再调用mt5.copy_rates_range分段。
短线贵金属噪声大,概率上分类(如涨跌阈值二分)比连续回归更稳,但类别不平衡要用本篇的平衡类方法处理,否则模型倾向预测多数类。
不是,冗余特征会放大过拟合,本篇特征工程节会讲聚类和选择,把高相关的冗余列砍掉,保留信息量独立的那批。