From 7d5c8c57b705a45a8c3a4792f5163190b3af33ae Mon Sep 17 00:00:00 2001 From: crypto_predictor Date: Sun, 22 Feb 2026 19:46:21 +0800 Subject: [PATCH] ensemble file --- ensemble_dlinear_jan_2026.py | 289 ++++++++++++++++ ensemble_models_feb_2026.py | 247 +++++++++++++ ensemble_models_feb_2026_back.py | 427 +++++++++++++++++++++++ ensemble_models_feb_2026_back_onnx.py | 477 ++++++++++++++++++++++++++ 4 files changed, 1440 insertions(+) create mode 100644 ensemble_dlinear_jan_2026.py create mode 100644 ensemble_models_feb_2026.py create mode 100644 ensemble_models_feb_2026_back.py create mode 100644 ensemble_models_feb_2026_back_onnx.py diff --git a/ensemble_dlinear_jan_2026.py b/ensemble_dlinear_jan_2026.py new file mode 100644 index 0000000..9f1b1eb --- /dev/null +++ b/ensemble_dlinear_jan_2026.py @@ -0,0 +1,289 @@ +"""2026年1月BTC数据 - DLinear 准确率测试 + +输入数据:预测K线的前一根(即用截止到t的数据预测t+1) +价格对比:预测结果与预测K线(t+1)的收盘价做对比 +时间:币安数据UTC,输出转为北京时间(UTC+8),按北京时间分天 +DLinear预测逻辑:整个df一次性预处理,按索引遍历窗口 +输出:每小时详细数据、每天准确率、总准确率、美股开盘标记 +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import torch +import numpy as np +import pandas as pd +import pickle +import pandas_ta as ta +from datetime import timedelta +from sklearn.preprocessing import StandardScaler +from data.fetcher import BinanceDataFetcher + +# ==================== 时区与美股判断 ==================== + +def utc_to_beijing(ts): + """UTC时间转北京时间(UTC+8)""" + return ts + timedelta(hours=8) + +def is_us_stock_open_by_utc(utc_ts): + """根据UTC时间判断美股当天是否开盘 + + 美股交易日基于美东时间(ET),UTC-5(冬令时) + 2026年1月在冬令时范围内(夏令时3月第二个周日开始) + 所以美东 = UTC - 5h + """ + et_ts = utc_ts - timedelta(hours=5) + et_date = pd.Timestamp(et_ts.date()) + + us_holidays_jan_2026 = { + pd.Timestamp('2026-01-01'), # 元旦 New Year's Day + pd.Timestamp('2026-01-19'), # 马丁·路德·金日 MLK Day + } + + if et_date.weekday() >= 5: + return False + if et_date in us_holidays_jan_2026: + return False + return True + +# ==================== pickle反序列化占位类 ==================== + +class ClassificationPreprocessor: + """pickle.load时需要能找到这个类名""" + def __init__(self, lookback=168, forecast_horizon=1): + self.lookback = lookback + self.forecast_horizon = forecast_horizon + self.scaler = StandardScaler() + self.is_fitted = False + +# ==================== 工具函数 ==================== + +def fmt_pred(val): + """格式化预测结果""" + if val is None or (isinstance(val, float) and np.isnan(val)): + return "N/A" + return "涨" if int(val) == 1 else "跌" + +def fmt_result(pred, actual): + """格式化对错""" + if pred is None or (isinstance(pred, float) and np.isnan(pred)): + return " " + return "✓" if int(pred) == int(actual) else "✗" + + +def main(): + print("=" * 90) + print("2026年1月 BTC 1h 涨跌预测准确率测试 (DLinear)") + print("输入:预测K线的前一根 | 对比:预测结果 vs 预测K线收盘价 | 时间:北京时间(UTC+8)") + print("=" * 90) + + device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu') + print(f"使用设备: {device}") + + # ========== 步骤1: 加载模型 ========== + print("\n[1/4] 加载模型...") + + from models.dlinear.model import DLinear + checkpoint = torch.load('./models/checkpoints/dlinear_classification_2026/best_model.pt', + map_location=device, weights_only=False) + model = DLinear(checkpoint['config']) + model.load_state_dict(checkpoint['model_state_dict']) + model = model.to(device).eval() + with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f: + preprocessor = pickle.load(f) + print(" ✓ DLinear已加载") + + # ========== 步骤2: 获取测试数据 ========== + print("\n[2/4] 获取数据...") + fetcher = BinanceDataFetcher() + df = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', end_date='2026-03-01', use_cache=False) + + print(f"数据范围(UTC): {df['timestamp'].min()} -> {df['timestamp'].max()}") + print(f"总行数: {len(df)}") + + # ========== 步骤3: 预处理与预测 ========== + print("\n[3/4] 运行预测...") + + # DLinear 整体预处理(无create_labels,整个df一次性处理) + lookback = preprocessor.lookback + df_dl = df.copy() + df_dl['rsi'] = ta.rsi(df_dl['close'], length=14) + macd = ta.macd(df_dl['close']) + if macd is not None and not macd.empty: + df_dl['macd'] = macd.iloc[:, 0] + df_dl['macd_hist'] = macd.iloc[:, 1] + df_dl['macd_signal'] = macd.iloc[:, 2] + df_dl = df_dl.dropna().reset_index(drop=True) + + feature_cols = ['open', 'high', 'low', 'close', 'volume', + 'rsi', 'macd', 'macd_hist', 'macd_signal'] + scaled = preprocessor.scaler.transform(df_dl[feature_cols]) + + X_dl = [] + idx_dl = [] + for i in range(len(scaled) - lookback): + X_dl.append(scaled[i:i + lookback]) + idx_dl.append(i + lookback - 1) + X_dl = np.array(X_dl) + + # 构建 timestamp -> 窗口索引映射 + dl_ts_map = {} + for i, idx in enumerate(idx_dl): + ts = df_dl.iloc[idx]['timestamp'] + dl_ts_map[ts] = i + + # 找出1月(北京时间)的所有预测目标K线索引 + jan_targets = [] + for target_idx in range(1, len(df)): + target_time_utc = df.iloc[target_idx]['timestamp'] + target_time_bj = utc_to_beijing(target_time_utc) + if target_time_bj.year == 2026 and target_time_bj.month == 2: + jan_targets.append(target_idx) + + print(f"1月(北京时间)目标K线数: {len(jan_targets)}") + + results = [] + + for target_idx in jan_targets: + target_time_utc = df.iloc[target_idx]['timestamp'] + target_time_bj = utc_to_beijing(target_time_utc) + + # 输入K线索引(前一根) + input_idx = target_idx - 1 + current_close = df.iloc[input_idx]['close'] + target_close = df.iloc[target_idx]['close'] + actual_label = 1 if target_close > current_close else 0 + + # --- DLinear 预测 --- + dl_pred = None + dl_prob = None + input_ts = df.iloc[input_idx]['timestamp'] + dl_i = dl_ts_map.get(input_ts) + if dl_i is not None and dl_i < len(X_dl): + x_tensor = torch.FloatTensor(X_dl[dl_i:dl_i+1]).to(device) + with torch.no_grad(): + logits = model(x_tensor).cpu().numpy() + prob = 1 / (1 + np.exp(-logits)) + dl_pred = int(prob.item() > 0.5) + dl_prob = float(prob.item()) + + results.append({ + 'time_utc': target_time_utc, + 'time_bj': target_time_bj, + 'current_close': current_close, + 'target_close': target_close, + 'actual': actual_label, + 'dlinear': dl_pred, + 'dl_prob': dl_prob, + }) + + # ========== 步骤4: 输出结果 ========== + if not results: + print("没有找到符合条件的测试样本") + return + + df_res = pd.DataFrame(results) + df_res['date_bj'] = df_res['time_bj'].dt.date + df_res['us_open'] = df_res['time_utc'].apply(is_us_stock_open_by_utc) + + total_samples = len(df_res) + + print(f"\n[4/4] 准确率统计 (2026年1月,北京时间)") + print(f"总样本数: {total_samples}") + + # ========== 每小时详细数据 ========== + print("\n" + "=" * 90) + print("每小时详细数据") + print("=" * 90) + + hourly_header = ( + f"{'北京时间':<18} {'前收盘':>10} {'现收盘':>10} {'实际':>4} " + f"{'DL':>3} {'':>1} {'概率':>6}" + ) + + for date_bj, day_group in df_res.groupby('date_bj'): + us_open = day_group['us_open'].iloc[0] + us_str = "美股开盘" if us_open else "美股休市" + + dl_day_correct = (day_group['dlinear'] == day_group['actual']).sum() + dl_day_total = day_group['dlinear'].notna().sum() + dl_day_acc = dl_day_correct / dl_day_total if dl_day_total > 0 else 0 + + print(f"\n--- {date_bj} ({us_str}) --- DL准确率:{dl_day_acc:.1%} ({dl_day_correct}/{dl_day_total})") + print(hourly_header) + print("-" * 90) + + for _, row in day_group.iterrows(): + time_str = row['time_bj'].strftime('%m-%d %H:%M') + actual_str = fmt_pred(row['actual']) + dl_str = fmt_pred(row['dlinear']) + dl_r = fmt_result(row['dlinear'], row['actual']) + prob_str = f"{row['dl_prob']:.3f}" if row['dl_prob'] is not None else " N/A" + + print( + f"{time_str:<18} {row['current_close']:>10.2f} {row['target_close']:>10.2f} {actual_str:>4} " + f"{dl_str:>3} {dl_r:>1} {prob_str:>6}" + ) + + # ========== 每日准确率汇总表 ========== + print("\n\n" + "=" * 90) + print("每日准确率汇总 (北京时间)") + print("=" * 90) + + header = f"{'日期':<14} {'样本':>4} {'美股':>6} {'DLinear':>8}" + print(header) + print("-" * 90) + + for date_bj, group in df_res.groupby('date_bj'): + total = len(group) + us_open = group['us_open'].iloc[0] + us_str = "开盘" if us_open else "休市" + + dl_valid = group.dropna(subset=['dlinear']) + dl_acc = (dl_valid['dlinear'] == dl_valid['actual']).mean() if len(dl_valid) > 0 else 0 + dl_str = f"{dl_acc:.1%}" + + print(f"{str(date_bj):<14} {total:>4} {us_str:>6} {dl_str:>8}") + + print("-" * 90) + + # --- 总准确率 --- + dl_valid_all = df_res.dropna(subset=['dlinear']) + dl_total = (dl_valid_all['dlinear'] == dl_valid_all['actual']).mean() if len(dl_valid_all) > 0 else 0 + + print(f"{'总计':<14} {total_samples:>4} {'':>6} {dl_total:.2%}") + print("=" * 90) + + # --- 美股开盘日 vs 休市日对比 --- + print("\n[美股开盘日 vs 休市日 准确率对比]") + print("-" * 60) + for label, flag in [("开盘日", True), ("休市日", False)]: + subset = df_res[df_res['us_open'] == flag] + if len(subset) == 0: + continue + n = len(subset) + dl_s = subset.dropna(subset=['dlinear']) + dl_a = (dl_s['dlinear'] == dl_s['actual']).mean() if len(dl_s) > 0 else 0 + print(f" {label} ({n}样本): DLinear={dl_a:.2%}") + + print("-" * 60) + + # --- 预测分布统计 --- + print("\n[预测分布统计]") + print("-" * 60) + actual_up = (df_res['actual'] == 1).sum() + actual_down = (df_res['actual'] == 0).sum() + print(f" 实际: 上涨={actual_up} ({actual_up/total_samples:.1%}) 下跌={actual_down} ({actual_down/total_samples:.1%})") + + dl_valid = df_res['dlinear'].dropna() + if len(dl_valid) > 0: + up = (dl_valid == 1).sum() + down = (dl_valid == 0).sum() + print(f" DLinear: 预测上涨={up} ({up/len(dl_valid):.1%}) 预测下跌={down} ({down/len(dl_valid):.1%})") + + print("=" * 90) + + +if __name__ == "__main__": + main() diff --git a/ensemble_models_feb_2026.py b/ensemble_models_feb_2026.py new file mode 100644 index 0000000..619eec4 --- /dev/null +++ b/ensemble_models_feb_2026.py @@ -0,0 +1,247 @@ +"""2026年2月BTC数据 - DLinear, N-BEATS, TTM-R2 准确率测试 (使用统一推理管理器) + +输入数据:预测K线的前一根(即用截止到t的数据预测t+1) +价格对比:预测结果与预测K线(t+1)的收盘价做对比 +时间:币安数据UTC,输出转为北京时间(UTC+8),按北京时间分天 +""" + +import sys +import os +import pandas as pd +import numpy as np +from datetime import timedelta +from typing import Dict, List, Any + +# 添加项目路径 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from data.fetcher import BinanceDataFetcher +from models.inference import get_inference_manager + +# ==================== 时区与美股判断 ==================== + +def utc_to_beijing(ts): + """UTC时间转北京时间(UTC+8)""" + return ts + timedelta(hours=8) + +def is_us_stock_open_by_utc(utc_ts): + """根据UTC时间判断美股当天是否开盘""" + et_ts = utc_ts - timedelta(hours=5) + et_date = pd.Timestamp(et_ts.date()) + + us_holidays_feb_2026 = { + pd.Timestamp('2026-02-16'), # 总统日 Presidents' Day + } + + if et_date.weekday() >= 5: + return False + if et_date in us_holidays_feb_2026: + return False + return True + +def fmt_pred(val): + """格式化预测结果""" + if val is None or (isinstance(val, float) and np.isnan(val)): + return "N/A" + return "涨" if int(val) == 1 else "跌" + +def fmt_result(pred, actual): + """格式化对错""" + if pred is None or (isinstance(pred, float) and np.isnan(pred)): + return " " + return "✓" if int(pred) == int(actual) else "✗" + + +def main(): + print("=" * 110) + print("2026年2月 BTC 1h 涨跌预测准确率测试 (DLinear, N-BEATS, TTM-R2)") + print("基于统一推理管理器 (models.inference)") + print("=" * 110) + + # ========== 步骤1: 加载模型 ========== + print("\n[1/4] 加载模型...") + manager = get_inference_manager() + manager.load_all_models() + + # ========== 步骤2: 获取测试数据 ========== + print("\n[2/4] 获取2026年2月数据...") + fetcher = BinanceDataFetcher() + # 获取足够长的数据以供lookback + df = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', end_date='2026-03-01', use_cache=False) + + print(f"数据范围(UTC): {df['timestamp'].min()} -> {df['timestamp'].max()}") + print(f"总行数: {len(df)}") + + # ========== 步骤3: 运行预测 ========== + print("\n[3/4] 运行预测 (逐小时回测)...") + + # 找出2月(北京时间)的所有预测目标K线索引 + jan_targets = [] + for target_idx in range(200, len(df)): # 从200开始确保有足够lookback + target_time_utc = df.iloc[target_idx]['timestamp'] + target_time_bj = utc_to_beijing(target_time_utc) + if target_time_bj.year == 2026 and target_time_bj.month == 2: + jan_targets.append(target_idx) + + print(f"2月(北京时间)目标K线数: {len(jan_targets)}") + + results = [] + + for i, target_idx in enumerate(jan_targets): + if i % 50 == 0: + print(f" 处理进度: {i}/{len(jan_targets)}...") + + target_time_utc = df.iloc[target_idx]['timestamp'] + target_time_bj = utc_to_beijing(target_time_utc) + + # 输入数据:截止到 target_idx - 1 + # 注意:predict_all 需要传入DataFrame,它会取最后一行作为 "当前" + # 所以我们需要传入 df.iloc[:target_idx] (不包含 target_idx) + # Python切片是 [start:end),所以 [:target_idx] 包含 0 到 target_idx-1 + df_input = df.iloc[:target_idx].copy() + + # 实际结果 + current_close = df.iloc[target_idx-1]['close'] + target_close = df.iloc[target_idx]['close'] + actual_label = 1 if target_close > current_close else 0 + + # 预测 + preds = manager.predict_all(df_input) + + dl_pred = preds.get('dlinear', {}).get('prediction') + nb_pred = preds.get('nbeats', {}).get('prediction') + ttm_pred = preds.get('ttm', {}).get('prediction') + ens_pred = preds.get('ensemble', {}).get('prediction') + + results.append({ + 'time_utc': target_time_utc, + 'time_bj': target_time_bj, + 'current_close': current_close, + 'target_close': target_close, + 'actual': actual_label, + 'dlinear': dl_pred, + 'nbeats': nb_pred, + 'ttm': ttm_pred, + 'ensemble': ens_pred, + 'us_open': is_us_stock_open_by_utc(target_time_utc) + }) + + # ========== 步骤4: 输出结果 ========== + if not results: + print("没有找到符合条件的测试样本") + return + + df_res = pd.DataFrame(results) + df_res['date_bj'] = df_res['time_bj'].dt.date + total_samples = len(df_res) + + print(f"\n[4/4] 准确率统计 (2026年2月,北京时间)") + print(f"总样本数: {total_samples}") + + # ========== 每小时详细数据 ========== + print("\n" + "=" * 110) + print("每小时详细数据") + print("=" * 110) + + hourly_header = ( + f"{'北京时间':<18} {'前收盘':>10} {'现收盘':>10} {'实际':>4} " + f"{'DL':>3} {'':>1} {'NB':>3} {'':>1} {'TTM':>3} {'':>1} {'集成':>4} {'':<1}" + ) + + for date_bj, day_group in df_res.groupby('date_bj'): + us_open = day_group['us_open'].iloc[0] + us_str = "美股开盘" if us_open else "美股休市" + + dl_day_acc = (day_group['dlinear'] == day_group['actual']).mean() + + nb_valid = day_group.dropna(subset=['nbeats']) + nb_day_acc = (nb_valid['nbeats'] == nb_valid['actual']).mean() if len(nb_valid) > 0 else None + + ttm_valid = day_group.dropna(subset=['ttm']) + ttm_day_acc = (ttm_valid['ttm'] == ttm_valid['actual']).mean() if len(ttm_valid) > 0 else None + + ens_valid = day_group.dropna(subset=['ensemble']) + ens_day_acc = (ens_valid['ensemble'] == ens_valid['actual']).mean() if len(ens_valid) > 0 else None + + nb_acc_str = f"{nb_day_acc:.1%}" if nb_day_acc is not None else "N/A" + ttm_acc_str = f"{ttm_day_acc:.1%}" if ttm_day_acc is not None else "N/A" + ens_acc_str = f"{ens_day_acc:.1%}" if ens_day_acc is not None else "N/A" + + print(f"\n--- {date_bj} ({us_str}) --- DL:{dl_day_acc:.1%} | NB:{nb_acc_str} | TTM:{ttm_acc_str} | 集成:{ens_acc_str}") + print(hourly_header) + print("-" * 110) + + for _, row in day_group.iterrows(): + time_str = row['time_bj'].strftime('%m-%d %H:%M') + actual_str = fmt_pred(row['actual']) + + dl_str = fmt_pred(row['dlinear']) + dl_r = fmt_result(row['dlinear'], row['actual']) + nb_str = fmt_pred(row['nbeats']) + nb_r = fmt_result(row['nbeats'], row['actual']) + ttm_str = fmt_pred(row['ttm']) + ttm_r = fmt_result(row['ttm'], row['actual']) + ens_str = fmt_pred(row['ensemble']) + ens_r = fmt_result(row['ensemble'], row['actual']) + + print( + f"{time_str:<18} {row['current_close']:>10.2f} {row['target_close']:>10.2f} {actual_str:>4} " + f"{dl_str:>3} {dl_r:>1} {nb_str:>3} {nb_r:>1} {ttm_str:>3} {ttm_r:>1} {ens_str:>4} {ens_r:<1}" + ) + + # ========== 每日准确率汇总表 ========== + print("\n\n" + "=" * 110) + print("每日准确率汇总 (北京时间)") + print("=" * 110) + + header = f"{'日期':<14} {'样本':>4} {'美股':>6} {'DLinear':>8} {'N-BEATS':>8} {'TTM-R2':>8} {'集成(投票)':>10}" + print(header) + print("-" * 110) + + for date_bj, group in df_res.groupby('date_bj'): + total = len(group) + us_open = group['us_open'].iloc[0] + us_str = "开盘" if us_open else "休市" + + dl_acc = (group['dlinear'] == group['actual']).mean() + + nb_valid = group.dropna(subset=['nbeats']) + nb_acc = (nb_valid['nbeats'] == nb_valid['actual']).mean() if len(nb_valid) > 0 else None + + ttm_valid = group.dropna(subset=['ttm']) + ttm_acc = (ttm_valid['ttm'] == ttm_valid['actual']).mean() if len(ttm_valid) > 0 else None + + ens_valid = group.dropna(subset=['ensemble']) + ens_acc = (ens_valid['ensemble'] == ens_valid['actual']).mean() if len(ens_valid) > 0 else None + + dl_str = f"{dl_acc:.1%}" + nb_str = f"{nb_acc:.1%}" if nb_acc is not None else "N/A" + ttm_str = f"{ttm_acc:.1%}" if ttm_acc is not None else "N/A" + ens_str = f"{ens_acc:.1%}" if ens_acc is not None else "N/A" + + print(f"{str(date_bj):<14} {total:>4} {us_str:>6} {dl_str:>8} {nb_str:>8} {ttm_str:>8} {ens_str:>10}") + + print("-" * 110) + + # --- 总准确率 --- + dl_total = (df_res['dlinear'] == df_res['actual']).mean() + + nb_all = df_res.dropna(subset=['nbeats']) + nb_total = (nb_all['nbeats'] == nb_all['actual']).mean() if len(nb_all) > 0 else None + + ttm_all = df_res.dropna(subset=['ttm']) + ttm_total = (ttm_all['ttm'] == ttm_all['actual']).mean() if len(ttm_all) > 0 else None + + ens_all = df_res.dropna(subset=['ensemble']) + ens_total = (ens_all['ensemble'] == ens_all['actual']).mean() if len(ens_all) > 0 else None + + nb_t_str = f"{nb_total:.2%}" if nb_total is not None else "N/A" + ttm_t_str = f"{ttm_total:.2%}" if ttm_total is not None else "N/A" + ens_t_str = f"{ens_total:.2%}" if ens_total is not None else "N/A" + + print(f"{'总计':<14} {total_samples:>4} {'':>6} {dl_total:.2%} {nb_t_str:>8} {ttm_t_str:>8} {ens_t_str:>10}") + print("=" * 110) + + +if __name__ == "__main__": + main() diff --git a/ensemble_models_feb_2026_back.py b/ensemble_models_feb_2026_back.py new file mode 100644 index 0000000..bc700b8 --- /dev/null +++ b/ensemble_models_feb_2026_back.py @@ -0,0 +1,427 @@ +"""2026年2月BTC数据 - DLinear, N-BEATS, TTM-R2 准确率测试 + +输入数据:预测K线的前一根(即用截止到t的数据预测t+1) +价格对比:预测结果与预测K线(t+1)的收盘价做对比 +时间:币安数据UTC,输出转为北京时间(UTC+8),按北京时间分天 +DLinear预测逻辑:整个df一次性预处理,按索引遍历窗口 +输出:每小时详细数据、每天准确率、总准确率、三模型集成、美股开盘标记 +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import torch +import numpy as np +import pandas as pd +import pickle +import pandas_ta as ta +from datetime import timedelta +from sklearn.preprocessing import StandardScaler +from data.fetcher import BinanceDataFetcher +from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction + +# ==================== 时区与美股判断 ==================== + +def utc_to_beijing(ts): + """UTC时间转北京时间(UTC+8)""" + return ts + timedelta(hours=8) + +def is_us_stock_open_by_utc(utc_ts): + """根据UTC时间判断美股当天是否开盘 + + 美股交易日基于美东时间(ET),UTC-5(冬令时) + 2026年2月在冬令时范围内(夏令时3月第二个周日开始) + 所以美东 = UTC - 5h + """ + et_ts = utc_ts - timedelta(hours=5) + et_date = pd.Timestamp(et_ts.date()) + + us_holidays_feb_2026 = { + pd.Timestamp('2026-02-16'), # 总统日 Presidents' Day + } + + if et_date.weekday() >= 5: + return False + if et_date in us_holidays_feb_2026: + return False + return True + +# ==================== 预处理器定义 ==================== + +class ClassificationPreprocessor: + """DLinear分类预处理器 - pickle反序列化占位类""" + def __init__(self, lookback=168, forecast_horizon=1): + self.lookback = lookback + self.forecast_horizon = forecast_horizon + self.scaler = StandardScaler() + self.is_fitted = False + +class NBeatsPreprocessor: + """N-BEATS预处理器""" + def __init__(self, lookback=168, forecast_horizon=1): + self.lookback = lookback + self.forecast_horizon = forecast_horizon + self.price_scaler = StandardScaler() + self.is_fitted = False + + def preprocess(self, df, fit=True): + df = df.copy() + prices = df['close'].values.reshape(-1, 1) + + if fit: + scaled_prices = self.price_scaler.fit_transform(prices) + self.is_fitted = True + else: + scaled_prices = self.price_scaler.transform(prices) + + X = [] + valid_indices = [] + + for i in range(len(scaled_prices) - self.lookback): + X.append(scaled_prices[i:i + self.lookback, 0]) + valid_indices.append(i + self.lookback - 1) + + return np.array(X), df, valid_indices + + +def fmt_pred(val): + """格式化预测结果""" + if val is None or (isinstance(val, float) and np.isnan(val)): + return "N/A" + return "涨" if int(val) == 1 else "跌" + +def fmt_result(pred, actual): + """格式化对错""" + if pred is None or (isinstance(pred, float) and np.isnan(pred)): + return " " + return "✓" if int(pred) == int(actual) else "✗" + + +def main(): + print("=" * 110) + print("2026年2月 BTC 1h 涨跌预测准确率测试 (DLinear, N-BEATS, TTM-R2)") + print("输入:预测K线的前一根 | 对比:预测结果 vs 预测K线收盘价 | 时间:北京时间(UTC+8)") + print("DLinear预测逻辑:整个df一次性预处理,按索引遍历窗口") + print("=" * 110) + + device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu') + print(f"使用设备: {device}") + + # ========== 步骤1: 加载模型 ========== + print("\n[1/4] 加载模型...") + + from models.dlinear.model import DLinear + dlinear_checkpoint = torch.load('./models/checkpoints/dlinear_classification_2026/best_model.pt', map_location=device, weights_only=False) + dlinear_model = DLinear(dlinear_checkpoint['config']) + dlinear_model.load_state_dict(dlinear_checkpoint['model_state_dict']) + dlinear_model = dlinear_model.to(device).eval() + with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f: + dlinear_preprocessor = pickle.load(f) + print(" ✓ DLinear已加载") + + from models.nbeats.model import NBeats + nbeats_checkpoint = torch.load('./models/checkpoints/nbeats_btc_1h/best_model.pt', map_location=device, weights_only=False) + nbeats_model = NBeats(nbeats_checkpoint['config']).to(device).eval() + with open('./models/checkpoints/nbeats_btc_1h/preprocessor.pkl', 'rb') as f: + nbeats_preprocessor = pickle.load(f) + print(" ✓ N-BEATS已加载") + + ttm_model = TinyTimeMixerForPrediction.from_pretrained( + "ibm-granite/granite-timeseries-ttm-r2", + trust_remote_code=True + ).to(device).eval() + print(" ✓ TTM-R2已加载") + + # ========== 步骤2: 获取测试数据 ========== + print("\n[2/4] 获取2026年1月数据...") + fetcher = BinanceDataFetcher() + df = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-10-01', end_date='2026-02-01', use_cache=False) + + print(f"数据范围(UTC): {df['timestamp'].min()} -> {df['timestamp'].max()}") + print(f"总行数: {len(df)}") + + # ========== 步骤3: 预处理与预测 ========== + print("\n[3/4] 运行预测...") + + # DLinear 整体预处理(无create_labels,整个df一次性处理) + dl_lookback = dlinear_preprocessor.lookback + df_dl = df.copy() + df_dl['rsi'] = ta.rsi(df_dl['close'], length=14) + macd = ta.macd(df_dl['close']) + if macd is not None and not macd.empty: + df_dl['macd'] = macd.iloc[:, 0] + df_dl['macd_hist'] = macd.iloc[:, 1] + df_dl['macd_signal'] = macd.iloc[:, 2] + df_dl = df_dl.dropna().reset_index(drop=True) + + feature_cols = ['open', 'high', 'low', 'close', 'volume', + 'rsi', 'macd', 'macd_hist', 'macd_signal'] + scaled_dl = dlinear_preprocessor.scaler.transform(df_dl[feature_cols]) + + X_dl = [] + idx_dl = [] + for i in range(len(scaled_dl) - dl_lookback): + X_dl.append(scaled_dl[i:i + dl_lookback]) + idx_dl.append(i + dl_lookback - 1) + X_dl = np.array(X_dl) + # 构建 timestamp -> 窗口索引映射 + dl_ts_map = {} + for i, idx in enumerate(idx_dl): + ts = df_dl.iloc[idx]['timestamp'] + dl_ts_map[ts] = i + + # N-BEATS 整体预处理(N-BEATS只用close,不受create_labels影响) + X_nb, df_nb, idx_nb = nbeats_preprocessor.preprocess(df, fit=False) + + # TTM-R2 整体预处理 + ttm_context_len = 512 + scaler_ttm = StandardScaler() + scaled_close_ttm = scaler_ttm.fit_transform(df[['close']]) + X_ttm = [] + idx_ttm = [] + for i in range(len(scaled_close_ttm) - ttm_context_len): + X_ttm.append(scaled_close_ttm[i:i+ttm_context_len]) + idx_ttm.append(i + ttm_context_len - 1) + X_ttm = np.array(X_ttm) + idx_ttm_map = {v: k for k, v in enumerate(idx_ttm)} + + # 找出2月(北京时间)的所有预测目标K线索引 + # df 中每一行都是一根已收盘的K线,timestamp 是 UTC + jan_targets = [] + for target_idx in range(1, len(df)): + target_time_utc = df.iloc[target_idx]['timestamp'] + target_time_bj = utc_to_beijing(target_time_utc) + if target_time_bj.year == 2026 and target_time_bj.month == 1: + jan_targets.append(target_idx) + + print(f"1月(北京时间)目标K线数: {len(jan_targets)}") + + results = [] + + for target_idx in jan_targets: + target_time_utc = df.iloc[target_idx]['timestamp'] + target_time_bj = utc_to_beijing(target_time_utc) + + # 输入K线索引(前一根) + input_idx = target_idx - 1 + current_close = df.iloc[input_idx]['close'] + target_close = df.iloc[target_idx]['close'] + actual_label = 1 if target_close > current_close else 0 + + # --- DLinear 预测(整个df一次性预处理,按索引查找窗口) --- + dl_pred = None + dl_prob = None + input_ts = df.iloc[input_idx]['timestamp'] + dl_i = dl_ts_map.get(input_ts) + if dl_i is not None and dl_i < len(X_dl): + x_dl_tensor = torch.FloatTensor(X_dl[dl_i:dl_i+1]).to(device) + with torch.no_grad(): + logits = dlinear_model(x_dl_tensor).cpu().numpy() + prob = 1 / (1 + np.exp(-logits)) + dl_pred = int(prob.item() > 0.5) + dl_prob = float(prob.item()) + + # --- N-BEATS 预测 --- + nb_pred = None + if input_idx in idx_nb: + nb_i = idx_nb.index(input_idx) + x_nb_tensor = torch.FloatTensor(X_nb[nb_i:nb_i+1]).to(device) + with torch.no_grad(): + nb_out = nbeats_model(x_nb_tensor).cpu().numpy() + if nb_out.ndim == 1: + pred_price_scaled = nb_out[0] + else: + pred_price_scaled = nb_out[0, 0] + current_price_scaled = X_nb[nb_i, -1] + nb_pred = 1 if pred_price_scaled > current_price_scaled else 0 + + # --- TTM-R2 预测 --- + ttm_pred = None + ttm_i = idx_ttm_map.get(input_idx) + if ttm_i is not None: + x_ttm_tensor = torch.FloatTensor(X_ttm[ttm_i:ttm_i+1]).to(device) + with torch.no_grad(): + ttm_out = ttm_model(x_ttm_tensor) + ttm_pred_scaled = ttm_out.prediction_outputs[0, 0, 0].cpu().numpy() + ttm_current_scaled = X_ttm[ttm_i, -1, 0] + ttm_pred = 1 if ttm_pred_scaled > ttm_current_scaled else 0 + + # 集成预测(多数投票) + votes = [v for v in [dl_pred, nb_pred, ttm_pred] if v is not None] + ens_pred = (1 if sum(votes) >= 2 else 0) if len(votes) == 3 else None + + results.append({ + 'time_utc': target_time_utc, + 'time_bj': target_time_bj, + 'current_close': current_close, + 'target_close': target_close, + 'actual': actual_label, + 'dlinear': dl_pred, + 'nbeats': nb_pred, + 'ttm': ttm_pred, + 'ensemble': ens_pred, + }) + + # ========== 步骤4: 输出结果 ========== + if not results: + print("没有找到符合条件的测试样本") + return + + df_res = pd.DataFrame(results) + df_res['date_bj'] = df_res['time_bj'].dt.date + df_res['us_open'] = df_res['time_utc'].apply(is_us_stock_open_by_utc) + + total_samples = len(df_res) + + print(f"\n[4/4] 准确率统计 (2026年2月,北京时间)") + print(f"总样本数: {total_samples}") + + # ========== 每小时详细数据 ========== + print("\n" + "=" * 110) + print("每小时详细数据") + print("=" * 110) + + hourly_header = ( + f"{'北京时间':<18} {'前收盘':>10} {'现收盘':>10} {'实际':>4} " + f"{'DL':>3} {'':>1} {'NB':>3} {'':>1} {'TTM':>3} {'':>1} {'集成':>4} {'':<1}" + ) + + for date_bj, day_group in df_res.groupby('date_bj'): + us_open = day_group['us_open'].iloc[0] + us_str = "美股开盘" if us_open else "美股休市" + + dl_day_acc = (day_group['dlinear'] == day_group['actual']).mean() + nb_day_valid = day_group.dropna(subset=['nbeats']) + nb_day_acc = (nb_day_valid['nbeats'] == nb_day_valid['actual']).mean() if len(nb_day_valid) > 0 else None + ttm_day_valid = day_group.dropna(subset=['ttm']) + ttm_day_acc = (ttm_day_valid['ttm'] == ttm_day_valid['actual']).mean() if len(ttm_day_valid) > 0 else None + ens_day_valid = day_group.dropna(subset=['ensemble']) + ens_day_acc = (ens_day_valid['ensemble'] == ens_day_valid['actual']).mean() if len(ens_day_valid) > 0 else None + + nb_acc_str = f"{nb_day_acc:.1%}" if nb_day_acc is not None else "N/A" + ttm_acc_str = f"{ttm_day_acc:.1%}" if ttm_day_acc is not None else "N/A" + ens_acc_str = f"{ens_day_acc:.1%}" if ens_day_acc is not None else "N/A" + + print(f"\n--- {date_bj} ({us_str}) --- DL:{dl_day_acc:.1%} | NB:{nb_acc_str} | TTM:{ttm_acc_str} | 集成:{ens_acc_str}") + print(hourly_header) + print("-" * 110) + + for _, row in day_group.iterrows(): + time_str = row['time_bj'].strftime('%m-%d %H:%M') + actual_str = fmt_pred(row['actual']) + + dl_str = fmt_pred(row['dlinear']) + dl_r = fmt_result(row['dlinear'], row['actual']) + nb_str = fmt_pred(row['nbeats']) + nb_r = fmt_result(row['nbeats'], row['actual']) + ttm_str = fmt_pred(row['ttm']) + ttm_r = fmt_result(row['ttm'], row['actual']) + ens_str = fmt_pred(row['ensemble']) + ens_r = fmt_result(row['ensemble'], row['actual']) + + print( + f"{time_str:<18} {row['current_close']:>10.2f} {row['target_close']:>10.2f} {actual_str:>4} " + f"{dl_str:>3} {dl_r:>1} {nb_str:>3} {nb_r:>1} {ttm_str:>3} {ttm_r:>1} {ens_str:>4} {ens_r:<1}" + ) + + # ========== 每日准确率汇总表 ========== + print("\n\n" + "=" * 110) + print("每日准确率汇总 (北京时间)") + print("=" * 110) + + header = f"{'日期':<14} {'样本':>4} {'美股':>6} {'DLinear':>8} {'N-BEATS':>8} {'TTM-R2':>8} {'集成(投票)':>10}" + print(header) + print("-" * 110) + + for date_bj, group in df_res.groupby('date_bj'): + total = len(group) + us_open = group['us_open'].iloc[0] + us_str = "开盘" if us_open else "休市" + + dl_acc = (group['dlinear'] == group['actual']).mean() + + nb_valid = group.dropna(subset=['nbeats']) + nb_acc = (nb_valid['nbeats'] == nb_valid['actual']).mean() if len(nb_valid) > 0 else None + + ttm_valid = group.dropna(subset=['ttm']) + ttm_acc = (ttm_valid['ttm'] == ttm_valid['actual']).mean() if len(ttm_valid) > 0 else None + + ens_valid = group.dropna(subset=['ensemble']) + ens_acc = (ens_valid['ensemble'] == ens_valid['actual']).mean() if len(ens_valid) > 0 else None + + dl_str = f"{dl_acc:.1%}" + nb_str = f"{nb_acc:.1%}" if nb_acc is not None else "N/A" + ttm_str = f"{ttm_acc:.1%}" if ttm_acc is not None else "N/A" + ens_str = f"{ens_acc:.1%}" if ens_acc is not None else "N/A" + + print(f"{str(date_bj):<14} {total:>4} {us_str:>6} {dl_str:>8} {nb_str:>8} {ttm_str:>8} {ens_str:>10}") + + print("-" * 110) + + # --- 总准确率 --- + dl_total = (df_res['dlinear'] == df_res['actual']).mean() + + nb_all = df_res.dropna(subset=['nbeats']) + nb_total = (nb_all['nbeats'] == nb_all['actual']).mean() if len(nb_all) > 0 else None + + ttm_all = df_res.dropna(subset=['ttm']) + ttm_total = (ttm_all['ttm'] == ttm_all['actual']).mean() if len(ttm_all) > 0 else None + + ens_all = df_res.dropna(subset=['ensemble']) + ens_total = (ens_all['ensemble'] == ens_all['actual']).mean() if len(ens_all) > 0 else None + + nb_t_str = f"{nb_total:.2%}" if nb_total is not None else "N/A" + ttm_t_str = f"{ttm_total:.2%}" if ttm_total is not None else "N/A" + ens_t_str = f"{ens_total:.2%}" if ens_total is not None else "N/A" + + print(f"{'总计':<14} {total_samples:>4} {'':>6} {dl_total:.2%} {nb_t_str:>8} {ttm_t_str:>8} {ens_t_str:>10}") + print("=" * 110) + + # --- 美股开盘日 vs 休市日对比 --- + print("\n[美股开盘日 vs 休市日 准确率对比]") + print("-" * 80) + for label, flag in [("开盘日", True), ("休市日", False)]: + subset = df_res[df_res['us_open'] == flag] + if len(subset) == 0: + continue + n = len(subset) + dl_a = (subset['dlinear'] == subset['actual']).mean() + + nb_s = subset.dropna(subset=['nbeats']) + nb_a = (nb_s['nbeats'] == nb_s['actual']).mean() if len(nb_s) > 0 else None + + ttm_s = subset.dropna(subset=['ttm']) + ttm_a = (ttm_s['ttm'] == ttm_s['actual']).mean() if len(ttm_s) > 0 else None + + ens_s = subset.dropna(subset=['ensemble']) + ens_a = (ens_s['ensemble'] == ens_s['actual']).mean() if len(ens_s) > 0 else None + + nb_str = f"{nb_a:.2%}" if nb_a is not None else "N/A" + ttm_str = f"{ttm_a:.2%}" if ttm_a is not None else "N/A" + ens_str = f"{ens_a:.2%}" if ens_a is not None else "N/A" + + print(f" {label} ({n}样本): DLinear={dl_a:.2%} N-BEATS={nb_str} TTM-R2={ttm_str} 集成={ens_str}") + + print("-" * 80) + + # --- 预测分布统计 --- + print("\n[预测分布统计]") + print("-" * 80) + actual_up = (df_res['actual'] == 1).sum() + actual_down = (df_res['actual'] == 0).sum() + print(f" 实际: 上涨={actual_up} ({actual_up/total_samples:.1%}) 下跌={actual_down} ({actual_down/total_samples:.1%})") + + for name, col in [("DLinear", "dlinear"), ("N-BEATS", "nbeats"), ("TTM-R2", "ttm"), ("集成", "ensemble")]: + valid = df_res[col].dropna() + if len(valid) == 0: + continue + up = (valid == 1).sum() + down = (valid == 0).sum() + print(f" {name}: 预测上涨={up} ({up/len(valid):.1%}) 预测下跌={down} ({down/len(valid):.1%})") + + print("=" * 110) + + +if __name__ == "__main__": + main() diff --git a/ensemble_models_feb_2026_back_onnx.py b/ensemble_models_feb_2026_back_onnx.py new file mode 100644 index 0000000..19045e1 --- /dev/null +++ b/ensemble_models_feb_2026_back_onnx.py @@ -0,0 +1,477 @@ +"""2026年2月BTC数据 - DLinear, N-BEATS, TTM-R2 准确率测试 (ONNX版) + +输入数据:预测K线的前一根(即用截止到t的数据预测t+1) +价格对比:预测结果与预测K线(t+1)的收盘价做对比 +时间:币安数据UTC,输出转为北京时间(UTC+8),按北京时间分天 +DLinear预测逻辑:整个df一次性预处理,按索引遍历窗口 +输出:每小时详细数据、每天准确率、总准确率、三模型集成、美股开盘标记 +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import numpy as np +import pandas as pd +import pickle +import pandas_ta as ta +import onnxruntime as ort +from datetime import timedelta +from sklearn.preprocessing import StandardScaler +from data.fetcher import BinanceDataFetcher + +# ==================== 时区与美股判断 ==================== + +def utc_to_beijing(ts): + """UTC时间转北京时间(UTC+8)""" + return ts + timedelta(hours=8) + +def is_us_stock_open_by_utc(utc_ts): + """根据UTC时间判断美股当天是否开盘 + + 美股交易日基于美东时间(ET),UTC-5(冬令时) + 2026年2月在冬令时范围内(夏令时3月第二个周日开始) + 所以美东 = UTC - 5h + """ + et_ts = utc_ts - timedelta(hours=5) + et_date = pd.Timestamp(et_ts.date()) + + us_holidays_feb_2026 = { + pd.Timestamp('2026-02-16'), # 总统日 Presidents' Day + } + + if et_date.weekday() >= 5: + return False + if et_date in us_holidays_feb_2026: + return False + return True + +# ==================== 预处理器定义 ==================== + +class ClassificationPreprocessor: + """DLinear分类预处理器 - pickle反序列化占位类""" + def __init__(self, lookback=168, forecast_horizon=1): + self.lookback = lookback + self.forecast_horizon = forecast_horizon + self.scaler = StandardScaler() + self.is_fitted = False + +class NBeatsPreprocessor: + """N-BEATS预处理器""" + def __init__(self, lookback=168, forecast_horizon=1): + self.lookback = lookback + self.forecast_horizon = forecast_horizon + self.price_scaler = StandardScaler() + self.is_fitted = False + + def preprocess(self, df, fit=True): + df = df.copy() + prices = df['close'].values.reshape(-1, 1) + + if fit: + scaled_prices = self.price_scaler.fit_transform(prices) + self.is_fitted = True + else: + scaled_prices = self.price_scaler.transform(prices) + + X = [] + valid_indices = [] + + for i in range(len(scaled_prices) - self.lookback): + X.append(scaled_prices[i:i + self.lookback, 0]) + valid_indices.append(i + self.lookback - 1) + + return np.array(X), df, valid_indices + + +def fmt_pred(val): + """格式化预测结果""" + if val is None or (isinstance(val, float) and np.isnan(val)): + return "N/A" + return "涨" if int(val) == 1 else "跌" + +def fmt_result(pred, actual): + """格式化对错""" + if pred is None or (isinstance(pred, float) and np.isnan(pred)): + return " " + return "✓" if int(pred) == int(actual) else "✗" + + +def main(): + print("=" * 110) + print("2026年2月 BTC 1h 涨跌预测准确率测试 (DLinear, N-BEATS, TTM-R2) - ONNX Version") + print("输入:预测K线的前一根 | 对比:预测结果 vs 预测K线收盘价 | 时间:北京时间(UTC+8)") + print("DLinear预测逻辑:整个df一次性预处理,按索引遍历窗口") + print("=" * 110) + + # ========== 步骤1: 加载模型 ========== + print("\n[1/4] 加载 ONNX 模型...") + + # Load DLinear ONNX + try: + dlinear_session = ort.InferenceSession('./models/checkpoints/dlinear_classification_2026/model.onnx') + print(" ✓ DLinear (ONNX) 已加载") + + with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f: + dlinear_preprocessor = pickle.load(f) + except Exception as e: + print(f" ✗ DLinear 加载失败: {e}") + return + + # Load N-BEATS ONNX + try: + nbeats_session = ort.InferenceSession('./models/checkpoints/nbeats_btc_1h/model.onnx') + print(" ✓ N-BEATS (ONNX) 已加载") + + with open('./models/checkpoints/nbeats_btc_1h/preprocessor.pkl', 'rb') as f: + nbeats_preprocessor = pickle.load(f) + except Exception as e: + print(f" ✗ N-BEATS 加载失败: {e}") + return + + # Load TTM-R2 ONNX + ttm_session = None + try: + temp_session = ort.InferenceSession('./models/checkpoints/ttm_r2_onnx/ttm_r2_crypto_channel_mixing.onnx') + + # Test inference with dummy input + print(" Testing TTM-R2 ONNX inference...") + dummy_input = np.random.randn(1, 512, 1).astype(np.float32) + ort_inputs = {} + input_names = [i.name for i in temp_session.get_inputs()] + print(f"Model inputs: {input_names}") + # 根据 session 中的名称映射输入 + for input_meta in temp_session.get_inputs(): + name = input_meta.name + if 'past_values' in name or name == 'input': + ort_inputs[name] = dummy_input + elif 'mask' in name: + # 创建全 1 mask + mask = np.ones_like(dummy_input) + # 检查期望的类型 + if input_meta.type == 'tensor(int64)': + mask = mask.astype(np.int64) + else: + mask = mask.astype(np.float32) + ort_inputs[name] = mask + print(f"Session run input keys: {list(ort_inputs.keys())}") + for k, v in ort_inputs.items(): + print(f"Input {k} shape: {v.shape}, dtype: {v.dtype}") + + temp_session.run(None, ort_inputs) + + ttm_session = temp_session + print(" ✓ TTM-R2 (ONNX) 已加载且可用 (Optimizations Disabled)") + except Exception as e: + print(f" ⚠ TTM-R2 (ONNX) 加载或推理失败: {e}") + print(" 将在本次测试中跳过 TTM-R2 模型") + ttm_session = None + + # ========== 步骤2: 获取测试数据 ========== + print("\n[2/4] 获取2026年2月数据...") + fetcher = BinanceDataFetcher() + df = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-10-01', end_date='2026-03-01', use_cache=False) + + print(f"数据范围(UTC): {df['timestamp'].min()} -> {df['timestamp'].max()}") + print(f"总行数: {len(df)}") + + # ========== 步骤3: 预处理与预测 ========== + print("\n[3/4] 运行预测...") + + # DLinear 整体预处理(无create_labels,整个df一次性处理) + dl_lookback = dlinear_preprocessor.lookback + df_dl = df.copy() + df_dl['rsi'] = ta.rsi(df_dl['close'], length=14) + macd = ta.macd(df_dl['close']) + if macd is not None and not macd.empty: + df_dl['macd'] = macd.iloc[:, 0] + df_dl['macd_hist'] = macd.iloc[:, 1] + df_dl['macd_signal'] = macd.iloc[:, 2] + df_dl = df_dl.dropna().reset_index(drop=True) + + feature_cols = ['open', 'high', 'low', 'close', 'volume', + 'rsi', 'macd', 'macd_hist', 'macd_signal'] + scaled_dl = dlinear_preprocessor.scaler.transform(df_dl[feature_cols]) + + X_dl = [] + idx_dl = [] + for i in range(len(scaled_dl) - dl_lookback): + X_dl.append(scaled_dl[i:i + dl_lookback]) + idx_dl.append(i + dl_lookback - 1) + X_dl = np.array(X_dl, dtype=np.float32) + # 构建 timestamp -> 窗口索引映射 + dl_ts_map = {} + for i, idx in enumerate(idx_dl): + ts = df_dl.iloc[idx]['timestamp'] + dl_ts_map[ts] = i + + # N-BEATS 整体预处理(N-BEATS只用close,不受create_labels影响) + X_nb, df_nb, idx_nb = nbeats_preprocessor.preprocess(df, fit=False) + X_nb = X_nb.astype(np.float32) # Ensure float32 + + # TTM-R2 整体预处理 + ttm_context_len = 512 + scaler_ttm = StandardScaler() + scaled_close_ttm = scaler_ttm.fit_transform(df[['close']]) + X_ttm = [] + idx_ttm = [] + for i in range(len(scaled_close_ttm) - ttm_context_len): + X_ttm.append(scaled_close_ttm[i:i+ttm_context_len]) + idx_ttm.append(i + ttm_context_len - 1) + X_ttm = np.array(X_ttm, dtype=np.float32) + idx_ttm_map = {v: k for k, v in enumerate(idx_ttm)} + + # 找出2月(北京时间)的所有预测目标K线索引 + # df 中每一行都是一根已收盘的K线,timestamp 是 UTC + jan_targets = [] + for target_idx in range(1, len(df)): + target_time_utc = df.iloc[target_idx]['timestamp'] + target_time_bj = utc_to_beijing(target_time_utc) + if target_time_bj.year == 2026 and target_time_bj.month == 2: + jan_targets.append(target_idx) + + print(f"2月(北京时间)目标K线数: {len(jan_targets)}") + + results = [] + + for target_idx in jan_targets: + target_time_utc = df.iloc[target_idx]['timestamp'] + target_time_bj = utc_to_beijing(target_time_utc) + + # 输入K线索引(前一根) + input_idx = target_idx - 1 + current_close = df.iloc[input_idx]['close'] + target_close = df.iloc[target_idx]['close'] + actual_label = 1 if target_close > current_close else 0 + + # --- DLinear 预测 (ONNX) --- + dl_pred = None + dl_prob = None + input_ts = df.iloc[input_idx]['timestamp'] + dl_i = dl_ts_map.get(input_ts) + if dl_i is not None and dl_i < len(X_dl): + x_dl_input = X_dl[dl_i:dl_i+1] # (1, 168, 9) + + # ONNX Inference + # Input name: 'input' + logits = dlinear_session.run(['output'], {'input': x_dl_input})[0] + + prob = 1 / (1 + np.exp(-logits)) + dl_pred = int(prob.item() > 0.5) + dl_prob = float(prob.item()) + + # --- N-BEATS 预测 (ONNX) --- + nb_pred = None + if input_idx in idx_nb: + nb_i = idx_nb.index(input_idx) + x_nb_input = X_nb[nb_i:nb_i+1] # (1, 168) + + # ONNX Inference + # Input name: 'input' + nb_out = nbeats_session.run(['forecast'], {'input': x_nb_input})[0] + + if nb_out.ndim == 1: + pred_price_scaled = nb_out[0] + else: + pred_price_scaled = nb_out[0, 0] + current_price_scaled = X_nb[nb_i, -1] + nb_pred = 1 if pred_price_scaled > current_price_scaled else 0 + + # --- TTM-R2 预测 (ONNX) --- + ttm_pred = None + ttm_i = idx_ttm_map.get(input_idx) + if ttm_i is not None and ttm_session is not None: + x_ttm_input = X_ttm[ttm_i:ttm_i+1] # (1, 512, 1) + + # Debug shape + # print(f"TTM input shape: {x_ttm_input.shape}, dtype: {x_ttm_input.dtype}") + + # ONNX Inference + # Input name: 'past_values' + # Try transposing to (1, 1, 512) just in case + # x_ttm_input = x_ttm_input.transpose(0, 2, 1) + + ttm_out = ttm_session.run(None, {'past_values': x_ttm_input})[0] + + ttm_pred_scaled = ttm_out[0, 0, 0] + ttm_current_scaled = X_ttm[ttm_i, -1, 0] + ttm_pred = 1 if ttm_pred_scaled > ttm_current_scaled else 0 + + # 集成预测(多数投票) + votes = [v for v in [dl_pred, nb_pred, ttm_pred] if v is not None] + ens_pred = (1 if sum(votes) >= 2 else 0) if len(votes) == 3 else None + + results.append({ + 'time_utc': target_time_utc, + 'time_bj': target_time_bj, + 'current_close': current_close, + 'target_close': target_close, + 'actual': actual_label, + 'dlinear': dl_pred, + 'nbeats': nb_pred, + 'ttm': ttm_pred, + 'ensemble': ens_pred, + }) + + # ========== 步骤4: 输出结果 ========== + if not results: + print("没有找到符合条件的测试样本") + return + + df_res = pd.DataFrame(results) + df_res['date_bj'] = df_res['time_bj'].dt.date + df_res['us_open'] = df_res['time_utc'].apply(is_us_stock_open_by_utc) + + total_samples = len(df_res) + + print(f"\n[4/4] 准确率统计 (2026年2月,北京时间)") + print(f"总样本数: {total_samples}") + + # ========== 每小时详细数据 ========== + print("\n" + "=" * 110) + print("每小时详细数据") + print("=" * 110) + + hourly_header = ( + f"{'北京时间':<18} {'前收盘':>10} {'现收盘':>10} {'实际':>4} " + f"{'DL':>3} {'':>1} {'NB':>3} {'':>1} {'TTM':>3} {'':>1} {'集成':>4} {'':<1}" + ) + + for date_bj, day_group in df_res.groupby('date_bj'): + us_open = day_group['us_open'].iloc[0] + us_str = "美股开盘" if us_open else "美股休市" + + dl_day_acc = (day_group['dlinear'] == day_group['actual']).mean() + nb_day_valid = day_group.dropna(subset=['nbeats']) + nb_day_acc = (nb_day_valid['nbeats'] == nb_day_valid['actual']).mean() if len(nb_day_valid) > 0 else None + ttm_day_valid = day_group.dropna(subset=['ttm']) + ttm_day_acc = (ttm_day_valid['ttm'] == ttm_day_valid['actual']).mean() if len(ttm_day_valid) > 0 else None + ens_day_valid = day_group.dropna(subset=['ensemble']) + ens_day_acc = (ens_day_valid['ensemble'] == ens_day_valid['actual']).mean() if len(ens_day_valid) > 0 else None + + nb_acc_str = f"{nb_day_acc:.1%}" if nb_day_acc is not None else "N/A" + ttm_acc_str = f"{ttm_day_acc:.1%}" if ttm_day_acc is not None else "N/A" + ens_acc_str = f"{ens_day_acc:.1%}" if ens_day_acc is not None else "N/A" + + print(f"\n--- {date_bj} ({us_str}) --- DL:{dl_day_acc:.1%} | NB:{nb_acc_str} | TTM:{ttm_acc_str} | 集成:{ens_acc_str}") + print(hourly_header) + print("-" * 110) + + for _, row in day_group.iterrows(): + time_str = row['time_bj'].strftime('%m-%d %H:%M') + actual_str = fmt_pred(row['actual']) + + dl_str = fmt_pred(row['dlinear']) + dl_r = fmt_result(row['dlinear'], row['actual']) + nb_str = fmt_pred(row['nbeats']) + nb_r = fmt_result(row['nbeats'], row['actual']) + ttm_str = fmt_pred(row['ttm']) + ttm_r = fmt_result(row['ttm'], row['actual']) + ens_str = fmt_pred(row['ensemble']) + ens_r = fmt_result(row['ensemble'], row['actual']) + + print( + f"{time_str:<18} {row['current_close']:>10.2f} {row['target_close']:>10.2f} {actual_str:>4} " + f"{dl_str:>3} {dl_r:>1} {nb_str:>3} {nb_r:>1} {ttm_str:>3} {ttm_r:>1} {ens_str:>4} {ens_r:<1}" + ) + + # ========== 每日准确率汇总表 ========== + print("\n\n" + "=" * 110) + print("每日准确率汇总 (北京时间)") + print("=" * 110) + + header = f"{'日期':<14} {'样本':>4} {'美股':>6} {'DLinear':>8} {'N-BEATS':>8} {'TTM-R2':>8} {'集成(投票)':>10}" + print(header) + print("-" * 110) + + for date_bj, group in df_res.groupby('date_bj'): + total = len(group) + us_open = group['us_open'].iloc[0] + us_str = "开盘" if us_open else "休市" + + dl_acc = (group['dlinear'] == group['actual']).mean() + + nb_valid = group.dropna(subset=['nbeats']) + nb_acc = (nb_valid['nbeats'] == nb_valid['actual']).mean() if len(nb_valid) > 0 else None + + ttm_valid = group.dropna(subset=['ttm']) + ttm_acc = (ttm_valid['ttm'] == ttm_valid['actual']).mean() if len(ttm_valid) > 0 else None + + ens_valid = group.dropna(subset=['ensemble']) + ens_acc = (ens_valid['ensemble'] == ens_valid['actual']).mean() if len(ens_valid) > 0 else None + + dl_str = f"{dl_acc:.1%}" + nb_str = f"{nb_acc:.1%}" if nb_acc is not None else "N/A" + ttm_str = f"{ttm_acc:.1%}" if ttm_acc is not None else "N/A" + ens_str = f"{ens_acc:.1%}" if ens_acc is not None else "N/A" + + print(f"{str(date_bj):<14} {total:>4} {us_str:>6} {dl_str:>8} {nb_str:>8} {ttm_str:>8} {ens_str:>10}") + + print("-" * 110) + + # --- 总准确率 --- + dl_total = (df_res['dlinear'] == df_res['actual']).mean() + + nb_all = df_res.dropna(subset=['nbeats']) + nb_total = (nb_all['nbeats'] == nb_all['actual']).mean() if len(nb_all) > 0 else None + + ttm_all = df_res.dropna(subset=['ttm']) + ttm_total = (ttm_all['ttm'] == ttm_all['actual']).mean() if len(ttm_all) > 0 else None + + ens_all = df_res.dropna(subset=['ensemble']) + ens_total = (ens_all['ensemble'] == ens_all['actual']).mean() if len(ens_all) > 0 else None + + nb_t_str = f"{nb_total:.2%}" if nb_total is not None else "N/A" + ttm_t_str = f"{ttm_total:.2%}" if ttm_total is not None else "N/A" + ens_t_str = f"{ens_total:.2%}" if ens_total is not None else "N/A" + + print(f"{'总计':<14} {total_samples:>4} {'':>6} {dl_total:.2%} {nb_t_str:>8} {ttm_t_str:>8} {ens_t_str:>10}") + print("=" * 110) + + # --- 美股开盘日 vs 休市日对比 --- + print("\n[美股开盘日 vs 休市日 准确率对比]") + print("-" * 80) + for label, flag in [("开盘日", True), ("休市日", False)]: + subset = df_res[df_res['us_open'] == flag] + if len(subset) == 0: + continue + n = len(subset) + dl_a = (subset['dlinear'] == subset['actual']).mean() + + nb_s = subset.dropna(subset=['nbeats']) + nb_a = (nb_s['nbeats'] == nb_s['actual']).mean() if len(nb_s) > 0 else None + + ttm_s = subset.dropna(subset=['ttm']) + ttm_a = (ttm_s['ttm'] == ttm_s['actual']).mean() if len(ttm_s) > 0 else None + + ens_s = subset.dropna(subset=['ensemble']) + ens_a = (ens_s['ensemble'] == ens_s['actual']).mean() if len(ens_s) > 0 else None + + nb_str = f"{nb_a:.2%}" if nb_a is not None else "N/A" + ttm_str = f"{ttm_a:.2%}" if ttm_a is not None else "N/A" + ens_str = f"{ens_a:.2%}" if ens_a is not None else "N/A" + + print(f" {label} ({n}样本): DLinear={dl_a:.2%} N-BEATS={nb_str} TTM-R2={ttm_str} 集成={ens_str}") + + print("-" * 80) + + # --- 预测分布统计 --- + print("\n[预测分布统计]") + print("-" * 80) + actual_up = (df_res['actual'] == 1).sum() + actual_down = (df_res['actual'] == 0).sum() + print(f" 实际: 上涨={actual_up} ({actual_up/total_samples:.1%}) 下跌={actual_down} ({actual_down/total_samples:.1%})") + + for name, col in [("DLinear", "dlinear"), ("N-BEATS", "nbeats"), ("TTM-R2", "ttm"), ("集成", "ensemble")]: + valid = df_res[col].dropna() + if len(valid) == 0: + continue + up = (valid == 1).sum() + down = (valid == 0).sum() + print(f" {name}: 预测上涨={up} ({up/len(valid):.1%}) 预测下跌={down} ({down/len(valid):.1%})") + + print("=" * 110) + + +if __name__ == "__main__": + main()