ensemble file

This commit is contained in:
crypto_predictor committed 2026-02-22 19:46:21 +08:00
1 parent f13b1c1afe
commit 7d5c8c57b7
4 files changed
+1440

No files matched your search

+427
View File
@@ -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()