"""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年3月 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年3月数据...") fetcher = BinanceDataFetcher() df = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01', end_date='2026-04-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)} # 找出3月(北京时间)的所有预测目标K线索引 # df 中每一行都是一根已收盘的K线,timestamp 是 UTC jan_targets = [] for target_idx in range(1, len(df)): # 同样在筛选样本时也将时间向后移动一个小时 target_time_utc = df.iloc[target_idx]['timestamp'] + timedelta(hours=1) target_time_bj = utc_to_beijing(target_time_utc) if target_time_bj.year == 2026 and target_time_bj.month == 3: jan_targets.append(target_idx) print(f"3月(北京时间)目标K线数: {len(jan_targets)}") results = [] for target_idx in jan_targets: # 原始timestamp表示k线的开始时间,因此预测的结果实际对应下一小时的k线。 # 所以展示时需要将时间向后移动1小时。 target_time_utc = df.iloc[target_idx]['timestamp'] + timedelta(hours=1) target_time_bj = utc_to_beijing(target_time_utc) # 现在我们将预测目标设为下一根k线(即target_idx+1), # 输入使用它的前一根(等于原先的target_idx)。 predicted_idx = target_idx + 1 if predicted_idx >= len(df): # 已经到达数据末尾,无法构建完整样本,跳过 continue input_idx = target_idx # 开盘/收盘对应预测k线 open_price = df.iloc[predicted_idx]['open'] close_price = df.iloc[predicted_idx]['close'] # 实际涨跌以预测k线的开盘为基准 actual_label = 1 if close_price > open_price 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, 'open': open_price, 'close': close_price, '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年3月,北京时间)") 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['open']:>10.2f} {row['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()