- 删除测试脚本: test_2026_classification.py, test_lagllama.py, test_timesfm.py, test_ttm.py - 删除2019相关文件: analyze_2019_*, apply_2019_*, dlinear_2019_*, three_models_2019_* - 添加集成模型相关代码和分析脚本 - 添加N-BEATS模型和CatBoost分类模型
459 lines
18 KiB
Python
459 lines
18 KiB
Python
"""保存所有测试结果到项目 - 生成完整报告"""
|
||
|
||
import sys
|
||
import os
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
import torch
|
||
import pickle
|
||
import numpy as np
|
||
import pandas as pd
|
||
import pandas_ta as ta
|
||
from sklearn.preprocessing import StandardScaler
|
||
from sklearn.metrics import confusion_matrix
|
||
from data.fetcher import BinanceDataFetcher
|
||
import json
|
||
from datetime import datetime
|
||
|
||
|
||
# ==================== 预处理器定义 ====================
|
||
|
||
class ClassificationPreprocessor:
|
||
"""DLinear分类预处理器"""
|
||
def __init__(self, lookback=168, forecast_horizon=1):
|
||
self.lookback = lookback
|
||
self.forecast_horizon = forecast_horizon
|
||
self.scaler = StandardScaler()
|
||
self.is_fitted = False
|
||
|
||
def add_features(self, df):
|
||
df = df.copy()
|
||
df['rsi'] = ta.rsi(df['close'], length=14)
|
||
macd = ta.macd(df['close'])
|
||
if macd is not None and not macd.empty:
|
||
df['macd'] = macd.iloc[:, 0]
|
||
df['macd_hist'] = macd.iloc[:, 1]
|
||
df['macd_signal'] = macd.iloc[:, 2]
|
||
return df
|
||
|
||
def create_labels(self, df):
|
||
df = df.copy()
|
||
df['future_price'] = df['close'].shift(-self.forecast_horizon)
|
||
df['future_return'] = (df['future_price'] - df['close']) / df['close']
|
||
df['label'] = (df['future_return'] > 0).astype(int)
|
||
return df[:-self.forecast_horizon]
|
||
|
||
def preprocess(self, df, fit=True):
|
||
df = self.add_features(df)
|
||
df = self.create_labels(df)
|
||
df = df.dropna().reset_index(drop=True)
|
||
|
||
feature_cols = ['open', 'high', 'low', 'close', 'volume',
|
||
'rsi', 'macd', 'macd_hist', 'macd_signal']
|
||
|
||
if fit:
|
||
scaled_features = self.scaler.fit_transform(df[feature_cols])
|
||
self.is_fitted = True
|
||
self.feature_columns = feature_cols
|
||
else:
|
||
scaled_features = self.scaler.transform(df[feature_cols])
|
||
|
||
X, y = [], []
|
||
for i in range(len(scaled_features) - self.lookback):
|
||
X.append(scaled_features[i:i + self.lookback])
|
||
y.append(df['label'].iloc[i + self.lookback])
|
||
|
||
return np.array(X), np.array(y), df
|
||
|
||
|
||
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()
|
||
df['future_price'] = df['close'].shift(-self.forecast_horizon)
|
||
df = df[:-self.forecast_horizon].reset_index(drop=True)
|
||
|
||
prices = df['close'].values.reshape(-1, 1)
|
||
future_prices = df['future_price'].values.reshape(-1, 1)
|
||
|
||
if fit:
|
||
scaled_prices = self.price_scaler.fit_transform(prices)
|
||
scaled_future = self.price_scaler.transform(future_prices)
|
||
self.is_fitted = True
|
||
else:
|
||
scaled_prices = self.price_scaler.transform(prices)
|
||
scaled_future = self.price_scaler.transform(future_prices)
|
||
|
||
X, y = [], []
|
||
for i in range(len(scaled_prices) - self.lookback):
|
||
X.append(scaled_prices[i:i + self.lookback, 0])
|
||
y.append(scaled_future[i + self.lookback, 0])
|
||
|
||
return np.array(X), np.array(y), df
|
||
|
||
def inverse_transform_price(self, scaled_price):
|
||
return self.price_scaler.inverse_transform(scaled_price.reshape(-1, 1)).flatten()
|
||
|
||
|
||
def main():
|
||
print("="*80)
|
||
print("保存完整测试结果到项目")
|
||
print("="*80)
|
||
|
||
# 创建结果目录
|
||
results_dir = "./results"
|
||
os.makedirs(results_dir, exist_ok=True)
|
||
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
||
# ========== 加载模型 ==========
|
||
print("\n[1/5] 加载模型...")
|
||
|
||
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||
|
||
from models.dlinear.model import DLinear
|
||
dlinear_checkpoint = torch.load(
|
||
'./models/checkpoints/dlinear_classification_2026/best_model.pt',
|
||
map_location=device
|
||
)
|
||
dlinear_model = DLinear(dlinear_checkpoint['config'])
|
||
dlinear_model.load_state_dict(dlinear_checkpoint['model_state_dict'])
|
||
dlinear_model = dlinear_model.to(device)
|
||
dlinear_model.eval()
|
||
|
||
with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f:
|
||
dlinear_preprocessor = pickle.load(f)
|
||
|
||
from models.nbeats.model import NBeats
|
||
nbeats_checkpoint = torch.load(
|
||
'./models/checkpoints/nbeats_btc_1h/best_model.pt',
|
||
map_location=device
|
||
)
|
||
nbeats_model = NBeats(nbeats_checkpoint['config'])
|
||
nbeats_model.load_state_dict(nbeats_checkpoint['model_state_dict'])
|
||
nbeats_model = nbeats_model.to(device)
|
||
nbeats_model.eval()
|
||
|
||
with open('./models/checkpoints/nbeats_btc_1h/preprocessor.pkl', 'rb') as f:
|
||
nbeats_preprocessor = pickle.load(f)
|
||
|
||
from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction
|
||
ttm_model = TinyTimeMixerForPrediction.from_pretrained(
|
||
"ibm-granite/granite-timeseries-ttm-r2",
|
||
trust_remote_code=True
|
||
)
|
||
ttm_model = ttm_model.to(device)
|
||
ttm_model.eval()
|
||
|
||
print("✓ 三个模型已加载")
|
||
|
||
# ========== 获取数据 ==========
|
||
print("\n[2/5] 获取2026年1月测试数据...")
|
||
fetcher = BinanceDataFetcher()
|
||
|
||
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
||
end_date='2026-01-30', use_cache=False)
|
||
|
||
print(f"✓ 数据: {len(df_2026)} 行, {df_2026['timestamp'].min()} 至 {df_2026['timestamp'].max()}")
|
||
|
||
# ========== 预处理和预测 ==========
|
||
print("\n[3/5] 预处理和预测...")
|
||
|
||
X_dlinear, y_dlinear, df_dlinear = dlinear_preprocessor.preprocess(df_2026, fit=False)
|
||
X_nbeats, y_nbeats, df_nbeats = nbeats_preprocessor.preprocess(df_2026, fit=False)
|
||
|
||
min_samples = min(len(X_dlinear), len(X_nbeats))
|
||
X_dlinear = X_dlinear[:min_samples]
|
||
y_dlinear = y_dlinear[:min_samples]
|
||
X_nbeats = X_nbeats[:min_samples]
|
||
|
||
# DLinear预测
|
||
X_dlinear_tensor = torch.FloatTensor(X_dlinear).to(device)
|
||
with torch.no_grad():
|
||
dlinear_logits = dlinear_model(X_dlinear_tensor).cpu().numpy()
|
||
dlinear_probs = 1 / (1 + np.exp(-dlinear_logits.flatten()))
|
||
dlinear_preds = (dlinear_probs > 0.5).astype(int)
|
||
|
||
# N-BEATS预测
|
||
X_nbeats_tensor = torch.FloatTensor(X_nbeats).to(device)
|
||
with torch.no_grad():
|
||
nbeats_pred_scaled = nbeats_model(X_nbeats_tensor).cpu().numpy()
|
||
|
||
nbeats_pred_prices = nbeats_preprocessor.inverse_transform_price(nbeats_pred_scaled)
|
||
current_open_prices = df_nbeats['open'].iloc[168:168+len(nbeats_pred_prices)].values
|
||
nbeats_preds = (nbeats_pred_prices > current_open_prices).astype(int)
|
||
|
||
# TTM-R2预测
|
||
context_length = 512
|
||
ttm_preds_list = []
|
||
all_prices = df_2026['close'].values
|
||
all_open_prices = df_2026['open'].values
|
||
|
||
for i in range(min_samples):
|
||
start_idx = i
|
||
end_idx = start_idx + context_length
|
||
|
||
if end_idx > len(all_prices):
|
||
context_data = all_prices[-context_length:].reshape(1, -1, 1)
|
||
current_open_price = all_open_prices[min(168 + i, len(all_open_prices) - 1)]
|
||
else:
|
||
context_data = all_prices[start_idx:end_idx].reshape(1, -1, 1)
|
||
current_open_price = all_open_prices[end_idx] if end_idx < len(all_open_prices) else all_open_prices[-1]
|
||
|
||
context_tensor = torch.FloatTensor(context_data).to(device)
|
||
with torch.no_grad():
|
||
output = ttm_model(context_tensor)
|
||
pred_price = output.prediction_outputs.cpu().numpy()[0, 0, 0]
|
||
ttm_preds_list.append(1 if pred_price > current_open_price else 0)
|
||
|
||
ttm_preds = np.array(ttm_preds_list)
|
||
|
||
# 双模型交集
|
||
two_model_agree_up = (dlinear_preds == 1) & (nbeats_preds == 1)
|
||
two_model_agree_down = (dlinear_preds == 0) & (nbeats_preds == 0)
|
||
two_model_preds = np.full(len(dlinear_preds), -1)
|
||
two_model_preds[two_model_agree_up] = 1
|
||
two_model_preds[two_model_agree_down] = 0
|
||
|
||
# 三模型投票
|
||
vote_sum = dlinear_preds + nbeats_preds + ttm_preds
|
||
three_model_preds = np.where(vote_sum >= 2, 1, 0)
|
||
|
||
true_labels = y_dlinear
|
||
|
||
print(f"✓ 预测完成,样本数: {min_samples}")
|
||
|
||
# ========== 生成详细结果 ==========
|
||
print("\n[4/5] 生成详细结果...")
|
||
|
||
# 时间戳和日期
|
||
timestamps = df_2026['timestamp'].iloc[168:168+min_samples].values
|
||
timestamps_utc = pd.to_datetime(timestamps)
|
||
timestamps_et = timestamps_utc.tz_localize('UTC').tz_convert('America/New_York')
|
||
dates = timestamps_et.date
|
||
|
||
# 创建详细结果DataFrame
|
||
results_df = pd.DataFrame({
|
||
'timestamp_utc': timestamps_utc,
|
||
'timestamp_et': timestamps_et,
|
||
'date': dates,
|
||
'true_label': true_labels,
|
||
'dlinear_prob': dlinear_probs,
|
||
'dlinear_pred': dlinear_preds,
|
||
'nbeats_pred': nbeats_preds,
|
||
'ttm_pred': ttm_preds,
|
||
'two_model_pred': two_model_preds,
|
||
'three_model_pred': three_model_preds,
|
||
'dlinear_correct': dlinear_preds == true_labels,
|
||
'two_model_correct': (two_model_preds == true_labels) & (two_model_preds != -1),
|
||
'three_model_correct': three_model_preds == true_labels
|
||
})
|
||
|
||
# 按天统计
|
||
daily_stats = results_df.groupby('date').agg({
|
||
'true_label': 'count',
|
||
'two_model_pred': lambda x: (x != -1).sum(),
|
||
'two_model_correct': 'sum',
|
||
'three_model_correct': 'sum',
|
||
'dlinear_correct': 'sum'
|
||
})
|
||
|
||
daily_stats.columns = ['total_samples', 'two_model_valid', 'two_model_correct',
|
||
'three_model_correct', 'dlinear_correct']
|
||
|
||
daily_stats['dlinear_accuracy'] = daily_stats['dlinear_correct'] / daily_stats['total_samples'] * 100
|
||
daily_stats['two_model_accuracy'] = daily_stats['two_model_correct'] / daily_stats['two_model_valid'] * 100
|
||
daily_stats['three_model_accuracy'] = daily_stats['three_model_correct'] / daily_stats['total_samples'] * 100
|
||
daily_stats['two_model_coverage'] = daily_stats['two_model_valid'] / daily_stats['total_samples'] * 100
|
||
|
||
daily_stats['two_model_accuracy'] = daily_stats['two_model_accuracy'].fillna(0)
|
||
|
||
# ========== 保存结果 ==========
|
||
print("\n[5/5] 保存结果文件...")
|
||
|
||
# 1. 保存逐小时详细结果
|
||
hourly_file = f"{results_dir}/hourly_predictions_2026_01.csv"
|
||
results_df.to_csv(hourly_file, index=False)
|
||
print(f"✓ 保存逐小时预测: {hourly_file}")
|
||
|
||
# 2. 保存每日统计
|
||
daily_file = f"{results_dir}/daily_statistics_2026_01.csv"
|
||
daily_stats.to_csv(daily_file)
|
||
print(f"✓ 保存每日统计: {daily_file}")
|
||
|
||
# 3. 生成总结报告
|
||
summary = {
|
||
"测试期间": {
|
||
"开始日期": str(df_2026['timestamp'].min()),
|
||
"结束日期": str(df_2026['timestamp'].max()),
|
||
"总天数": len(daily_stats),
|
||
"总样本数": int(min_samples)
|
||
},
|
||
"单模型性能": {
|
||
"DLinear": {
|
||
"准确率": f"{(dlinear_preds == true_labels).mean() * 100:.2f}%",
|
||
"预测上涨比例": f"{(dlinear_preds == 1).mean() * 100:.1f}%"
|
||
},
|
||
"N-BEATS": {
|
||
"准确率": f"{(nbeats_preds == true_labels).mean() * 100:.2f}%",
|
||
"预测上涨比例": f"{(nbeats_preds == 1).mean() * 100:.1f}%"
|
||
},
|
||
"TTM-R2": {
|
||
"准确率": f"{(ttm_preds == true_labels).mean() * 100:.2f}%",
|
||
"预测上涨比例": f"{(ttm_preds == 1).mean() * 100:.1f}%"
|
||
}
|
||
},
|
||
"集成策略性能": {
|
||
"双模型交集": {
|
||
"准确率": f"{((two_model_preds == true_labels) & (two_model_preds != -1)).sum() / (two_model_preds != -1).sum() * 100:.2f}%",
|
||
"覆盖率": f"{(two_model_preds != -1).mean() * 100:.2f}%",
|
||
"有效预测数": int((two_model_preds != -1).sum())
|
||
},
|
||
"三模型投票": {
|
||
"准确率": f"{(three_model_preds == true_labels).mean() * 100:.2f}%",
|
||
"覆盖率": "100.00%",
|
||
"有效预测数": int(len(three_model_preds))
|
||
}
|
||
},
|
||
"DLinear概率分布": {
|
||
"0.4-0.5区间": f"{((dlinear_probs >= 0.4) & (dlinear_probs < 0.5)).sum()}个 ({((dlinear_probs >= 0.4) & (dlinear_probs < 0.5)).mean() * 100:.1f}%)",
|
||
"0.5-0.6区间": f"{((dlinear_probs >= 0.5) & (dlinear_probs < 0.6)).sum()}个 ({((dlinear_probs >= 0.5) & (dlinear_probs < 0.6)).mean() * 100:.1f}%)",
|
||
"0.6-0.7区间": f"{((dlinear_probs >= 0.6) & (dlinear_probs < 0.7)).sum()}个 ({((dlinear_probs >= 0.6) & (dlinear_probs < 0.7)).mean() * 100:.1f}%)",
|
||
"平均概率": f"{dlinear_probs.mean():.3f}"
|
||
},
|
||
"最佳/最差交易日": {
|
||
"双模型最佳日": {
|
||
"日期": str(daily_stats['two_model_accuracy'].idxmax()),
|
||
"准确率": f"{daily_stats['two_model_accuracy'].max():.1f}%"
|
||
},
|
||
"双模型最差日": {
|
||
"日期": str(daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].idxmin()),
|
||
"准确率": f"{daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].min():.1f}%"
|
||
},
|
||
"三模型最佳日": {
|
||
"日期": str(daily_stats['three_model_accuracy'].idxmax()),
|
||
"准确率": f"{daily_stats['three_model_accuracy'].max():.1f}%"
|
||
},
|
||
"三模型最差日": {
|
||
"日期": str(daily_stats['three_model_accuracy'].idxmin()),
|
||
"准确率": f"{daily_stats['three_model_accuracy'].min():.1f}%"
|
||
}
|
||
}
|
||
}
|
||
|
||
summary_file = f"{results_dir}/summary_report_2026_01.json"
|
||
with open(summary_file, 'w', encoding='utf-8') as f:
|
||
json.dump(summary, f, indent=2, ensure_ascii=False)
|
||
print(f"✓ 保存总结报告: {summary_file}")
|
||
|
||
# 4. 生成Markdown报告
|
||
markdown_report = f"""# 加密货币价格预测模型测试报告
|
||
|
||
## 测试概况
|
||
|
||
- **测试期间**: 2026年1月1日 - 2026年1月30日
|
||
- **总天数**: {len(daily_stats)}天
|
||
- **总样本数**: {min_samples}个小时
|
||
- **生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||
|
||
---
|
||
|
||
## 单模型性能
|
||
|
||
### DLinear分类模型
|
||
- **准确率**: {(dlinear_preds == true_labels).mean() * 100:.2f}%
|
||
- **预测上涨比例**: {(dlinear_preds == 1).mean() * 100:.1f}%
|
||
- **预测下跌比例**: {(dlinear_preds == 0).mean() * 100:.1f}%
|
||
|
||
### N-BEATS回归模型
|
||
- **准确率**: {(nbeats_preds == true_labels).mean() * 100:.2f}%
|
||
- **预测上涨比例**: {(nbeats_preds == 1).mean() * 100:.1f}%
|
||
- **预测下跌比例**: {(nbeats_preds == 0).mean() * 100:.1f}%
|
||
|
||
### TTM-R2零样本模型
|
||
- **准确率**: {(ttm_preds == true_labels).mean() * 100:.2f}%
|
||
- **预测上涨比例**: {(ttm_preds == 1).mean() * 100:.1f}%
|
||
- **预测下跌比例**: {(ttm_preds == 0).mean() * 100:.1f}%
|
||
|
||
---
|
||
|
||
## 集成策略性能
|
||
|
||
### 双模型交集(DLinear + N-BEATS)
|
||
- **准确率**: {((two_model_preds == true_labels) & (two_model_preds != -1)).sum() / (two_model_preds != -1).sum() * 100:.2f}%
|
||
- **覆盖率**: {(two_model_preds != -1).mean() * 100:.2f}%
|
||
- **有效预测数**: {(two_model_preds != -1).sum()}次
|
||
|
||
### 三模型多数投票(DLinear + N-BEATS + TTM-R2)
|
||
- **准确率**: {(three_model_preds == true_labels).mean() * 100:.2f}%
|
||
- **覆盖率**: 100.00%
|
||
- **有效预测数**: {len(three_model_preds)}次
|
||
|
||
---
|
||
|
||
## DLinear概率分布分析
|
||
|
||
| 概率区间 | 样本数 | 占比 |
|
||
|---------|--------|------|
|
||
| 0.4-0.5 | {((dlinear_probs >= 0.4) & (dlinear_probs < 0.5)).sum()} | {((dlinear_probs >= 0.4) & (dlinear_probs < 0.5)).mean() * 100:.1f}% |
|
||
| 0.5-0.6 | {((dlinear_probs >= 0.5) & (dlinear_probs < 0.6)).sum()} | {((dlinear_probs >= 0.5) & (dlinear_probs < 0.6)).mean() * 100:.1f}% |
|
||
| 0.6-0.7 | {((dlinear_probs >= 0.6) & (dlinear_probs < 0.7)).sum()} | {((dlinear_probs >= 0.6) & (dlinear_probs < 0.7)).mean() * 100:.1f}% |
|
||
|
||
**平均概率**: {dlinear_probs.mean():.3f}
|
||
|
||
---
|
||
|
||
## 最佳/最差交易日
|
||
|
||
### 双模型交集
|
||
- **最佳日**: {daily_stats['two_model_accuracy'].idxmax()} ({daily_stats['two_model_accuracy'].max():.1f}%)
|
||
- **最差日**: {daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].idxmin()} ({daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].min():.1f}%)
|
||
|
||
### 三模型投票
|
||
- **最佳日**: {daily_stats['three_model_accuracy'].idxmax()} ({daily_stats['three_model_accuracy'].max():.1f}%)
|
||
- **最差日**: {daily_stats['three_model_accuracy'].idxmin()} ({daily_stats['three_model_accuracy'].min():.1f}%)
|
||
|
||
---
|
||
|
||
## 结论
|
||
|
||
1. **最佳单模型**: DLinear ({(dlinear_preds == true_labels).mean() * 100:.2f}%)
|
||
2. **最佳集成策略**: 双模型交集 ({((two_model_preds == true_labels) & (two_model_preds != -1)).sum() / (two_model_preds != -1).sum() * 100:.2f}%)
|
||
3. **DLinear置信度**: 99.2%的预测集中在0.45-0.55区间,缺乏高置信度预测
|
||
|
||
## 数据文件
|
||
|
||
- 逐小时预测结果: `hourly_predictions_2026_01.csv`
|
||
- 每日统计数据: `daily_statistics_2026_01.csv`
|
||
- 总结报告: `summary_report_2026_01.json`
|
||
|
||
---
|
||
|
||
生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||
"""
|
||
|
||
markdown_file = f"{results_dir}/README_2026_01.md"
|
||
with open(markdown_file, 'w', encoding='utf-8') as f:
|
||
f.write(markdown_report)
|
||
print(f"✓ 保存Markdown报告: {markdown_file}")
|
||
|
||
print("\n" + "="*80)
|
||
print("✓ 所有结果已保存到 ./results/ 目录")
|
||
print("="*80)
|
||
print(f"\n保存的文件:")
|
||
print(f" 1. {hourly_file}")
|
||
print(f" 2. {daily_file}")
|
||
print(f" 3. {summary_file}")
|
||
print(f" 4. {markdown_file}")
|
||
print("\n" + "="*80)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|