因果推断与量化交易中的回归模型探索(基础篇)
「回归模型在MT5里的因果陷阱」
在 MetaTrader 5 上做量化,很多人第一反应是挂一个线性回归去拟合价格,再反过来推断「因为指标动了,所以价格会跟」。但统计上的相关性不等于因果,回归系数显著只说明样本内联动,样本外大概率塌房。
- 年 7 月 17 日一篇社区帖拿到 64 次浏览、15 条互动,讨论的正是这类用回归做因果推断的坑——可见实盘圈早就开始警惕「拟合即真理」的错觉。
真要落地,先在 MT5 策略测试器里把回归窗口做成可变参数,用不同品种(如 XAUUSD、EURUSD)各跑一轮 2020–2025 年数据,看 R² 在样本外衰减有多快。外汇与贵金属杠杆高、跳空频繁,回归残差往往非正态,结论只能说「可能倾向反转」,绝不能当确定性信号。
◍ 为什么二分类 EA 容易赚对方向亏了钱
二分类把市场压成 0/1:在 MT5 的 EA 里常见写法是预测买入标 0、卖出标 1,模型只回答“往哪边”。这省事,但把仓位管理最需要的“动多少”删掉了。 核心盲区在幅度。分类器说涨,没说涨 5 点还是 50 点;说跌,没说回撤深度。外汇和贵金属杠杆高、滑点跳空频繁,这种幅度缺失会直接把决策引向亏损——尤其黄金秒级波动常超 20 点,方向对但止损扛不住的情况很普遍。 一个反直觉的数据点:胜率 70% 可能净亏,胜率 30% 也可能净赚。决定 PnL 的是盈利单均值/亏损单均值,不是猜对次数。二分类对所有“猜对”一视同仁,小波动蹭头和趋势大行情被同等对待,结果常是大量小赚被少数大亏吃掉。 对实盘而言,这意味着光看方向准确率挑 EA 是危险的。没有波动幅度估计,EA 没法优先做盈亏比优的单,也避不开亏大于盈的坑,方向胜率再高也填不平决策盲区。
回归标注怎么绕过分类模型的幅度盲区
二分类标注只给方向信号:涨或跌,等价处理所有波动。实际盘面上,未来偏移 5 点和 50 点在外汇与贵金属里完全是两种风险收益结构,固定标注会把噪声和真实趋势混为一谈。 回归标注直接算差值:未来价格减当前价格,把幅度也编码进目标变量。这样训练出的模型能顺带筛掉低于阈值的小幅波动,只留有可能覆盖点差和滑点的有效行情,贵金属隔夜跳空时这点尤其关键。 初版 calculate_labels_r 在 [min_val, max_val] 里随机抽一个未来点算差,速度靠 Numba JIT 提上来了,但漏掉了窗口内的中间路径。改进版 calculate_labels_mean_r 改成取窗口内全部收盘价均值再减当前价,相当于把区间波动摊平,标注对毛刺更不敏感。 两个 get_labels_r 除调用函数不同,其余裁剪、填列、去 NaN 逻辑一致。把 min=1、max=15 接进自己的 K 线序列,分别跑两版标注,对比标签分布标准差,能直观看出均值法是否压住了随机抽点带来的离散。外汇与贵金属杠杆高,标注再漂亮也只代表历史概率,实盘前务必小仓位验证。
@njit def calculate_labels_r(close_data, min_val, max_val): labels = [] for i in range(len(close_data) - max_val): rand = random.randint(min_val, max_val) labels.append(close_data[i + rand] - close_data[i]) class="kw">return labels def get_labels_r(dataset, min = class="num">1, max = class="num">15) -> pd.DataFrame: # Extract closing prices from the dataset close_data = dataset[&class="macro">#x27;close&class="macro">#x27;].values labels = calculate_labels_r(close_data, min, max) # Trim the dataset to match the length of calculated labels dataset = dataset.iloc[:len(labels)].copy() # Add the calculated labels as a new column dataset[&class="macro">#x27;labels&class="macro">#x27;] = labels # Remove rows with NaN values(potentially introduced in &class="macro">#x27;calculate_labels&class="macro">#x27;) dataset = dataset.dropna() class="kw">return dataset @njit def calculate_labels_mean_r(close_data, min_val, max_val): labels = [] for i in range(len(close_data) - max_val): # Calculate the average price value in the window from min_val to max_val future_prices = close_data[i + min_val : i + max_val + class="num">1] mean_future_price = np.mean(future_prices) # Calculate the difference between the average future value and the current price labels.append(mean_future_price - close_data[i]) class="kw">return labels def get_labels_r(dataset, min = class="num">1, max = class="num">15) -> pd.DataFrame: # Extract closing prices from the dataset close_data = dataset[&class="macro">#x27;close&class="macro">#x27;].values # Calculate buy/hold labels based on future price movements labels = calculate_labels_mean_r(close_data, min, max) # Trim the dataset to match the length of calculated labels dataset = dataset.iloc[:len(labels)].copy() # Add the calculated labels as a new column dataset[&class="macro">#x27;labels&class="macro">#x27;] = labels # Remove rows with NaN values(potentially introduced in &class="macro">#x27;calculate_labels&class="macro">#x27;) dataset = dataset.dropna() class="kw">return dataset
「用回归偏差过滤无效信号」
把因果推断思路接进预测流程时,这里换成了回归模型而不是分类模型。核心动作是在原始数据的随机子样本上训多个回归器,再把真实目标值和模型预测值放一起比,不再输出‘做不做交易’的0/1,而是吐出预测值相对真实值的平均偏差。 偏差过大往往意味着这套特征组合在该样本上失效,拿平均偏差当门槛就能把这类无效预测筛掉。外汇和贵金属波动受突发事件干扰明显,这种过滤只能降低噪声,不保证胜率,杠杆品种高风险依旧。 下面这段 Python 风格函数可直接在本地复现元学习过滤逻辑:
<span class="keyword">def</span> meta_learners(data, models_number: <span class="built_in">class="type">int</span>, iterations: <span class="built_in">class="type">int</span>, depth: <span class="built_in">class="type">int</span>): data = data.copy() data = data[(data.index < hyper_params[<span class="class="type">class="kw">string">&class="macro">#x27;forward&class="macro">#x27;</span>]) & (data.index > hyper_params[<span class="class="type">class="kw">string">&class="macro">#x27;backward&class="macro">#x27;</span>])].copy() X = data[data.columns[<span class="number">class="num">1</span>:-<span class="number">class="num">1</span>]] y = data[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] = <span class="number">class="num">0</span> <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(models_number): X_train, X_val, y_train, y_val = train_test_split( X, y, train_size = <span class="number">class="num">0.5</span>, test_size = <span class="number">class="num">0.5</span>, shuffle = <span class="literal">True</span>) <span class="comment"># fit debias model with train and validation subsets</span> meta_m = <b>CatBoostRegressor</b>(iterations = iterations, depth = depth, verbose = <span class="literal">False</span>, use_best_model = <span class="literal">True</span>) meta_m.fit(X_train, y_train, eval_set = (X_val, y_val), plot = <span class="literal">False</span>) coreset = X.copy() coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] = y coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels_pred&class="macro">#x27;</span>] = meta_m.predict(X) <b> data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] += <span class="built_in">abs</span>(coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] - coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels_pred&class="macro">#x27;</span>]) </b> <b> data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] = data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] / models_number </b> <span class="keyword">class="kw">return</span> data
<span class="keyword">def</span> meta_learners(data, models_number: <span class="built_in">class="type">int</span>, iterations: <span class="built_in">class="type">int</span>, depth: <span class="built_in">class="type">int</span>): data = data.copy() data = data[(data.index < hyper_params[<span class="class="type">class="kw">string">&class="macro">#x27;forward&class="macro">#x27;</span>]) & (data.index > hyper_params[<span class="class="type">class="kw">string">&class="macro">#x27;backward&class="macro">#x27;</span>])].copy() X = data[data.columns[<span class="number">class="num">1</span>:-<span class="number">class="num">1</span>]] y = data[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] = <span class="number">class="num">0</span> <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(models_number): X_train, X_val, y_train, y_val = train_test_split( X, y, train_size = <span class="number">class="num">0.5</span>, test_size = <span class="number">class="num">0.5</span>, shuffle = <span class="literal">True</span>) <span class="comment"># fit debias model with train and validation subsets</span> meta_m = <b>CatBoostRegressor</b>(iterations = iterations, depth = depth, verbose = <span class="literal">False</span>, use_best_model = <span class="literal">True</span>) meta_m.fit(X_train, y_train, eval_set = (X_val, y_val), plot = <span class="literal">False</span>) coreset = X.copy() coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] = y coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels_pred&class="macro">#x27;</span>] = meta_m.predict(X) <b> data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] += <span class="built_in">abs</span>(coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] - coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels_pred&class="macro">#x27;</span>]) </b> <b> data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] = data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] / models_number </b> <span class="keyword">class="kw">return</span> data