ensemble file
This commit is contained in:
1 parent
f13b1c1afe
commit
7d5c8c57b7
4 files changed
+1440
No files matched your search
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user