价格走势角度分析:用于预测金融市场的混合模型·进阶篇
(2/3)·从纸质图表的模糊角线到机器学习可读的连续价格角度,差的不是经验而是算法
「给角度序列贴上未来标签」
做价格角度的机器学习,第一步不是训模型,而是先把‘未来’写进表里。下面这段 Python 把每个 bar 往后推 24 根(prediction_period=24),比较那时的 close 与当前 close:涨了标 1,跌了标 0,塞进 future_direction 列;同时算百分比变动 future_change_pct,正为负都留痕。 标注逻辑很直白:够不到 24 根未来的样本直接填 np.nan,不强行猜。外汇与贵金属波动受杠杆放大,24 根 H1 就是一天,这种中短窗在黄金上常出现 0.3%–1.2% 的 future_change_pct 跨度,但方向标签噪声也不小。 prepare_features 接着用 lookback=15 滑窗,把连续 15 个 angle 摊成 angle_0…angle_14 的特征字典,再补 angle_mean、angle_std、angle_min 等衍生量。样本不足 lookback 就直接返回 None,避免在 MT5 导出的短样本上瞎跑。 小布盯盘里若接了这套角度流,建议先把 lookback 从 15 调到 30 试一周,看黄金 M15 上 future_direction 的标签翻转频率是否降下来。
def add_future_price_info(angular_df, prediction_period=class="num">24): # Add future price direction future_directions = [] for i in range(len(angular_df)): if i + prediction_period < len(angular_df): # class="num">1 = growth, class="num">0 = fall future_dir = class="num">1 if angular_df[&class="macro">#x27;close&class="macro">#x27;].iloc[i + prediction_period] > angular_df[&class="macro">#x27;close&class="macro">#x27;].iloc[i] else class="num">0 future_directions.append(future_dir) else: future_directions.append(np.nan) angular_df[&class="macro">#x27;future_direction&class="macro">#x27;] = future_directions # Calculate magnitude of the future change(in percent) future_changes = [] for i in range(len(angular_df)): if i + prediction_period < len(angular_df): pct_change = (angular_df[&class="macro">#x27;close&class="macro">#x27;].iloc[i + prediction_period] - angular_df[&class="macro">#x27;close&class="macro">#x27;].iloc[i]) / angular_df[&class="macro">#x27;close&class="macro">#x27;].iloc[i] * class="num">100 future_changes.append(pct_change) else: future_changes.append(np.nan) angular_df[&class="macro">#x27;future_change_pct&class="macro">#x27;] = future_changes class="kw">return angular_df def prepare_features(angular_df, lookback=class="num">15): features = [] targets_class = [] # For classification(direction) targets_reg = [] # For regression(percent change) # Discard strings with NaN filtered_df = angular_df.dropna(subset=[&class="macro">#x27;angle&class="macro">#x27;, &class="macro">#x27;future_direction&class="macro">#x27;, &class="macro">#x27;future_change_pct&class="macro">#x27;]) # Check if there is enough data if len(filtered_df) <= lookback: print("Not enough data for analysis") class="kw">return None, None, None for i in range(lookback, len(filtered_df)): # Get latest lookback of bars window = filtered_df.iloc[i-lookback:i] # Take last angles as a sequence feature_dict = { f&class="macro">#x27;angle_{j}&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].iloc[j] for j in range(lookback) } # Add derivative characteristics of angles feature_dict.update({ &class="macro">#x27;angle_mean&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].mean(), &class="macro">#x27;angle_std&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].std(), &class="macro">#x27;angle_min&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].min(),
窗口特征怎么一次性抽出来
这段逻辑干的事很直接:在一个滚动窗口里把角度、价格、成交量三类特征一次性算完,再塞进列表等模型消费。角度部分除了取窗口内最大值,还分别算了最后1根、最近3根、5根、10根均值,等于同时看瞬时斜率和短期斜率惯性。 positive_angles_ratio 用 (angle>0).mean() 给出窗口内正角度占比,能快速判断这段走势是多头倾斜主导还是空头倾斜主导。价格端给了标准差、区间涨跌幅和高低价相对收盘的区间百分比,波动与空间一眼可辨。 成交量特征里 tick_volume_ratio 做了除零保护:均值大于0才除,否则返回1,避免回测时直接崩。最后三行把特征字典追加进 features,并把未来方向、未来涨跌幅分别写入分类与回归目标数组,函数整体 return 出 DataFrame 和两只 numpy 数组,可直接喂给 sklearn。 开 MT5 导出 tick 数据跑一遍这段,重点看 angle_last_10_mean 与 positive_angles_ratio 在同周期黄金15分钟图上的分布,外汇与贵金属杠杆高,信号仅作概率参考。
&class="macro">#x27;angle_max&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].max(), &class="macro">#x27;angle_last&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].iloc[-class="num">1], &class="macro">#x27;angle_last_3_mean&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].iloc[-class="num">3:].mean(), &class="macro">#x27;angle_last_5_mean&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].iloc[-class="num">5:].mean(), &class="macro">#x27;angle_last_10_mean&class="macro">#x27;: window[&class="macro">#x27;angle&class="macro">#x27;].iloc[-class="num">10:].mean(), &class="macro">#x27;positive_angles_ratio&class="macro">#x27;: (window[&class="macro">#x27;angle&class="macro">#x27;] > class="num">0).mean(), &class="macro">#x27;current_price&class="macro">#x27;: window[&class="macro">#x27;close&class="macro">#x27;].iloc[-class="num">1], &class="macro">#x27;price_std&class="macro">#x27;: window[&class="macro">#x27;close&class="macro">#x27;].std(), &class="macro">#x27;price_change_pct&class="macro">#x27;: (window[&class="macro">#x27;close&class="macro">#x27;].iloc[-class="num">1] - window[&class="macro">#x27;close&class="macro">#x27;].iloc[class="num">0]) / window[&class="macro">#x27;close&class="macro">#x27;].iloc[class="num">0] * class="num">100, &class="macro">#x27;high_low_range&class="macro">#x27;: (window[&class="macro">#x27;high&class="macro">#x27;].max() - window[&class="macro">#x27;low&class="macro">#x27;].min()) / window[&class="macro">#x27;close&class="macro">#x27;].iloc[-class="num">1] * class="num">100, &class="macro">#x27;last_tick_volume&class="macro">#x27;: window[&class="macro">#x27;tick_volume&class="macro">#x27;].iloc[-class="num">1], &class="macro">#x27;avg_tick_volume&class="macro">#x27;: window[&class="macro">#x27;tick_volume&class="macro">#x27;].mean(), &class="macro">#x27;tick_volume_ratio&class="macro">#x27;: window[&class="macro">#x27;tick_volume&class="macro">#x27;].iloc[-class="num">1] / window[&class="macro">#x27;tick_volume&class="macro">#x27;].mean() if window[&class="macro">#x27;tick_volume&class="macro">#x27;].mean() > class="num">0 else class="num">1, }) features.append(feature_dict) targets_class.append(filtered_df.iloc[i][&class="macro">#x27;future_direction&class="macro">#x27;]) targets_reg.append(filtered_df.iloc[i][&class="macro">#x27;future_change_pct&class="macro">#x27;]) class="kw">return pd.DataFrame(features), np.array(targets_class), np.array(targets_reg)
◍ 双模型拆解价格角度的涨跌与幅度
把角度特征喂给模型时,单用一个网络往往只能猜方向,幅度信息就丢了。这里拆成两个 CatBoost 模型并行跑:分类器判涨跌方向,回归器估后续波动的百分比幅度,两者拼起来才是一个带概率和量级的标准信号。 训练函数里数据按 0.3 比例切分测试集,random_state 锁死 42 保证可复现;两个模型都设 500 轮、学习率 0.03、深度 6,分类用 Logloss、回归用 RMSE,早停轮数 50。跑完会直接打印分类准确率和回归 RMSE,并把特征重要性前 5 行吐出来。 实盘调用时,predict_future_movement 默认取最近 15 根的角度数据做推理。外汇与贵金属杠杆高、滑点突发行情多,模型输出仅代表历史模式下的倾向概率,不代表未来必现,开 MT5 接自己的品种前先跑一轮回测看准确率掉没掉。
from catboost class="kw">import CatBoostClassifier, CatBoostRegressor from sklearn.model_selection class="kw">import train_test_split from sklearn.metrics class="kw">import accuracy_score, mean_squared_error def train_hybrid_model(X, y_class, y_reg, test_size=class="num">0.3): # Splitting data into training and test X_train, X_test, y_class_train, y_class_test, y_reg_train, y_reg_test = train_test_split( X, y_class, y_reg, test_size=test_size, random_state=class="num">42, shuffle=True ) # Parameters for classification model params_class = { &class="macro">#x27;iterations&class="macro">#x27;: class="num">500, &class="macro">#x27;learning_rate&class="macro">#x27;: class="num">0.03, &class="macro">#x27;depth&class="macro">#x27;: class="num">6, &class="macro">#x27;loss_function&class="macro">#x27;: &class="macro">#x27;Logloss&class="macro">#x27;, &class="macro">#x27;random_seed&class="macro">#x27;: class="num">42, &class="macro">#x27;verbose&class="macro">#x27;: False } # Parameters for regression model params_reg = { &class="macro">#x27;iterations&class="macro">#x27;: class="num">500, &class="macro">#x27;learning_rate&class="macro">#x27;: class="num">0.03, &class="macro">#x27;depth&class="macro">#x27;: class="num">6, &class="macro">#x27;loss_function&class="macro">#x27;: &class="macro">#x27;RMSE&class="macro">#x27;, &class="macro">#x27;random_seed&class="macro">#x27;: class="num">42, &class="macro">#x27;verbose&class="macro">#x27;: False } # Training classification model(directional prediction) print("Training classification model...") model_class = CatBoostClassifier(**params_class) model_class.fit(X_train, y_class_train, eval_set=(X_test, y_class_test), early_stopping_rounds=class="num">50, verbose=False) # Checking classification accuracy y_class_pred = model_class.predict(X_test) accuracy = accuracy_score(y_class_test, y_class_pred) print(f"Classification accuracy: {accuracy:.4f} ({accuracy*class="num">100:.2f}%)") # Training regression model(forecast of percentage change) print("\nTraining regression model...") model_reg = CatBoostRegressor(**params_reg) model_reg.fit(X_train, y_reg_train, eval_set=(X_test, y_reg_test), early_stopping_rounds=class="num">50, verbose=False) # Checking regression accuracy y_reg_pred = model_reg.predict(X_test) rmse = np.sqrt(mean_squared_error(y_reg_test, y_reg_pred)) print(f"RMSE regressions: {rmse:.4f}") # Print importance of features print("\nImportance of features for classification:") feature_importance = model_class.get_feature_importance(prettified=True) print(feature_importance.head(class="num">5)) class="kw">return model_class, model_reg def predict_future_movement(model_class, model_reg, angular_df, lookback=class="num">15): # Get latest data
「把角度序列喂给模型前的特征拼装」
预测之前先卡一道数据量门槛:若可用 K 线不足 lookback 根,直接打印提示并返回 None,避免后续切片越界。这一步在外汇与贵金属这种高波动品种上尤其必要,样本不够时模型输出可信度倾向骤降。 拿到最近 lookback 根 K 线后,先把每根的角度按索引展平成 angle_0 到 angle_{lookback-1} 的扁平特征,保证和训练时的输入维度完全一致。任何维度错位都会让 sklearn 模型在 predict 时直接抛异常。 衍生统计量是这套特征工程的核心:除了 angle_mean/std/min/max,还单独取了最近 3、5、10 根的角度均值,用来刻画短期斜率的加速或钝化。positive_angles_ratio 给出窗口内正角度占比,若接近 1 说明多头推力连续占优,可能暗示趋势延续。 价格侧补了 current_price、price_std、price_change_pct(首尾收盘价百分比差)以及 high_low_range(高低极差占末价百分比)。成交量侧用 last_tick_volume 与 avg_tick_volume 的比值捕捉放量异动——比值显著大于 1 时,价格突破倾向更值得关注,但外汇杠杆交易高风险,仍需结合止损。 最后把字典转成单行 DataFrame 送进分类与回归模型:分类给方向概率与 UP/DOWN 标签,回归给 change_pct 幅度预估。注意 probability 取的是预测类别对应的那一项,不是固定取索引 1。
if len(angular_df) < lookback: print("Not enough data for forecast") class="kw">return None # Get latest lookback of bars last_window = angular_df.tail(lookback) # Form features as during training feature_dict = { f&class="macro">#x27;angle_{j}&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].iloc[j] for j in range(lookback) } # Add derivative characteristics feature_dict.update({ &class="macro">#x27;angle_mean&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].mean(), &class="macro">#x27;angle_std&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].std(), &class="macro">#x27;angle_min&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].min(), &class="macro">#x27;angle_max&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].max(), &class="macro">#x27;angle_last&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].iloc[-class="num">1], &class="macro">#x27;angle_last_3_mean&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].iloc[-class="num">3:].mean(), &class="macro">#x27;angle_last_5_mean&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].iloc[-class="num">5:].mean(), &class="macro">#x27;angle_last_10_mean&class="macro">#x27;: last_window[&class="macro">#x27;angle&class="macro">#x27;].iloc[-class="num">10:].mean(), &class="macro">#x27;positive_angles_ratio&class="macro">#x27;: (last_window[&class="macro">#x27;angle&class="macro">#x27;] > class="num">0).mean(), &class="macro">#x27;current_price&class="macro">#x27;: last_window[&class="macro">#x27;close&class="macro">#x27;].iloc[-class="num">1], &class="macro">#x27;price_std&class="macro">#x27;: last_window[&class="macro">#x27;close&class="macro">#x27;].std(), &class="macro">#x27;price_change_pct&class="macro">#x27;: (last_window[&class="macro">#x27;close&class="macro">#x27;].iloc[-class="num">1] - last_window[&class="macro">#x27;close&class="macro">#x27;].iloc[class="num">0]) / last_window[&class="macro">#x27;close&class="macro">#x27;].iloc[class="num">0] * class="num">100, &class="macro">#x27;high_low_range&class="macro">#x27;: (last_window[&class="macro">#x27;high&class="macro">#x27;].max() - last_window[&class="macro">#x27;low&class="macro">#x27;].min()) / last_window[&class="macro">#x27;close&class="macro">#x27;].iloc[-class="num">1] * class="num">100, &class="macro">#x27;last_tick_volume&class="macro">#x27;: last_window[&class="macro">#x27;tick_volume&class="macro">#x27;].iloc[-class="num">1], &class="macro">#x27;avg_tick_volume&class="macro">#x27;: last_window[&class="macro">#x27;tick_volume&class="macro">#x27;].mean(), &class="macro">#x27;tick_volume_ratio&class="macro">#x27;: last_window[&class="macro">#x27;tick_volume&class="macro">#x27;].iloc[-class="num">1] / last_window[&class="macro">#x27;tick_volume&class="macro">#x27;].mean() if last_window[&class="macro">#x27;tick_volume&class="macro">#x27;].mean() > class="num">0 else class="num">1, }) # Convert to format for model X_pred = pd.DataFrame([feature_dict]) # Model predictions direction_proba = model_class.predict_proba(X_pred)[class="num">0] direction = model_class.predict(X_pred)[class="num">0] change_pct = model_reg.predict(X_pred)[class="num">0] # Form result result = { &class="macro">#x27;direction&class="macro">#x27;: &class="macro">#x27;UP&class="macro">#x27; if direction == class="num">1 else &class="macro">#x27;DOWN&class="macro">#x27;, &class="macro">#x27;probability&class="macro">#x27;: direction_proba[class="type">int(direction)], &class="macro">#x27;change_pct&class="macro">#x27;: change_pct,
信号分级的阈值逻辑
这段 Python 片段把模型输出转成了可执行的交易信号,核心在概率与幅度双过滤。方向分类概率突破 0.7 且变动幅度超 0.5% 才给 STRONG 级别,否则仅在 0.6 以上给普通 BUY/SELL,其余一律 NEUTRAL。 current_price 取最近一根收盘,predicted_price 用变动百分比线性外推,意味着信号强度直接挂钩模型对「涨跌幅」的置信。外汇与贵金属杠杆高,这类阈值只是概率倾向,实盘需结合价差与滑点重估。 把 0.7 / 0.5 这两个硬门槛调低,信号会变密但假突破概率可能上升;调高则更稀却更挑行情。开 MT5 用历史tick回测自己的品种,看哪组阈值在欧美盘口损耗最小。
&class="macro">#x27;current_price&class="macro">#x27;: last_window[&class="macro">#x27;close&class="macro">#x27;].iloc[-class="num">1], &class="macro">#x27;predicted_price&class="macro">#x27;: last_window[&class="macro">#x27;close&class="macro">#x27;].iloc[-class="num">1] * (class="num">1 + change_pct/class="num">100), } # Form signal if direction == class="num">1 and direction_proba[class="num">1] > class="num">0.7 and change_pct > class="num">0.5: result[&class="macro">#x27;signal&class="macro">#x27;] = &class="macro">#x27;STRONG_BUY&class="macro">#x27; elif direction == class="num">1 and direction_proba[class="num">1] > class="num">0.6: result[&class="macro">#x27;signal&class="macro">#x27;] = &class="macro">#x27;BUY&class="macro">#x27; elif direction == class="num">0 and direction_proba[class="num">0] > class="num">0.7 and change_pct < -class="num">0.5: result[&class="macro">#x27;signal&class="macro">#x27;] = &class="macro">#x27;STRONG_SELL&class="macro">#x27; elif direction == class="num">0 and direction_proba[class="num">0] > class="num">0.6: result[&class="macro">#x27;signal&class="macro">#x27;] = &class="macro">#x27;SELL&class="macro">#x27; else: result[&class="macro">#x27;signal&class="macro">#x27;] = &class="macro">#x27;NEUTRAL&class="macro">#x27; class="kw">return result
◍ 用历史K线给角度策略做回测
光看角度回归的理论曲线不够,得丢进历史数据里跑一遍才知道信号靠不靠谱。下面这段回测函数把含角度列的 DataFrame 接进去,用前视 24 根 K 线作为预测 horizon,对每一根决策 K 线调用已有的分类与回归模型拿信号。 函数核心循环从 lookback=15 开始,避开了前段数据不足的区域;只保留 angle 非空的行,逐根取截至当前的窗口做预测。若模型返回 BUY/STRONG_BUY 记 1,SELL/STRONG_SELL 记 -1,其余记 0,同时把 24 根后的收盘价变动百分比作为实际盈亏基准存下来。 跑完之后用 numpy 算 active 信号的 PnL:胜率是盈利笔数占总笔数比例,avg_win 与 avg_loss 分别是盈亏均值,profit_factor 取盈利总和绝对值除以亏损总和。外汇与贵金属波动大、杠杆高,回测盈利不代表实盘能复现,信号失效概率始终存在。 你直接把这段贴进 MT5 的 Python 环境(或本地 Jupyter 接 MT5 导出的 angular_df),把 lookback 调到 20 或 30,看 win_rate 和 profit_factor 怎么跳,比盯论坛参数帖实在。
def backtest_strategy(angular_df, model_class, model_reg, lookback=class="num">15): # Filter data clean_df = angular_df.dropna(subset=[&class="macro">#x27;angle&class="macro">#x27;]) # To store results signals = [] actual_changes = [] timestamps = [] # Modelling trading based on historical data for i in range(lookback, len(clean_df) - class="num">24): # class="num">24 bars - forecast horizon # Data at the time of decision window_df = clean_df.iloc[:i] # Get prediction prediction = predict_future_movement(model_class, model_reg, window_df, lookback) if prediction: # Record signal(class="num">1 = buy, -class="num">1 = sell, class="num">0 = neutral) if prediction[&class="macro">#x27;signal&class="macro">#x27;] in [&class="macro">#x27;BUY&class="macro">#x27;, &class="macro">#x27;STRONG_BUY&class="macro">#x27;]: signals.append(class="num">1) elif prediction[&class="macro">#x27;signal&class="macro">#x27;] in [&class="macro">#x27;SELL&class="macro">#x27;, &class="macro">#x27;STRONG_SELL&class="macro">#x27;]: signals.append(-class="num">1) else: signals.append(class="num">0) # Record actual change actual_change = (clean_df.iloc[i+class="num">24][&class="macro">#x27;close&class="macro">#x27;] - clean_df.iloc[i][&class="macro">#x27;close&class="macro">#x27;]) / clean_df.iloc[i][&class="macro">#x27;close&class="macro">#x27;] * class="num">100 actual_changes.append(actual_change) # Record time timestamps.append(clean_df.iloc[i][&class="macro">#x27;time&class="macro">#x27;]) # Result analysis signals = np.array(signals) actual_changes = np.array(actual_changes) # Calculate P&L for signals(except neutral ones) active_signals = signals != class="num">0 pnl = signals[active_signals] * actual_changes[active_signals] # Statistics win_rate = np.sum(pnl > class="num">0) / len(pnl) avg_win = np.mean(pnl[pnl > class="num">0]) if np.any(pnl > class="num">0) else class="num">0 avg_loss = np.mean(pnl[pnl < class="num">0]) if np.any(pnl < class="num">0) else class="num">0 profit_factor = abs(np.sum(pnl[pnl > class="num">0]) / np.sum(pnl[pnl < class="num">0])) if np.sum(pnl[pnl < class="num">0]) != class="num">0 else class="type">class="kw">float(&class="macro">#x27;inf&class="macro">#x27;) result = { &class="macro">#x27;total_signals&class="macro">#x27;: len(pnl), &class="macro">#x27;win_rate&class="macro">#x27;: win_rate, &class="macro">#x27;avg_win&class="macro">#x27;: avg_win, &class="macro">#x27;avg_loss&class="macro">#x27;: avg_loss,