refactor: 清理测试文件和2019相关代码
- 删除测试脚本: 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分类模型
This commit is contained in:
1 parent
c74f4f6949
commit
1bd9d25f60
33 files changed
+6557
-233
No files matched your search
@@ -6,7 +6,13 @@
|
|||||||
"Bash(python:*)",
|
"Bash(python:*)",
|
||||||
"Bash(source:*)",
|
"Bash(source:*)",
|
||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(git add:*)"
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(tree:*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"Bash(conda activate:*)",
|
||||||
|
"WebSearch",
|
||||||
|
"Bash(pip install:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
"""分析2019年和2026年1月准确率<40%交易日的前一天技术特征"""
|
||||||
|
|
||||||
|
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 data.fetcher import BinanceDataFetcher
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_technical_features(df):
|
||||||
|
"""计算丰富的技术特征"""
|
||||||
|
df = df.copy()
|
||||||
|
|
||||||
|
# 基础价格特征
|
||||||
|
df['price_change'] = df['close'] - df['open']
|
||||||
|
df['price_change_pct'] = (df['close'] - df['open']) / df['open'] * 100
|
||||||
|
df['high_low_range'] = df['high'] - df['low']
|
||||||
|
df['high_low_range_pct'] = (df['high'] - df['low']) / df['low'] * 100
|
||||||
|
df['body_ratio'] = abs(df['close'] - df['open']) / (df['high'] - df['low'] + 1e-10)
|
||||||
|
|
||||||
|
# 移动平均线
|
||||||
|
df['sma_5'] = ta.sma(df['close'], length=5)
|
||||||
|
df['sma_10'] = ta.sma(df['close'], length=10)
|
||||||
|
df['sma_20'] = ta.sma(df['close'], length=20)
|
||||||
|
df['sma_50'] = ta.sma(df['close'], length=50)
|
||||||
|
|
||||||
|
# 价格相对MA位置
|
||||||
|
df['price_vs_sma5'] = (df['close'] - df['sma_5']) / df['sma_5'] * 100
|
||||||
|
df['price_vs_sma20'] = (df['close'] - df['sma_20']) / df['sma_20'] * 100
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
df['rsi_14'] = ta.rsi(df['close'], length=14)
|
||||||
|
df['rsi_7'] = ta.rsi(df['close'], length=7)
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
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]
|
||||||
|
|
||||||
|
# 布林带
|
||||||
|
bbands = ta.bbands(df['close'], length=20)
|
||||||
|
if bbands is not None and not bbands.empty:
|
||||||
|
df['bb_upper'] = bbands.iloc[:, 0]
|
||||||
|
df['bb_mid'] = bbands.iloc[:, 1]
|
||||||
|
df['bb_lower'] = bbands.iloc[:, 2]
|
||||||
|
df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['bb_mid'] * 100
|
||||||
|
df['bb_position'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'] + 1e-10) * 100
|
||||||
|
|
||||||
|
# ATR波动率
|
||||||
|
df['atr_14'] = ta.atr(df['high'], df['low'], df['close'], length=14)
|
||||||
|
df['atr_pct'] = df['atr_14'] / df['close'] * 100
|
||||||
|
|
||||||
|
# 成交量特征
|
||||||
|
df['volume_sma_20'] = ta.sma(df['volume'], length=20)
|
||||||
|
df['volume_ratio'] = df['volume'] / (df['volume_sma_20'] + 1e-10)
|
||||||
|
|
||||||
|
# 价格动量
|
||||||
|
df['momentum_1'] = df['close'].pct_change(1) * 100
|
||||||
|
df['momentum_3'] = df['close'].pct_change(3) * 100
|
||||||
|
df['momentum_7'] = df['close'].pct_change(7) * 100
|
||||||
|
df['momentum_24'] = df['close'].pct_change(24) * 100
|
||||||
|
|
||||||
|
# 波动率
|
||||||
|
df['volatility_5'] = df['close'].pct_change().rolling(5).std() * 100
|
||||||
|
df['volatility_10'] = df['close'].pct_change().rolling(10).std() * 100
|
||||||
|
df['volatility_20'] = df['close'].pct_change().rolling(20).std() * 100
|
||||||
|
|
||||||
|
# 连续涨跌天数
|
||||||
|
df['is_up'] = (df['close'] > df['open']).astype(int)
|
||||||
|
df['consecutive_days'] = 0
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_period(model, preprocessor, device, period_name, start_date, end_date):
|
||||||
|
"""分析单个时期的数据,返回低准确率日期"""
|
||||||
|
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"分析 {period_name}")
|
||||||
|
print(f"{'='*80}")
|
||||||
|
|
||||||
|
# 获取数据(需要额外获取前一天的数据)
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
|
||||||
|
# 获取包含前一天的数据
|
||||||
|
start_dt = pd.to_datetime(start_date) - pd.Timedelta(days=2)
|
||||||
|
df = fetcher.fetch_ohlcv('BTC', '1h', start_date=start_dt.strftime('%Y-%m-%d'),
|
||||||
|
end_date=end_date, use_cache=False)
|
||||||
|
|
||||||
|
print(f"✓ 数据: {len(df)} 行")
|
||||||
|
|
||||||
|
# 预处理
|
||||||
|
X, y, df_processed = preprocessor.preprocess(df, fit=False)
|
||||||
|
|
||||||
|
print(f"✓ 有效样本: {len(X)}")
|
||||||
|
|
||||||
|
# DLinear预测
|
||||||
|
X_tensor = torch.FloatTensor(X).to(device)
|
||||||
|
with torch.no_grad():
|
||||||
|
logits = model(X_tensor).cpu().numpy()
|
||||||
|
probs = 1 / (1 + np.exp(-logits))
|
||||||
|
preds = (probs > 0.5).astype(int).flatten()
|
||||||
|
|
||||||
|
true_labels = y
|
||||||
|
|
||||||
|
# 构建每日结果
|
||||||
|
timestamps = df['timestamp'].iloc[168:168+len(X)].values
|
||||||
|
timestamps_utc = pd.to_datetime(timestamps)
|
||||||
|
timestamps_et = timestamps_utc.tz_localize('UTC').tz_convert('America/New_York')
|
||||||
|
dates = timestamps_et.date
|
||||||
|
|
||||||
|
results_df = pd.DataFrame({
|
||||||
|
'date': dates,
|
||||||
|
'timestamp_et': timestamps_et,
|
||||||
|
'correct': preds == true_labels,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 按天统计准确率
|
||||||
|
daily_stats = results_df.groupby('date').agg({
|
||||||
|
'correct': ['count', 'sum']
|
||||||
|
})
|
||||||
|
daily_stats.columns = ['total', 'correct']
|
||||||
|
daily_stats['accuracy'] = daily_stats['correct'] / daily_stats['total'] * 100
|
||||||
|
|
||||||
|
# 找出准确率<40%的日期
|
||||||
|
low_acc_dates = daily_stats[daily_stats['accuracy'] < 40].index.tolist()
|
||||||
|
|
||||||
|
print(f"✓ 准确率<40%的日期: {len(low_acc_dates)}天")
|
||||||
|
for date in low_acc_dates:
|
||||||
|
acc = daily_stats.loc[date, 'accuracy']
|
||||||
|
print(f" {date}: {acc:.1f}%")
|
||||||
|
|
||||||
|
# 计算技术特征
|
||||||
|
df_with_features = calculate_technical_features(df)
|
||||||
|
|
||||||
|
# 为每天聚合技术特征(使用当天的24小时数据)
|
||||||
|
# 先确保timestamp是datetime类型
|
||||||
|
if not pd.api.types.is_datetime64_any_dtype(df_with_features['timestamp']):
|
||||||
|
df_with_features['timestamp'] = pd.to_datetime(df_with_features['timestamp'])
|
||||||
|
|
||||||
|
df_with_features['date'] = df_with_features['timestamp'].dt.tz_localize('UTC').tz_convert('America/New_York').dt.date
|
||||||
|
|
||||||
|
# 返回低准确率日期和完整数据
|
||||||
|
return low_acc_dates, df_with_features, daily_stats
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("="*80)
|
||||||
|
print("分析准确率<40%交易日的前一天技术特征共同点")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 加载模型 ==========
|
||||||
|
print("\n[1/3] 加载DLinear模型...")
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
|
||||||
|
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)
|
||||||
|
model.eval()
|
||||||
|
|
||||||
|
with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f:
|
||||||
|
preprocessor = pickle.load(f)
|
||||||
|
|
||||||
|
print("✓ DLinear模型已加载")
|
||||||
|
|
||||||
|
# ========== 分析两个时期 ==========
|
||||||
|
print("\n[2/3] 分析2019年和2026年1月...")
|
||||||
|
|
||||||
|
# 分析2019年1月
|
||||||
|
low_acc_2019, df_2019, stats_2019 = analyze_period(
|
||||||
|
model, preprocessor, device,
|
||||||
|
"2019年1月", "2019-01-01", "2019-01-31"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 分析2026年1月
|
||||||
|
low_acc_2026, df_2026, stats_2026 = analyze_period(
|
||||||
|
model, preprocessor, device,
|
||||||
|
"2026年1月", "2026-01-01", "2026-01-31"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========== 提取前一天特征 ==========
|
||||||
|
print("\n[3/3] 分析前一天技术特征...")
|
||||||
|
|
||||||
|
# 合并所有低准确率日期
|
||||||
|
all_low_acc_dates = low_acc_2019 + low_acc_2026
|
||||||
|
|
||||||
|
print(f"\n总计{len(all_low_acc_dates)}个低准确率交易日")
|
||||||
|
|
||||||
|
# 提取前一天的特征
|
||||||
|
prev_day_features = []
|
||||||
|
|
||||||
|
for date in all_low_acc_dates:
|
||||||
|
# 获取前一天日期
|
||||||
|
prev_date = date - timedelta(days=1)
|
||||||
|
|
||||||
|
# 确定使用哪个数据集
|
||||||
|
if date in low_acc_2019:
|
||||||
|
df = df_2019
|
||||||
|
period = "2019年"
|
||||||
|
else:
|
||||||
|
df = df_2026
|
||||||
|
period = "2026年"
|
||||||
|
|
||||||
|
# 获取前一天的数据
|
||||||
|
prev_day_data = df[df['date'] == prev_date]
|
||||||
|
|
||||||
|
if len(prev_day_data) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 聚合前一天的技术特征(使用24小时的平均值、最大值、最小值等)
|
||||||
|
features = {
|
||||||
|
'date': date,
|
||||||
|
'prev_date': prev_date,
|
||||||
|
'period': period,
|
||||||
|
'accuracy': stats_2019.loc[date, 'accuracy'] if date in low_acc_2019 else stats_2026.loc[date, 'accuracy'],
|
||||||
|
# 价格特征
|
||||||
|
'prev_close': prev_day_data['close'].iloc[-1],
|
||||||
|
'prev_open': prev_day_data['open'].iloc[0],
|
||||||
|
'prev_price_change_pct': prev_day_data['price_change_pct'].mean(),
|
||||||
|
'prev_high_low_range_pct': prev_day_data['high_low_range_pct'].mean(),
|
||||||
|
'prev_body_ratio': prev_day_data['body_ratio'].mean(),
|
||||||
|
# RSI
|
||||||
|
'prev_rsi_14': prev_day_data['rsi_14'].iloc[-1],
|
||||||
|
'prev_rsi_7': prev_day_data['rsi_7'].iloc[-1],
|
||||||
|
# MACD
|
||||||
|
'prev_macd': prev_day_data['macd'].iloc[-1] if 'macd' in prev_day_data.columns else None,
|
||||||
|
'prev_macd_hist': prev_day_data['macd_hist'].iloc[-1] if 'macd_hist' in prev_day_data.columns else None,
|
||||||
|
# 布林带
|
||||||
|
'prev_bb_width': prev_day_data['bb_width'].iloc[-1] if 'bb_width' in prev_day_data.columns else None,
|
||||||
|
'prev_bb_position': prev_day_data['bb_position'].iloc[-1] if 'bb_position' in prev_day_data.columns else None,
|
||||||
|
# 波动率
|
||||||
|
'prev_atr_pct': prev_day_data['atr_pct'].iloc[-1] if 'atr_pct' in prev_day_data.columns else None,
|
||||||
|
'prev_volatility_5': prev_day_data['volatility_5'].iloc[-1] if 'volatility_5' in prev_day_data.columns else None,
|
||||||
|
'prev_volatility_20': prev_day_data['volatility_20'].iloc[-1] if 'volatility_20' in prev_day_data.columns else None,
|
||||||
|
# 成交量
|
||||||
|
'prev_volume_ratio': prev_day_data['volume_ratio'].iloc[-1] if 'volume_ratio' in prev_day_data.columns else None,
|
||||||
|
# 动量
|
||||||
|
'prev_momentum_1': prev_day_data['momentum_1'].iloc[-1] if 'momentum_1' in prev_day_data.columns else None,
|
||||||
|
'prev_momentum_3': prev_day_data['momentum_3'].iloc[-1] if 'momentum_3' in prev_day_data.columns else None,
|
||||||
|
'prev_momentum_7': prev_day_data['momentum_7'].iloc[-1] if 'momentum_7' in prev_day_data.columns else None,
|
||||||
|
'prev_momentum_24': prev_day_data['momentum_24'].iloc[-1] if 'momentum_24' in prev_day_data.columns else None,
|
||||||
|
# 价格相对MA位置
|
||||||
|
'prev_price_vs_sma5': prev_day_data['price_vs_sma5'].iloc[-1] if 'price_vs_sma5' in prev_day_data.columns else None,
|
||||||
|
'prev_price_vs_sma20': prev_day_data['price_vs_sma20'].iloc[-1] if 'price_vs_sma20' in prev_day_data.columns else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_day_features.append(features)
|
||||||
|
|
||||||
|
# 转换为DataFrame
|
||||||
|
features_df = pd.DataFrame(prev_day_features)
|
||||||
|
|
||||||
|
# ========== 分析共同点 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 前一天技术特征统计分析")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print(f"\n分析样本数: {len(features_df)}个低准确率交易日")
|
||||||
|
print(f" 2019年: {(features_df['period']=='2019年').sum()}天")
|
||||||
|
print(f" 2026年: {(features_df['period']=='2026年').sum()}天")
|
||||||
|
|
||||||
|
# 统计各特征的平均值、中位数、标准差
|
||||||
|
numeric_cols = [col for col in features_df.columns if col.startswith('prev_') and features_df[col].dtype in [np.float64, np.int64]]
|
||||||
|
|
||||||
|
print(f"\n前一天技术特征统计:")
|
||||||
|
print(f"{'特征':<30} {'平均值':>10} {'中位数':>10} {'标准差':>10} {'共同点':<30}")
|
||||||
|
print("-" * 95)
|
||||||
|
|
||||||
|
for col in numeric_cols:
|
||||||
|
if features_df[col].notna().sum() > 0:
|
||||||
|
mean_val = features_df[col].mean()
|
||||||
|
median_val = features_df[col].median()
|
||||||
|
std_val = features_df[col].std()
|
||||||
|
|
||||||
|
# 判断共同点
|
||||||
|
common_trait = ""
|
||||||
|
if 'rsi' in col:
|
||||||
|
if mean_val > 70:
|
||||||
|
common_trait = "超买区域"
|
||||||
|
elif mean_val < 30:
|
||||||
|
common_trait = "超卖区域"
|
||||||
|
elif 45 <= mean_val <= 55:
|
||||||
|
common_trait = "中性区域"
|
||||||
|
elif 'volatility' in col:
|
||||||
|
if mean_val > 3:
|
||||||
|
common_trait = "高波动"
|
||||||
|
elif mean_val < 1:
|
||||||
|
common_trait = "低波动"
|
||||||
|
elif 'momentum' in col:
|
||||||
|
if mean_val > 2:
|
||||||
|
common_trait = "强势上涨"
|
||||||
|
elif mean_val < -2:
|
||||||
|
common_trait = "强势下跌"
|
||||||
|
elif abs(mean_val) < 0.5:
|
||||||
|
common_trait = "横盘震荡"
|
||||||
|
elif 'bb_position' in col:
|
||||||
|
if mean_val > 80:
|
||||||
|
common_trait = "接近布林上轨"
|
||||||
|
elif mean_val < 20:
|
||||||
|
common_trait = "接近布林下轨"
|
||||||
|
elif 'volume_ratio' in col:
|
||||||
|
if mean_val > 1.5:
|
||||||
|
common_trait = "成交量放大"
|
||||||
|
elif mean_val < 0.8:
|
||||||
|
common_trait = "成交量萎缩"
|
||||||
|
|
||||||
|
print(f"{col:<30} {mean_val:>10.2f} {median_val:>10.2f} {std_val:>10.2f} {common_trait:<30}")
|
||||||
|
|
||||||
|
# ========== 对比分析 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("🔍 2019年 vs 2026年对比分析")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
features_2019 = features_df[features_df['period'] == '2019年']
|
||||||
|
features_2026 = features_df[features_df['period'] == '2026年']
|
||||||
|
|
||||||
|
if len(features_2019) > 0 and len(features_2026) > 0:
|
||||||
|
print(f"\n{'特征':<30} {'2019年平均':>12} {'2026年平均':>12} {'差异':>10}")
|
||||||
|
print("-" * 70)
|
||||||
|
|
||||||
|
for col in numeric_cols[:15]: # 显示前15个特征
|
||||||
|
if features_2019[col].notna().sum() > 0 and features_2026[col].notna().sum() > 0:
|
||||||
|
mean_2019 = features_2019[col].mean()
|
||||||
|
mean_2026 = features_2026[col].mean()
|
||||||
|
diff = mean_2026 - mean_2019
|
||||||
|
|
||||||
|
print(f"{col:<30} {mean_2019:>12.2f} {mean_2026:>12.2f} {diff:>10.2f}")
|
||||||
|
|
||||||
|
# ========== 关键共同点总结 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("💡 关键共同点总结")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# RSI分析
|
||||||
|
rsi_mean = features_df['prev_rsi_14'].mean()
|
||||||
|
print(f"\n1. RSI指标:")
|
||||||
|
print(f" 平均RSI: {rsi_mean:.1f}")
|
||||||
|
if rsi_mean > 70:
|
||||||
|
print(f" 🔴 共同点: 前一天处于超买状态")
|
||||||
|
elif rsi_mean < 30:
|
||||||
|
print(f" 🟢 共同点: 前一天处于超卖状态")
|
||||||
|
else:
|
||||||
|
print(f" ➖ 共同点: 前一天RSI在中性区域")
|
||||||
|
|
||||||
|
# 波动率分析
|
||||||
|
vol_mean = features_df['prev_volatility_20'].mean()
|
||||||
|
print(f"\n2. 波动率:")
|
||||||
|
print(f" 平均波动率: {vol_mean:.2f}%")
|
||||||
|
if vol_mean > 3:
|
||||||
|
print(f" ⚠️ 共同点: 前一天波动率较高")
|
||||||
|
elif vol_mean < 1:
|
||||||
|
print(f" ✅ 共同点: 前一天波动率较低")
|
||||||
|
|
||||||
|
# 动量分析
|
||||||
|
mom_24_mean = features_df['prev_momentum_24'].mean()
|
||||||
|
print(f"\n3. 24小时动量:")
|
||||||
|
print(f" 平均动量: {mom_24_mean:.2f}%")
|
||||||
|
if abs(mom_24_mean) > 5:
|
||||||
|
print(f" 🚀 共同点: 前一天有强趋势({'+' if mom_24_mean > 0 else ''}{mom_24_mean:.1f}%)")
|
||||||
|
else:
|
||||||
|
print(f" 🔄 共同点: 前一天横盘震荡")
|
||||||
|
|
||||||
|
# 布林带位置
|
||||||
|
bb_pos_mean = features_df['prev_bb_position'].mean()
|
||||||
|
print(f"\n4. 布林带位置:")
|
||||||
|
print(f" 平均位置: {bb_pos_mean:.1f}%")
|
||||||
|
if bb_pos_mean > 80:
|
||||||
|
print(f" 🔴 共同点: 前一天价格接近布林上轨")
|
||||||
|
elif bb_pos_mean < 20:
|
||||||
|
print(f" 🟢 共同点: 前一天价格接近布林下轨")
|
||||||
|
else:
|
||||||
|
print(f" ➖ 共同点: 前一天价格在布林带中部")
|
||||||
|
|
||||||
|
# 成交量
|
||||||
|
vol_ratio_mean = features_df['prev_volume_ratio'].mean()
|
||||||
|
print(f"\n5. 成交量:")
|
||||||
|
print(f" 平均成交量比: {vol_ratio_mean:.2f}x")
|
||||||
|
if vol_ratio_mean > 1.5:
|
||||||
|
print(f" 📈 共同点: 前一天成交量显著放大")
|
||||||
|
elif vol_ratio_mean < 0.8:
|
||||||
|
print(f" 📉 共同点: 前一天成交量萎缩")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 技术特征分析完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 保存详细数据
|
||||||
|
output_file = "./results/low_accuracy_prev_day_features.csv"
|
||||||
|
os.makedirs("./results", exist_ok=True)
|
||||||
|
features_df.to_csv(output_file, index=False)
|
||||||
|
print(f"\n✓ 详细数据已保存至: {output_file}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"meta":{"test_sets":["test"],"test_metrics":[{"best_value":"Max","name":"Accuracy"},{"best_value":"Min","name":"Logloss"}],"learn_metrics":[{"best_value":"Max","name":"Accuracy"},{"best_value":"Min","name":"Logloss"}],"launch_mode":"Train","parameters":"","iteration_count":1000,"learn_sets":["learn"],"name":"experiment"},
|
||||||
|
"iterations":[
|
||||||
|
{"learn":[0.5470774019,0.6925856518],"iteration":0,"passed_time":0.06120060416,"remaining_time":61.13940356,"test":[0.5215327622,0.6929069842]},
|
||||||
|
{"learn":[0.5485192475,0.6920296194],"iteration":1,"passed_time":0.06652548291,"remaining_time":33.19621597,"test":[0.5201435517,0.6926980686]},
|
||||||
|
{"learn":[0.5508078912,0.6915437145],"iteration":2,"passed_time":0.07133867461,"remaining_time":23.70821953,"test":[0.5235008104,0.6925995058]},
|
||||||
|
{"learn":[0.5511969607,0.6911042525],"iteration":3,"passed_time":0.07626899597,"remaining_time":18.99098,"test":[0.5243111831,0.6923741119]},
|
||||||
|
{"learn":[0.5528218977,0.6906429178],"iteration":4,"passed_time":0.08172279652,"remaining_time":16.26283651,"test":[0.5240796481,0.6922359346]},
|
||||||
|
{"learn":[0.553096535,0.6902328044],"iteration":5,"passed_time":0.08686295957,"remaining_time":14.39029697,"test":[0.5217642973,0.6921532273]},
|
||||||
|
{"learn":[0.5537144688,0.6899070645],"iteration":6,"passed_time":0.0917179446,"remaining_time":13.01084557,"test":[0.5241954156,0.6920533177]},
|
||||||
|
{"learn":[0.552730352,0.6895966245],"iteration":7,"passed_time":0.09718091218,"remaining_time":12.05043311,"test":[0.520838157,0.6919667553]},
|
||||||
|
{"learn":[0.5539204467,0.6892625204],"iteration":8,"passed_time":0.1018938082,"remaining_time":11.21964044,"test":[0.520838157,0.6919667553]},
|
||||||
|
{"learn":[0.554561267,0.6889386437],"iteration":9,"passed_time":0.1079246734,"remaining_time":10.68454267,"test":[0.520838157,0.6918984047]},
|
||||||
|
{"learn":[0.554561267,0.6886345552],"iteration":10,"passed_time":0.1134390597,"remaining_time":10.19920273,"test":[0.5213012271,0.6918473786]},
|
||||||
|
{"learn":[0.5548587907,0.6883728473],"iteration":11,"passed_time":0.1182243753,"remaining_time":9.7338069,"test":[0.5217642973,0.691760433]},
|
||||||
|
{"learn":[0.5552020872,0.6881185803],"iteration":12,"passed_time":0.1234375829,"remaining_time":9.371761104,"test":[0.5231535078,0.6917269838]},
|
||||||
|
{"learn":[0.5564150684,0.6879043975],"iteration":13,"passed_time":0.1290524316,"remaining_time":9.088978394,"test":[0.5210696921,0.6916992448]},
|
||||||
|
{"learn":[0.5559802261,0.6877314929],"iteration":14,"passed_time":0.1346781556,"remaining_time":8.843865552,"test":[0.522343135,0.6916911046]},
|
||||||
|
{"learn":[0.5564150684,0.6875118582],"iteration":15,"passed_time":0.1392779638,"remaining_time":8.565594775,"test":[0.5213012271,0.6916992139]},
|
||||||
|
{"learn":[0.5569414565,0.6872755986],"iteration":16,"passed_time":0.1448462273,"remaining_time":8.375520082,"test":[0.5204908544,0.6917199845]},
|
||||||
|
{"learn":[0.5561862041,0.6870435386],"iteration":17,"passed_time":0.1499537223,"remaining_time":8.18080863,"test":[0.5226904376,0.6917129587]},
|
||||||
|
{"learn":[0.5561633176,0.6868442431],"iteration":18,"passed_time":0.1555536537,"remaining_time":8.031480752,"test":[0.5225746701,0.6916992772]},
|
||||||
|
{"learn":[0.5575593903,0.6866694237],"iteration":19,"passed_time":0.1608244053,"remaining_time":7.88039586,"test":[0.5207223894,0.6917050845]},
|
||||||
|
{"learn":[0.5575593903,0.686503203],"iteration":20,"passed_time":0.1667731839,"remaining_time":7.774807002,"test":[0.5247742533,0.6916027729]},
|
||||||
|
{"learn":[0.557124548,0.6863452751],"iteration":21,"passed_time":0.171689088,"remaining_time":7.632360368,"test":[0.5239638805,0.6916046624]},
|
||||||
|
{"learn":[0.5569185701,0.6862172686],"iteration":22,"passed_time":0.1765527817,"remaining_time":7.49965512,"test":[0.5241954156,0.6916203654]},
|
||||||
|
{"learn":[0.5573762988,0.6860730724],"iteration":23,"passed_time":0.1818841191,"remaining_time":7.396620842,"test":[0.5243111831,0.6916491893]},
|
||||||
|
{"learn":[0.5571703209,0.6859302623],"iteration":24,"passed_time":0.186612224,"remaining_time":7.277876738,"test":[0.5244269507,0.6916666458]},
|
||||||
|
{"learn":[0.5570101158,0.6858087212],"iteration":25,"passed_time":0.1914440415,"remaining_time":7.171788323,"test":[0.5235008104,0.691666967]},
|
||||||
|
{"learn":[0.5571474344,0.6857036471],"iteration":26,"passed_time":0.1967539613,"remaining_time":7.090429791,"test":[0.5229219727,0.6916827648]},
|
||||||
|
{"learn":[0.5571932073,0.6855803136],"iteration":27,"passed_time":0.2023491842,"remaining_time":7.024407393,"test":[0.5245427182,0.6917018334]},
|
||||||
|
{"learn":[0.5572160937,0.6854590307],"iteration":28,"passed_time":0.2073690924,"remaining_time":6.943289267,"test":[0.5222273674,0.6916998315]},
|
||||||
|
{"learn":[0.5568041379,0.6853245403],"iteration":29,"passed_time":0.2177755069,"remaining_time":7.041408056,"test":[0.5224589025,0.6917500551]},
|
||||||
|
{"learn":[0.5570558887,0.6852010211],"iteration":30,"passed_time":0.2259016222,"remaining_time":7.061247481,"test":[0.5222273674,0.691867972]},
|
||||||
|
{"learn":[0.5570330022,0.6850648161],"iteration":31,"passed_time":0.2344519627,"remaining_time":7.092171873,"test":[0.5215327622,0.6918745283]},
|
||||||
|
{"learn":[0.5578340276,0.6849486439],"iteration":32,"passed_time":0.2411928145,"remaining_time":7.067680354,"test":[0.5214169947,0.691873578]},
|
||||||
|
{"learn":[0.5576738225,0.6848167763],"iteration":33,"passed_time":0.2475354838,"remaining_time":7.032919923,"test":[0.5215327622,0.6918621914]},
|
||||||
|
{"learn":[0.5577424818,0.6847064338],"iteration":34,"passed_time":0.255081701,"remaining_time":7.0329669,"test":[0.5222273674,0.691870106]},
|
||||||
|
{"learn":[0.5582917563,0.6846073419],"iteration":35,"passed_time":0.2616022524,"remaining_time":7.005126981,"test":[0.5219958324,0.6919348629]},
|
||||||
|
{"learn":[0.5587037122,0.6845016816],"iteration":36,"passed_time":0.2691936381,"remaining_time":7.006310094,"test":[0.5245427182,0.6920133967]},
|
||||||
|
{"learn":[0.5587494851,0.6844106082],"iteration":37,"passed_time":0.2758961134,"remaining_time":6.984527923,"test":[0.5246584858,0.6920145372]},
|
||||||
|
{"learn":[0.5591156681,0.6843432648],"iteration":38,"passed_time":0.2833089086,"remaining_time":6.981022081,"test":[0.5247742533,0.6920231409]},
|
||||||
|
{"learn":[0.5589325766,0.6842678359],"iteration":39,"passed_time":0.2897567071,"remaining_time":6.95416097,"test":[0.5248900208,0.6920429941]},
|
||||||
|
{"learn":[0.5590012359,0.6841515192],"iteration":40,"passed_time":0.3004245903,"remaining_time":7.027004441,"test":[0.5248900208,0.6920318228]},
|
||||||
|
{"learn":[0.5590698952,0.6840610381],"iteration":41,"passed_time":0.3083540311,"remaining_time":7.033408614,"test":[0.5252373235,0.6920202842]},
|
||||||
|
{"learn":[0.5593445324,0.6839565281],"iteration":42,"passed_time":0.3168845792,"remaining_time":7.052524239,"test":[0.525353091,0.6920430337]},
|
||||||
|
{"learn":[0.5592529867,0.6838539206],"iteration":43,"passed_time":0.3241559521,"remaining_time":7.043024778,"test":[0.5247742533,0.6920265347]},
|
||||||
|
{"learn":[0.559321646,0.6837417759],"iteration":44,"passed_time":0.3312213169,"remaining_time":7.029252391,"test":[0.525353091,0.6920697067]},
|
||||||
|
{"learn":[0.559687829,0.6836669298],"iteration":45,"passed_time":0.3364545253,"remaining_time":6.977774285,"test":[0.5263949988,0.692116771]},
|
||||||
|
{"learn":[0.5603744221,0.6835634424],"iteration":46,"passed_time":0.3416096056,"remaining_time":6.926679875,"test":[0.5259319287,0.692098178]},
|
||||||
|
{"learn":[0.5599853527,0.6834698484],"iteration":47,"passed_time":0.3493788734,"remaining_time":6.929347655,"test":[0.5260476962,0.692152499]},
|
||||||
|
{"learn":[0.5601913306,0.6833860083],"iteration":48,"passed_time":0.3567442501,"remaining_time":6.923750649,"test":[0.5262792313,0.6922137468]},
|
||||||
|
{"learn":[0.5602599899,0.6833086288],"iteration":49,"passed_time":0.3643377608,"remaining_time":6.922417456,"test":[0.5270896041,0.6922167687]},
|
||||||
|
{"learn":[0.5606490594,0.683239475],"iteration":50,"passed_time":0.3696800569,"remaining_time":6.87894851,"test":[0.5272053716,0.6922875182]},
|
||||||
|
{"learn":[0.5609694695,0.6831625258],"iteration":51,"passed_time":0.3754608288,"remaining_time":6.844939726,"test":[0.526858069,0.6923662256]},
|
||||||
|
{"learn":[0.5611983339,0.683056699],"iteration":52,"passed_time":0.3809127543,"remaining_time":6.806120346,"test":[0.526858069,0.6923707756]},
|
||||||
|
{"learn":[0.5617018355,0.6829611591],"iteration":53,"passed_time":0.3866230651,"remaining_time":6.773063325,"test":[0.5270896041,0.6923776257]},
|
||||||
|
{"learn":[0.5619993592,0.6828690012],"iteration":54,"passed_time":0.3917164763,"remaining_time":6.730401274,"test":[0.5259319287,0.692400255]},
|
||||||
|
{"learn":[0.5620451321,0.6827776665],"iteration":55,"passed_time":0.3965791283,"remaining_time":6.685191019,"test":[0.5266265339,0.692269877]},
|
||||||
|
{"learn":[0.5624342015,0.6826927644],"iteration":56,"passed_time":0.4017419589,"remaining_time":6.646362583,"test":[0.5260476962,0.6922932744]},
|
||||||
|
{"learn":[0.5626401794,0.6825957686],"iteration":57,"passed_time":0.4064048529,"remaining_time":6.60057537,"test":[0.5266265339,0.6923264029]},
|
||||||
|
{"learn":[0.5626630659,0.6825449467],"iteration":58,"passed_time":0.4116929385,"remaining_time":6.566153478,"test":[0.5263949988,0.692314059]},
|
||||||
|
{"learn":[0.5627088387,0.6824559998],"iteration":59,"passed_time":0.4168931457,"remaining_time":6.531325949,"test":[0.5263949988,0.6923015601]},
|
||||||
|
{"learn":[0.562983476,0.6823684725],"iteration":60,"passed_time":0.4216150837,"remaining_time":6.4901076,"test":[0.5263949988,0.6923592968]},
|
||||||
|
{"learn":[0.5632352268,0.6822923813],"iteration":61,"passed_time":0.4271669298,"remaining_time":6.462622261,"test":[0.5267423015,0.6923476579]},
|
||||||
|
{"learn":[0.5630521353,0.6822333406],"iteration":62,"passed_time":0.4320567079,"remaining_time":6.425986275,"test":[0.5265107664,0.692348824]},
|
||||||
|
{"learn":[0.5634412047,0.6821367311],"iteration":63,"passed_time":0.4365471784,"remaining_time":6.384502485,"test":[0.5259319287,0.6923600086]},
|
||||||
|
{"learn":[0.5636242962,0.6820867227],"iteration":64,"passed_time":0.441430998,"remaining_time":6.349815124,"test":[0.5254688585,0.692372751]},
|
||||||
|
{"learn":[0.563715842,0.6820256999],"iteration":65,"passed_time":0.4463418185,"remaining_time":6.316413008,"test":[0.5260476962,0.6924455742]},
|
||||||
|
{"learn":[0.5638531606,0.6819329129],"iteration":66,"passed_time":0.4521836345,"remaining_time":6.296825836,"test":[0.5255846261,0.6924748157]},
|
||||||
|
{"learn":[0.5638989335,0.6818706983],"iteration":67,"passed_time":0.4572557949,"remaining_time":6.26709413,"test":[0.5273211392,0.6924789867]},
|
||||||
|
{"learn":[0.5639447064,0.6818045601],"iteration":68,"passed_time":0.4627077204,"remaining_time":6.24320127,"test":[0.5267423015,0.6924876711]},
|
||||||
|
{"learn":[0.564082025,0.68171304],"iteration":69,"passed_time":0.4675020363,"remaining_time":6.211098482,"test":[0.5266265339,0.692489792]},
|
||||||
|
{"learn":[0.5640362521,0.6816251881],"iteration":70,"passed_time":0.4724773595,"remaining_time":6.182133337,"test":[0.5266265339,0.6924869911]},
|
||||||
|
{"learn":[0.5645168673,0.6815203712],"iteration":71,"passed_time":0.4776511072,"remaining_time":6.156392048,"test":[0.5267423015,0.6925206162]},
|
||||||
|
{"learn":[0.564448208,0.6814559429],"iteration":72,"passed_time":0.4825719282,"remaining_time":6.12800243,"test":[0.5262792313,0.6925313913]},
|
||||||
|
{"learn":[0.5646541859,0.6813334168],"iteration":73,"passed_time":0.4878153037,"remaining_time":6.104283395,"test":[0.5258161611,0.6925367884]},
|
||||||
|
{"learn":[0.5649059367,0.6812138418],"iteration":74,"passed_time":0.493698288,"remaining_time":6.088945552,"test":[0.5265107664,0.6925626459]},
|
||||||
|
{"learn":[0.5649059367,0.6811428702],"iteration":75,"passed_time":0.4991032116,"remaining_time":6.068044309,"test":[0.5273211392,0.6925711713]},
|
||||||
|
{"learn":[0.565180574,0.6810679335],"iteration":76,"passed_time":0.5041589963,"remaining_time":6.043360436,"test":[0.526858069,0.6926776812]},
|
||||||
|
{"learn":[0.5653407791,0.6810189357],"iteration":77,"passed_time":0.509317035,"remaining_time":6.020388543,"test":[0.5274369067,0.6926884897]},
|
||||||
|
{"learn":[0.5655696434,0.6809239905],"iteration":78,"passed_time":0.5151628512,"remaining_time":6.005885898,"test":[0.5266265339,0.6926819413]},
|
||||||
|
{"learn":[0.5651119147,0.6808452293],"iteration":79,"passed_time":0.5199159989,"remaining_time":5.979033987,"test":[0.5272053716,0.692682082]},
|
||||||
|
{"learn":[0.5653407791,0.6807843648],"iteration":80,"passed_time":0.5252913379,"remaining_time":5.959786908,"test":[0.5270896041,0.6927326486]},
|
||||||
|
{"learn":[0.565180574,0.6807084812],"iteration":81,"passed_time":0.5302294096,"remaining_time":5.935982903,"test":[0.5266265339,0.69272742]},
|
||||||
|
{"learn":[0.5654780977,0.6806237044],"iteration":82,"passed_time":0.5348115921,"remaining_time":5.908701566,"test":[0.5272053716,0.6927260891]},
|
||||||
|
{"learn":[0.5656840756,0.680541791],"iteration":83,"passed_time":0.5399401714,"remaining_time":5.887919012,"test":[0.5267423015,0.6927850957]},
|
||||||
|
{"learn":[0.5653636655,0.6804402561],"iteration":84,"passed_time":0.5459297433,"remaining_time":5.876773119,"test":[0.5266265339,0.6928797523]},
|
||||||
|
{"learn":[0.565546757,0.6803841011],"iteration":85,"passed_time":0.5557997614,"remaining_time":5.906988161,"test":[0.526858069,0.6928753106]},
|
||||||
|
{"learn":[0.5657298485,0.6803302076],"iteration":86,"passed_time":0.5625750729,"remaining_time":5.903805075,"test":[0.525353091,0.6929332764]},
|
||||||
|
{"learn":[0.5663248959,0.6802294187],"iteration":87,"passed_time":0.5682492989,"remaining_time":5.889129097,"test":[0.5265107664,0.6929225447]},
|
||||||
|
{"learn":[0.5660273722,0.6801427052],"iteration":88,"passed_time":0.5754560859,"remaining_time":5.890342632,"test":[0.5262792313,0.6929127979]},
|
||||||
|
{"learn":[0.5664393281,0.680092343],"iteration":89,"passed_time":0.5809644303,"remaining_time":5.874195906,"test":[0.5278999768,0.6929168776]},
|
||||||
|
{"learn":[0.5666224196,0.6800188515],"iteration":90,"passed_time":0.5858589586,"remaining_time":5.852151575,"test":[0.5261634638,0.6930123071]},
|
||||||
|
{"learn":[0.567377672,0.6799633931],"iteration":91,"passed_time":0.5921711266,"remaining_time":5.844471554,"test":[0.5260476962,0.6930271402]},
|
||||||
|
{"learn":[0.5676523092,0.6799142593],"iteration":92,"passed_time":0.597831102,"remaining_time":5.830460318,"test":[0.525353091,0.693074147]},
|
||||||
|
{"learn":[0.5677667414,0.6798218244],"iteration":93,"passed_time":0.602484454,"remaining_time":5.806924632,"test":[0.5257003936,0.693098238]},
|
||||||
|
{"learn":[0.5679727194,0.6797624264],"iteration":94,"passed_time":0.6079672558,"remaining_time":5.791688068,"test":[0.5252373235,0.6931168231]},
|
||||||
|
{"learn":[0.5682702431,0.6796825487],"iteration":95,"passed_time":0.6136230643,"remaining_time":5.778283856,"test":[0.5252373235,0.6930889734]},
|
||||||
|
{"learn":[0.5683160159,0.6796119175],"iteration":96,"passed_time":0.6183110011,"remaining_time":5.756029216,"test":[0.5265107664,0.6931814079]},
|
||||||
|
{"learn":[0.5681786973,0.6795251979],"iteration":97,"passed_time":0.6239766434,"remaining_time":5.743131962,"test":[0.5265107664,0.6935736538]},
|
||||||
|
{"learn":[0.5684533346,0.6794647206],"iteration":98,"passed_time":0.6295706578,"remaining_time":5.729728916,"test":[0.5257003936,0.6935709125]},
|
||||||
|
{"learn":[0.5690026091,0.6793963783],"iteration":99,"passed_time":0.634798366,"remaining_time":5.713185294,"test":[0.5262792313,0.6935839531]},
|
||||||
|
{"learn":[0.5689797226,0.6793126045],"iteration":100,"passed_time":0.6405399697,"remaining_time":5.701439929,"test":[0.5250057884,0.6936165151]},
|
||||||
|
{"learn":[0.569208587,0.6792105367],"iteration":101,"passed_time":0.6456661739,"remaining_time":5.684394354,"test":[0.5250057884,0.6936424871]},
|
||||||
|
{"learn":[0.5693687921,0.6791494493],"iteration":102,"passed_time":0.6504003208,"remaining_time":5.6641659,"test":[0.5246584858,0.6936432847]},
|
||||||
|
{"learn":[0.5695518836,0.6790732211],"iteration":103,"passed_time":0.6554747729,"remaining_time":5.647167274,"test":[0.5245427182,0.6936493519]},
|
||||||
|
{"learn":[0.5698265208,0.6789905315],"iteration":104,"passed_time":0.6618330678,"remaining_time":5.641339006,"test":[0.5248900208,0.6936865636]},
|
||||||
|
{"learn":[0.5698951801,0.67895488],"iteration":105,"passed_time":0.667103236,"remaining_time":5.626323519,"test":[0.5252373235,0.693675143]},
|
||||||
|
{"learn":[0.5700324987,0.6788948137],"iteration":106,"passed_time":0.6723126518,"remaining_time":5.61098316,"test":[0.5255846261,0.6936874053]},
|
||||||
|
{"learn":[0.5704902275,0.6788457101],"iteration":107,"passed_time":0.6775288179,"remaining_time":5.595886163,"test":[0.5254688585,0.693689784]},
|
||||||
|
{"learn":[0.5704902275,0.6787737664],"iteration":108,"passed_time":0.6826580222,"remaining_time":5.580259613,"test":[0.5251215559,0.6936707742]},
|
||||||
|
{"learn":[0.5708106376,0.6786897689],"iteration":109,"passed_time":0.6879676087,"remaining_time":5.566283379,"test":[0.5241954156,0.6937073345]},
|
||||||
|
{"learn":[0.5709021834,0.6786357569],"iteration":110,"passed_time":0.6932970293,"remaining_time":5.552622153,"test":[0.5243111831,0.6937614267]},
|
||||||
|
{"learn":[0.5711768206,0.6785404316],"iteration":111,"passed_time":0.6991878889,"remaining_time":5.543561119,"test":[0.5219958324,0.6937989599]},
|
||||||
|
{"learn":[0.5714972307,0.6784635222],"iteration":112,"passed_time":0.7044325144,"remaining_time":5.529483542,"test":[0.5218800648,0.6938027615]},
|
||||||
|
{"learn":[0.5716345494,0.6783830081],"iteration":113,"passed_time":0.7094082543,"remaining_time":5.513471169,"test":[0.5232692753,0.6939119665]},
|
||||||
|
{"learn":[0.5716803222,0.6783261377],"iteration":114,"passed_time":0.714549459,"remaining_time":5.498924098,"test":[0.5235008104,0.6939374584]},
|
||||||
|
{"learn":[0.5717947544,0.6782551829],"iteration":115,"passed_time":0.7192722305,"remaining_time":5.481350446,"test":[0.5229219727,0.6939266092]},
|
||||||
|
{"learn":[0.5717260951,0.6782039946],"iteration":116,"passed_time":0.7258457006,"remaining_time":5.477963706,"test":[0.5222273674,0.6939398216]},
|
||||||
|
{"learn":[0.5719549595,0.6781381441],"iteration":117,"passed_time":0.7316315144,"remaining_time":5.468635557,"test":[0.5221115999,0.6939581303]},
|
||||||
|
{"learn":[0.5720007324,0.6780959678],"iteration":118,"passed_time":0.7375646257,"remaining_time":5.460457439,"test":[0.522343135,0.6940189884]},
|
||||||
|
{"learn":[0.5721609374,0.6780580972],"iteration":119,"passed_time":0.7444090649,"remaining_time":5.45899981,"test":[0.5224589025,0.6940203223]},
|
||||||
|
{"learn":[0.5722982561,0.6779909469],"iteration":120,"passed_time":0.7511739177,"remaining_time":5.456874989,"test":[0.5228062051,0.6940373975]},
|
||||||
|
{"learn":[0.5722753696,0.6779155506],"iteration":121,"passed_time":0.7581201944,"remaining_time":5.455979759,"test":[0.5216485298,0.6940836851]},
|
||||||
|
{"learn":[0.5726186662,0.67787157],"iteration":122,"passed_time":0.7638367137,"remaining_time":5.446217869,"test":[0.5217642973,0.6940971538]},
|
||||||
|
{"learn":[0.5729161899,0.6778021605],"iteration":123,"passed_time":0.7741768755,"remaining_time":5.469185024,"test":[0.5222273674,0.6941038908]},
|
||||||
|
{"learn":[0.5732594864,0.6777441308],"iteration":124,"passed_time":0.7811099433,"remaining_time":5.467769603,"test":[0.5222273674,0.6941318899]},
|
||||||
|
{"learn":[0.5737172152,0.6776640278],"iteration":125,"passed_time":0.7923285151,"remaining_time":5.495993034,"test":[0.5245427182,0.6941318581]},
|
||||||
|
{"learn":[0.5741062846,0.6775768826],"iteration":126,"passed_time":0.7972769206,"remaining_time":5.480494108,"test":[0.5240796481,0.6939774214]},
|
||||||
|
{"learn":[0.5738545338,0.6775322572],"iteration":127,"passed_time":0.8045955871,"remaining_time":5.481307437,"test":[0.5246584858,0.6939790446]},
|
||||||
|
{"learn":[0.5738545338,0.6774381457],"iteration":128,"passed_time":0.8096396213,"remaining_time":5.466636513,"test":[0.5244269507,0.6939875313]},
|
||||||
|
{"learn":[0.5739460796,0.6773933791],"iteration":129,"passed_time":0.8141404672,"remaining_time":5.448478511,"test":[0.5239638805,0.6940065251]},
|
||||||
|
{"learn":[0.5738545338,0.6773073305],"iteration":130,"passed_time":0.8197628995,"remaining_time":5.437969158,"test":[0.5237323455,0.6940062666]},
|
||||||
|
{"learn":[0.5746555591,0.6772430348],"iteration":131,"passed_time":0.8257601383,"remaining_time":5.429998485,"test":[0.5231535078,0.6940655128]},
|
||||||
|
{"learn":[0.5745640134,0.6771630132],"iteration":132,"passed_time":0.8309698042,"remaining_time":5.41692346,"test":[0.5226904376,0.6940488902]},
|
||||||
|
{"learn":[0.5747242184,0.6770868641],"iteration":133,"passed_time":0.8383795993,"remaining_time":5.418184574,"test":[0.5221115999,0.6940625077]},
|
||||||
|
{"learn":[0.5749988557,0.6770144627],"iteration":134,"passed_time":0.8433877987,"remaining_time":5.403929229,"test":[0.5225746701,0.6940868198]},
|
||||||
|
{"learn":[0.5747699913,0.6769488704],"iteration":135,"passed_time":0.848858975,"remaining_time":5.392751135,"test":[0.5229219727,0.6940640084]},
|
||||||
|
{"learn":[0.5751590607,0.6768712881],"iteration":136,"passed_time":0.8548430883,"remaining_time":5.384887483,"test":[0.5232692753,0.6941097371]},
|
||||||
|
{"learn":[0.5755939031,0.6767999464],"iteration":137,"passed_time":0.860643611,"remaining_time":5.375904295,"test":[0.5244269507,0.6940823091]},
|
||||||
|
{"learn":[0.5755710166,0.6767315844],"iteration":138,"passed_time":0.8670014892,"remaining_time":5.370419297,"test":[0.5237323455,0.6941720366]},
|
||||||
|
{"learn":[0.5758914267,0.6766580612],"iteration":139,"passed_time":0.8721688617,"remaining_time":5.357608722,"test":[0.5235008104,0.6942806828]}
|
||||||
|
]}
|
||||||
Binary file not shown.
@@ -0,0 +1,141 @@
|
|||||||
|
iter Accuracy Logloss
|
||||||
|
0 0.5470774019 0.6925856518
|
||||||
|
1 0.5485192475 0.6920296194
|
||||||
|
2 0.5508078912 0.6915437145
|
||||||
|
3 0.5511969607 0.6911042525
|
||||||
|
4 0.5528218977 0.6906429178
|
||||||
|
5 0.553096535 0.6902328044
|
||||||
|
6 0.5537144688 0.6899070645
|
||||||
|
7 0.552730352 0.6895966245
|
||||||
|
8 0.5539204467 0.6892625204
|
||||||
|
9 0.554561267 0.6889386437
|
||||||
|
10 0.554561267 0.6886345552
|
||||||
|
11 0.5548587907 0.6883728473
|
||||||
|
12 0.5552020872 0.6881185803
|
||||||
|
13 0.5564150684 0.6879043975
|
||||||
|
14 0.5559802261 0.6877314929
|
||||||
|
15 0.5564150684 0.6875118582
|
||||||
|
16 0.5569414565 0.6872755986
|
||||||
|
17 0.5561862041 0.6870435386
|
||||||
|
18 0.5561633176 0.6868442431
|
||||||
|
19 0.5575593903 0.6866694237
|
||||||
|
20 0.5575593903 0.686503203
|
||||||
|
21 0.557124548 0.6863452751
|
||||||
|
22 0.5569185701 0.6862172686
|
||||||
|
23 0.5573762988 0.6860730724
|
||||||
|
24 0.5571703209 0.6859302623
|
||||||
|
25 0.5570101158 0.6858087212
|
||||||
|
26 0.5571474344 0.6857036471
|
||||||
|
27 0.5571932073 0.6855803136
|
||||||
|
28 0.5572160937 0.6854590307
|
||||||
|
29 0.5568041379 0.6853245403
|
||||||
|
30 0.5570558887 0.6852010211
|
||||||
|
31 0.5570330022 0.6850648161
|
||||||
|
32 0.5578340276 0.6849486439
|
||||||
|
33 0.5576738225 0.6848167763
|
||||||
|
34 0.5577424818 0.6847064338
|
||||||
|
35 0.5582917563 0.6846073419
|
||||||
|
36 0.5587037122 0.6845016816
|
||||||
|
37 0.5587494851 0.6844106082
|
||||||
|
38 0.5591156681 0.6843432648
|
||||||
|
39 0.5589325766 0.6842678359
|
||||||
|
40 0.5590012359 0.6841515192
|
||||||
|
41 0.5590698952 0.6840610381
|
||||||
|
42 0.5593445324 0.6839565281
|
||||||
|
43 0.5592529867 0.6838539206
|
||||||
|
44 0.559321646 0.6837417759
|
||||||
|
45 0.559687829 0.6836669298
|
||||||
|
46 0.5603744221 0.6835634424
|
||||||
|
47 0.5599853527 0.6834698484
|
||||||
|
48 0.5601913306 0.6833860083
|
||||||
|
49 0.5602599899 0.6833086288
|
||||||
|
50 0.5606490594 0.683239475
|
||||||
|
51 0.5609694695 0.6831625258
|
||||||
|
52 0.5611983339 0.683056699
|
||||||
|
53 0.5617018355 0.6829611591
|
||||||
|
54 0.5619993592 0.6828690012
|
||||||
|
55 0.5620451321 0.6827776665
|
||||||
|
56 0.5624342015 0.6826927644
|
||||||
|
57 0.5626401794 0.6825957686
|
||||||
|
58 0.5626630659 0.6825449467
|
||||||
|
59 0.5627088387 0.6824559998
|
||||||
|
60 0.562983476 0.6823684725
|
||||||
|
61 0.5632352268 0.6822923813
|
||||||
|
62 0.5630521353 0.6822333406
|
||||||
|
63 0.5634412047 0.6821367311
|
||||||
|
64 0.5636242962 0.6820867227
|
||||||
|
65 0.563715842 0.6820256999
|
||||||
|
66 0.5638531606 0.6819329129
|
||||||
|
67 0.5638989335 0.6818706983
|
||||||
|
68 0.5639447064 0.6818045601
|
||||||
|
69 0.564082025 0.68171304
|
||||||
|
70 0.5640362521 0.6816251881
|
||||||
|
71 0.5645168673 0.6815203712
|
||||||
|
72 0.564448208 0.6814559429
|
||||||
|
73 0.5646541859 0.6813334168
|
||||||
|
74 0.5649059367 0.6812138418
|
||||||
|
75 0.5649059367 0.6811428702
|
||||||
|
76 0.565180574 0.6810679335
|
||||||
|
77 0.5653407791 0.6810189357
|
||||||
|
78 0.5655696434 0.6809239905
|
||||||
|
79 0.5651119147 0.6808452293
|
||||||
|
80 0.5653407791 0.6807843648
|
||||||
|
81 0.565180574 0.6807084812
|
||||||
|
82 0.5654780977 0.6806237044
|
||||||
|
83 0.5656840756 0.680541791
|
||||||
|
84 0.5653636655 0.6804402561
|
||||||
|
85 0.565546757 0.6803841011
|
||||||
|
86 0.5657298485 0.6803302076
|
||||||
|
87 0.5663248959 0.6802294187
|
||||||
|
88 0.5660273722 0.6801427052
|
||||||
|
89 0.5664393281 0.680092343
|
||||||
|
90 0.5666224196 0.6800188515
|
||||||
|
91 0.567377672 0.6799633931
|
||||||
|
92 0.5676523092 0.6799142593
|
||||||
|
93 0.5677667414 0.6798218244
|
||||||
|
94 0.5679727194 0.6797624264
|
||||||
|
95 0.5682702431 0.6796825487
|
||||||
|
96 0.5683160159 0.6796119175
|
||||||
|
97 0.5681786973 0.6795251979
|
||||||
|
98 0.5684533346 0.6794647206
|
||||||
|
99 0.5690026091 0.6793963783
|
||||||
|
100 0.5689797226 0.6793126045
|
||||||
|
101 0.569208587 0.6792105367
|
||||||
|
102 0.5693687921 0.6791494493
|
||||||
|
103 0.5695518836 0.6790732211
|
||||||
|
104 0.5698265208 0.6789905315
|
||||||
|
105 0.5698951801 0.67895488
|
||||||
|
106 0.5700324987 0.6788948137
|
||||||
|
107 0.5704902275 0.6788457101
|
||||||
|
108 0.5704902275 0.6787737664
|
||||||
|
109 0.5708106376 0.6786897689
|
||||||
|
110 0.5709021834 0.6786357569
|
||||||
|
111 0.5711768206 0.6785404316
|
||||||
|
112 0.5714972307 0.6784635222
|
||||||
|
113 0.5716345494 0.6783830081
|
||||||
|
114 0.5716803222 0.6783261377
|
||||||
|
115 0.5717947544 0.6782551829
|
||||||
|
116 0.5717260951 0.6782039946
|
||||||
|
117 0.5719549595 0.6781381441
|
||||||
|
118 0.5720007324 0.6780959678
|
||||||
|
119 0.5721609374 0.6780580972
|
||||||
|
120 0.5722982561 0.6779909469
|
||||||
|
121 0.5722753696 0.6779155506
|
||||||
|
122 0.5726186662 0.67787157
|
||||||
|
123 0.5729161899 0.6778021605
|
||||||
|
124 0.5732594864 0.6777441308
|
||||||
|
125 0.5737172152 0.6776640278
|
||||||
|
126 0.5741062846 0.6775768826
|
||||||
|
127 0.5738545338 0.6775322572
|
||||||
|
128 0.5738545338 0.6774381457
|
||||||
|
129 0.5739460796 0.6773933791
|
||||||
|
130 0.5738545338 0.6773073305
|
||||||
|
131 0.5746555591 0.6772430348
|
||||||
|
132 0.5745640134 0.6771630132
|
||||||
|
133 0.5747242184 0.6770868641
|
||||||
|
134 0.5749988557 0.6770144627
|
||||||
|
135 0.5747699913 0.6769488704
|
||||||
|
136 0.5751590607 0.6768712881
|
||||||
|
137 0.5755939031 0.6767999464
|
||||||
|
138 0.5755710166 0.6767315844
|
||||||
|
139 0.5758914267 0.6766580612
|
||||||
|
Binary file not shown.
@@ -0,0 +1,141 @@
|
|||||||
|
iter Accuracy Logloss
|
||||||
|
0 0.5215327622 0.6929069842
|
||||||
|
1 0.5201435517 0.6926980686
|
||||||
|
2 0.5235008104 0.6925995058
|
||||||
|
3 0.5243111831 0.6923741119
|
||||||
|
4 0.5240796481 0.6922359346
|
||||||
|
5 0.5217642973 0.6921532273
|
||||||
|
6 0.5241954156 0.6920533177
|
||||||
|
7 0.520838157 0.6919667553
|
||||||
|
8 0.520838157 0.6919667553
|
||||||
|
9 0.520838157 0.6918984047
|
||||||
|
10 0.5213012271 0.6918473786
|
||||||
|
11 0.5217642973 0.691760433
|
||||||
|
12 0.5231535078 0.6917269838
|
||||||
|
13 0.5210696921 0.6916992448
|
||||||
|
14 0.522343135 0.6916911046
|
||||||
|
15 0.5213012271 0.6916992139
|
||||||
|
16 0.5204908544 0.6917199845
|
||||||
|
17 0.5226904376 0.6917129587
|
||||||
|
18 0.5225746701 0.6916992772
|
||||||
|
19 0.5207223894 0.6917050845
|
||||||
|
20 0.5247742533 0.6916027729
|
||||||
|
21 0.5239638805 0.6916046624
|
||||||
|
22 0.5241954156 0.6916203654
|
||||||
|
23 0.5243111831 0.6916491893
|
||||||
|
24 0.5244269507 0.6916666458
|
||||||
|
25 0.5235008104 0.691666967
|
||||||
|
26 0.5229219727 0.6916827648
|
||||||
|
27 0.5245427182 0.6917018334
|
||||||
|
28 0.5222273674 0.6916998315
|
||||||
|
29 0.5224589025 0.6917500551
|
||||||
|
30 0.5222273674 0.691867972
|
||||||
|
31 0.5215327622 0.6918745283
|
||||||
|
32 0.5214169947 0.691873578
|
||||||
|
33 0.5215327622 0.6918621914
|
||||||
|
34 0.5222273674 0.691870106
|
||||||
|
35 0.5219958324 0.6919348629
|
||||||
|
36 0.5245427182 0.6920133967
|
||||||
|
37 0.5246584858 0.6920145372
|
||||||
|
38 0.5247742533 0.6920231409
|
||||||
|
39 0.5248900208 0.6920429941
|
||||||
|
40 0.5248900208 0.6920318228
|
||||||
|
41 0.5252373235 0.6920202842
|
||||||
|
42 0.525353091 0.6920430337
|
||||||
|
43 0.5247742533 0.6920265347
|
||||||
|
44 0.525353091 0.6920697067
|
||||||
|
45 0.5263949988 0.692116771
|
||||||
|
46 0.5259319287 0.692098178
|
||||||
|
47 0.5260476962 0.692152499
|
||||||
|
48 0.5262792313 0.6922137468
|
||||||
|
49 0.5270896041 0.6922167687
|
||||||
|
50 0.5272053716 0.6922875182
|
||||||
|
51 0.526858069 0.6923662256
|
||||||
|
52 0.526858069 0.6923707756
|
||||||
|
53 0.5270896041 0.6923776257
|
||||||
|
54 0.5259319287 0.692400255
|
||||||
|
55 0.5266265339 0.692269877
|
||||||
|
56 0.5260476962 0.6922932744
|
||||||
|
57 0.5266265339 0.6923264029
|
||||||
|
58 0.5263949988 0.692314059
|
||||||
|
59 0.5263949988 0.6923015601
|
||||||
|
60 0.5263949988 0.6923592968
|
||||||
|
61 0.5267423015 0.6923476579
|
||||||
|
62 0.5265107664 0.692348824
|
||||||
|
63 0.5259319287 0.6923600086
|
||||||
|
64 0.5254688585 0.692372751
|
||||||
|
65 0.5260476962 0.6924455742
|
||||||
|
66 0.5255846261 0.6924748157
|
||||||
|
67 0.5273211392 0.6924789867
|
||||||
|
68 0.5267423015 0.6924876711
|
||||||
|
69 0.5266265339 0.692489792
|
||||||
|
70 0.5266265339 0.6924869911
|
||||||
|
71 0.5267423015 0.6925206162
|
||||||
|
72 0.5262792313 0.6925313913
|
||||||
|
73 0.5258161611 0.6925367884
|
||||||
|
74 0.5265107664 0.6925626459
|
||||||
|
75 0.5273211392 0.6925711713
|
||||||
|
76 0.526858069 0.6926776812
|
||||||
|
77 0.5274369067 0.6926884897
|
||||||
|
78 0.5266265339 0.6926819413
|
||||||
|
79 0.5272053716 0.692682082
|
||||||
|
80 0.5270896041 0.6927326486
|
||||||
|
81 0.5266265339 0.69272742
|
||||||
|
82 0.5272053716 0.6927260891
|
||||||
|
83 0.5267423015 0.6927850957
|
||||||
|
84 0.5266265339 0.6928797523
|
||||||
|
85 0.526858069 0.6928753106
|
||||||
|
86 0.525353091 0.6929332764
|
||||||
|
87 0.5265107664 0.6929225447
|
||||||
|
88 0.5262792313 0.6929127979
|
||||||
|
89 0.5278999768 0.6929168776
|
||||||
|
90 0.5261634638 0.6930123071
|
||||||
|
91 0.5260476962 0.6930271402
|
||||||
|
92 0.525353091 0.693074147
|
||||||
|
93 0.5257003936 0.693098238
|
||||||
|
94 0.5252373235 0.6931168231
|
||||||
|
95 0.5252373235 0.6930889734
|
||||||
|
96 0.5265107664 0.6931814079
|
||||||
|
97 0.5265107664 0.6935736538
|
||||||
|
98 0.5257003936 0.6935709125
|
||||||
|
99 0.5262792313 0.6935839531
|
||||||
|
100 0.5250057884 0.6936165151
|
||||||
|
101 0.5250057884 0.6936424871
|
||||||
|
102 0.5246584858 0.6936432847
|
||||||
|
103 0.5245427182 0.6936493519
|
||||||
|
104 0.5248900208 0.6936865636
|
||||||
|
105 0.5252373235 0.693675143
|
||||||
|
106 0.5255846261 0.6936874053
|
||||||
|
107 0.5254688585 0.693689784
|
||||||
|
108 0.5251215559 0.6936707742
|
||||||
|
109 0.5241954156 0.6937073345
|
||||||
|
110 0.5243111831 0.6937614267
|
||||||
|
111 0.5219958324 0.6937989599
|
||||||
|
112 0.5218800648 0.6938027615
|
||||||
|
113 0.5232692753 0.6939119665
|
||||||
|
114 0.5235008104 0.6939374584
|
||||||
|
115 0.5229219727 0.6939266092
|
||||||
|
116 0.5222273674 0.6939398216
|
||||||
|
117 0.5221115999 0.6939581303
|
||||||
|
118 0.522343135 0.6940189884
|
||||||
|
119 0.5224589025 0.6940203223
|
||||||
|
120 0.5228062051 0.6940373975
|
||||||
|
121 0.5216485298 0.6940836851
|
||||||
|
122 0.5217642973 0.6940971538
|
||||||
|
123 0.5222273674 0.6941038908
|
||||||
|
124 0.5222273674 0.6941318899
|
||||||
|
125 0.5245427182 0.6941318581
|
||||||
|
126 0.5240796481 0.6939774214
|
||||||
|
127 0.5246584858 0.6939790446
|
||||||
|
128 0.5244269507 0.6939875313
|
||||||
|
129 0.5239638805 0.6940065251
|
||||||
|
130 0.5237323455 0.6940062666
|
||||||
|
131 0.5231535078 0.6940655128
|
||||||
|
132 0.5226904376 0.6940488902
|
||||||
|
133 0.5221115999 0.6940625077
|
||||||
|
134 0.5225746701 0.6940868198
|
||||||
|
135 0.5229219727 0.6940640084
|
||||||
|
136 0.5232692753 0.6941097371
|
||||||
|
137 0.5244269507 0.6940823091
|
||||||
|
138 0.5237323455 0.6941720366
|
||||||
|
139 0.5235008104 0.6942806828
|
||||||
|
@@ -0,0 +1,141 @@
|
|||||||
|
iter Passed Remaining
|
||||||
|
0 61 61139
|
||||||
|
1 66 33196
|
||||||
|
2 71 23708
|
||||||
|
3 76 18990
|
||||||
|
4 81 16262
|
||||||
|
5 86 14390
|
||||||
|
6 91 13010
|
||||||
|
7 97 12050
|
||||||
|
8 101 11219
|
||||||
|
9 107 10684
|
||||||
|
10 113 10199
|
||||||
|
11 118 9733
|
||||||
|
12 123 9371
|
||||||
|
13 129 9088
|
||||||
|
14 134 8843
|
||||||
|
15 139 8565
|
||||||
|
16 144 8375
|
||||||
|
17 149 8180
|
||||||
|
18 155 8031
|
||||||
|
19 160 7880
|
||||||
|
20 166 7774
|
||||||
|
21 171 7632
|
||||||
|
22 176 7499
|
||||||
|
23 181 7396
|
||||||
|
24 186 7277
|
||||||
|
25 191 7171
|
||||||
|
26 196 7090
|
||||||
|
27 202 7024
|
||||||
|
28 207 6943
|
||||||
|
29 217 7041
|
||||||
|
30 225 7061
|
||||||
|
31 234 7092
|
||||||
|
32 241 7067
|
||||||
|
33 247 7032
|
||||||
|
34 255 7032
|
||||||
|
35 261 7005
|
||||||
|
36 269 7006
|
||||||
|
37 275 6984
|
||||||
|
38 283 6981
|
||||||
|
39 289 6954
|
||||||
|
40 300 7027
|
||||||
|
41 308 7033
|
||||||
|
42 316 7052
|
||||||
|
43 324 7043
|
||||||
|
44 331 7029
|
||||||
|
45 336 6977
|
||||||
|
46 341 6926
|
||||||
|
47 349 6929
|
||||||
|
48 356 6923
|
||||||
|
49 364 6922
|
||||||
|
50 369 6878
|
||||||
|
51 375 6844
|
||||||
|
52 380 6806
|
||||||
|
53 386 6773
|
||||||
|
54 391 6730
|
||||||
|
55 396 6685
|
||||||
|
56 401 6646
|
||||||
|
57 406 6600
|
||||||
|
58 411 6566
|
||||||
|
59 416 6531
|
||||||
|
60 421 6490
|
||||||
|
61 427 6462
|
||||||
|
62 432 6425
|
||||||
|
63 436 6384
|
||||||
|
64 441 6349
|
||||||
|
65 446 6316
|
||||||
|
66 452 6296
|
||||||
|
67 457 6267
|
||||||
|
68 462 6243
|
||||||
|
69 467 6211
|
||||||
|
70 472 6182
|
||||||
|
71 477 6156
|
||||||
|
72 482 6128
|
||||||
|
73 487 6104
|
||||||
|
74 493 6088
|
||||||
|
75 499 6068
|
||||||
|
76 504 6043
|
||||||
|
77 509 6020
|
||||||
|
78 515 6005
|
||||||
|
79 519 5979
|
||||||
|
80 525 5959
|
||||||
|
81 530 5935
|
||||||
|
82 534 5908
|
||||||
|
83 539 5887
|
||||||
|
84 545 5876
|
||||||
|
85 555 5906
|
||||||
|
86 562 5903
|
||||||
|
87 568 5889
|
||||||
|
88 575 5890
|
||||||
|
89 580 5874
|
||||||
|
90 585 5852
|
||||||
|
91 592 5844
|
||||||
|
92 597 5830
|
||||||
|
93 602 5806
|
||||||
|
94 607 5791
|
||||||
|
95 613 5778
|
||||||
|
96 618 5756
|
||||||
|
97 623 5743
|
||||||
|
98 629 5729
|
||||||
|
99 634 5713
|
||||||
|
100 640 5701
|
||||||
|
101 645 5684
|
||||||
|
102 650 5664
|
||||||
|
103 655 5647
|
||||||
|
104 661 5641
|
||||||
|
105 667 5626
|
||||||
|
106 672 5610
|
||||||
|
107 677 5595
|
||||||
|
108 682 5580
|
||||||
|
109 687 5566
|
||||||
|
110 693 5552
|
||||||
|
111 699 5543
|
||||||
|
112 704 5529
|
||||||
|
113 709 5513
|
||||||
|
114 714 5498
|
||||||
|
115 719 5481
|
||||||
|
116 725 5477
|
||||||
|
117 731 5468
|
||||||
|
118 737 5460
|
||||||
|
119 744 5458
|
||||||
|
120 751 5456
|
||||||
|
121 758 5455
|
||||||
|
122 763 5446
|
||||||
|
123 774 5469
|
||||||
|
124 781 5467
|
||||||
|
125 792 5495
|
||||||
|
126 797 5480
|
||||||
|
127 804 5481
|
||||||
|
128 809 5466
|
||||||
|
129 814 5448
|
||||||
|
130 819 5437
|
||||||
|
131 825 5429
|
||||||
|
132 830 5416
|
||||||
|
133 838 5418
|
||||||
|
134 843 5403
|
||||||
|
135 848 5392
|
||||||
|
136 854 5384
|
||||||
|
137 860 5375
|
||||||
|
138 867 5370
|
||||||
|
139 872 5357
|
||||||
|
@@ -0,0 +1,460 @@
|
|||||||
|
"""按天统计双模型和三模型的准确率"""
|
||||||
|
|
||||||
|
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 data.fetcher import BinanceDataFetcher
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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("按天统计:双模型交集 vs 三模型投票 每日准确率分析")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 加载模型 ==========
|
||||||
|
print("\n[1/4] 加载模型...")
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
|
||||||
|
# 加载DLinear
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 加载N-BEATS
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 加载TTM-R2
|
||||||
|
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(f"✓ 三个模型已加载")
|
||||||
|
|
||||||
|
# ========== 获取数据 ==========
|
||||||
|
print("\n[2/4] 获取2026年测试数据...")
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
|
||||||
|
try:
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-26',
|
||||||
|
end_date='2026-02-02', use_cache=False)
|
||||||
|
except:
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', use_cache=False)
|
||||||
|
|
||||||
|
print(f"✓ 测试数据: {len(df_2026)} 行")
|
||||||
|
|
||||||
|
# ========== 预处理 ==========
|
||||||
|
print("\n[3/4] 预处理数据...")
|
||||||
|
|
||||||
|
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]
|
||||||
|
y_nbeats = y_nbeats[:min_samples]
|
||||||
|
|
||||||
|
# ========== 预测 ==========
|
||||||
|
print("\n[4/4] 执行预测...")
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
dlinear_preds = (dlinear_probs > 0.5).astype(int).flatten()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
print(f"✓ 预测完成")
|
||||||
|
|
||||||
|
# ========== 构建结果DataFrame ==========
|
||||||
|
# 获取对应的时间戳(第i个样本对应df_2026的第168+i根K线)
|
||||||
|
timestamps = df_2026['timestamp'].iloc[168:168+min_samples].values
|
||||||
|
|
||||||
|
# 转换为美国东部时间 (ET)
|
||||||
|
timestamps_utc = pd.to_datetime(timestamps)
|
||||||
|
timestamps_et = timestamps_utc.tz_localize('UTC').tz_convert('America/New_York')
|
||||||
|
dates = timestamps_et.date
|
||||||
|
|
||||||
|
# 双模型交集
|
||||||
|
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
|
||||||
|
|
||||||
|
# 创建DataFrame
|
||||||
|
results_df = pd.DataFrame({
|
||||||
|
'date': dates,
|
||||||
|
'timestamp': timestamps,
|
||||||
|
'timestamp_et': timestamps_et,
|
||||||
|
'true_label': true_labels,
|
||||||
|
'dlinear_pred': dlinear_preds,
|
||||||
|
'nbeats_pred': nbeats_preds,
|
||||||
|
'ttm_pred': ttm_preds,
|
||||||
|
'two_model_pred': two_model_preds,
|
||||||
|
'three_model_pred': three_model_preds,
|
||||||
|
'two_model_correct': (two_model_preds == true_labels) & (two_model_preds != -1),
|
||||||
|
'three_model_correct': (three_model_preds == true_labels)
|
||||||
|
})
|
||||||
|
|
||||||
|
# 添加周几信息(美国东部时间)
|
||||||
|
results_df['day_of_week'] = pd.to_datetime(results_df['timestamp_et']).dt.day_name()
|
||||||
|
results_df['is_weekend'] = pd.to_datetime(results_df['timestamp_et']).dt.dayofweek >= 5 # 5=Saturday, 6=Sunday
|
||||||
|
|
||||||
|
# 2026年1月美国重要经济数据发布日期(美国东部时间)
|
||||||
|
# 数据来源:美联储、劳工部等官方日历
|
||||||
|
us_economic_events = {
|
||||||
|
'2026-01-08': ['初请失业金人数'],
|
||||||
|
'2026-01-09': ['非农就业报告 (NFP)', '失业率'],
|
||||||
|
'2026-01-13': ['CPI通胀数据'],
|
||||||
|
'2026-01-14': ['PPI生产者物价指数'],
|
||||||
|
'2026-01-15': ['零售销售数据'],
|
||||||
|
'2026-01-16': ['密歇根大学消费者信心指数'],
|
||||||
|
'2026-01-22': ['初请失业金人数'],
|
||||||
|
'2026-01-27': ['耐用品订单'],
|
||||||
|
'2026-01-28': ['FOMC利率决议', 'GDP初值'],
|
||||||
|
'2026-01-29': ['PCE物价指数', '个人收入与支出'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# ========== 按天统计 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 每日准确率统计 (美国东部时间)")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
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', # 三模型正确数
|
||||||
|
'is_weekend': 'first', # 是否周末
|
||||||
|
'day_of_week': 'first' # 星期几
|
||||||
|
})
|
||||||
|
|
||||||
|
daily_stats.columns = ['total_samples', 'two_model_valid', 'two_model_correct',
|
||||||
|
'three_model_correct', 'is_weekend', 'day_of_week']
|
||||||
|
|
||||||
|
# 计算准确率
|
||||||
|
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
|
||||||
|
|
||||||
|
# 填充NaN(当天没有有效预测)
|
||||||
|
daily_stats['two_model_accuracy'] = daily_stats['two_model_accuracy'].fillna(0)
|
||||||
|
|
||||||
|
# 添加美国经济数据发布标记
|
||||||
|
daily_stats['us_events'] = daily_stats.index.map(
|
||||||
|
lambda x: us_economic_events.get(str(x), [])
|
||||||
|
)
|
||||||
|
daily_stats['has_us_data'] = daily_stats['us_events'].apply(lambda x: len(x) > 0)
|
||||||
|
|
||||||
|
print(f"\n每日详细统计:")
|
||||||
|
print(f"{'日期':<12} {'星期':<10} {'周末':<4} {'美国数据':<4} {'样本':>4} "
|
||||||
|
f"{'双模型%':>8} {'覆盖%':>6} {'三模型%':>8} {'差异':>7}")
|
||||||
|
print("-" * 95)
|
||||||
|
|
||||||
|
for date, row in daily_stats.iterrows():
|
||||||
|
diff = row['two_model_accuracy'] - row['three_model_accuracy']
|
||||||
|
diff_str = f"{diff:+.1f}" if row['two_model_valid'] > 0 else "N/A"
|
||||||
|
|
||||||
|
weekend_mark = "✓" if row['is_weekend'] else ""
|
||||||
|
us_data_mark = "📊" if row['has_us_data'] else ""
|
||||||
|
|
||||||
|
# 根据准确率设置颜色标记
|
||||||
|
two_acc = row['two_model_accuracy']
|
||||||
|
three_acc = row['three_model_accuracy']
|
||||||
|
|
||||||
|
two_mark = "🔥" if two_acc >= 70 else ("✅" if two_acc >= 60 else ("⚠️" if two_acc < 40 else ""))
|
||||||
|
three_mark = "🔥" if three_acc >= 70 else ("✅" if three_acc >= 60 else ("⚠️" if three_acc < 40 else ""))
|
||||||
|
|
||||||
|
print(f"{date} {row['day_of_week'][:3]:>8} {weekend_mark:>4} {us_data_mark:>4} "
|
||||||
|
f"{row['total_samples']:>4.0f} "
|
||||||
|
f"{row['two_model_accuracy']:>7.1f}{two_mark} "
|
||||||
|
f"{row['two_model_coverage']:>5.1f} "
|
||||||
|
f"{row['three_model_accuracy']:>7.1f}{three_mark} "
|
||||||
|
f"{diff_str:>7}")
|
||||||
|
|
||||||
|
# 如果有美国经济数据发布,显示详情
|
||||||
|
if row['has_us_data']:
|
||||||
|
events_str = ", ".join(row['us_events'])
|
||||||
|
print(f" └─ 📊 {events_str}")
|
||||||
|
|
||||||
|
# ========== 周末 vs 工作日分析 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📅 周末 vs 工作日准确率对比")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
weekend_stats = daily_stats[daily_stats['is_weekend']]
|
||||||
|
weekday_stats = daily_stats[~daily_stats['is_weekend']]
|
||||||
|
|
||||||
|
if len(weekend_stats) > 0:
|
||||||
|
print(f"\n🌴 周末 ({len(weekend_stats)}天):")
|
||||||
|
print(f" 双模型平均准确率: {weekend_stats['two_model_accuracy'].mean():.2f}%")
|
||||||
|
print(f" 三模型平均准确率: {weekend_stats['three_model_accuracy'].mean():.2f}%")
|
||||||
|
print(f" 平均覆盖率: {weekend_stats['two_model_coverage'].mean():.2f}%")
|
||||||
|
|
||||||
|
if len(weekday_stats) > 0:
|
||||||
|
print(f"\n💼 工作日 ({len(weekday_stats)}天):")
|
||||||
|
print(f" 双模型平均准确率: {weekday_stats['two_model_accuracy'].mean():.2f}%")
|
||||||
|
print(f" 三模型平均准确率: {weekday_stats['three_model_accuracy'].mean():.2f}%")
|
||||||
|
print(f" 平均覆盖率: {weekday_stats['two_model_coverage'].mean():.2f}%")
|
||||||
|
|
||||||
|
# ========== 美国数据发布日 vs 普通日分析 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 美国经济数据发布日 vs 普通日准确率对比")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
us_data_days = daily_stats[daily_stats['has_us_data']]
|
||||||
|
normal_days = daily_stats[~daily_stats['has_us_data']]
|
||||||
|
|
||||||
|
if len(us_data_days) > 0:
|
||||||
|
print(f"\n📊 美国数据发布日 ({len(us_data_days)}天):")
|
||||||
|
print(f" 双模型平均准确率: {us_data_days['two_model_accuracy'].mean():.2f}%")
|
||||||
|
print(f" 三模型平均准确率: {us_data_days['three_model_accuracy'].mean():.2f}%")
|
||||||
|
print(f" 平均覆盖率: {us_data_days['two_model_coverage'].mean():.2f}%")
|
||||||
|
print(f"\n 具体日期:")
|
||||||
|
for date in us_data_days.index:
|
||||||
|
events = ", ".join(us_data_days.loc[date, 'us_events'])
|
||||||
|
two_acc = us_data_days.loc[date, 'two_model_accuracy']
|
||||||
|
three_acc = us_data_days.loc[date, 'three_model_accuracy']
|
||||||
|
print(f" {date}: 双{two_acc:.1f}% / 三{three_acc:.1f}% - {events}")
|
||||||
|
|
||||||
|
if len(normal_days) > 0:
|
||||||
|
print(f"\n📅 普通日 ({len(normal_days)}天):")
|
||||||
|
print(f" 双模型平均准确率: {normal_days['two_model_accuracy'].mean():.2f}%")
|
||||||
|
print(f" 三模型平均准确率: {normal_days['three_model_accuracy'].mean():.2f}%")
|
||||||
|
print(f" 平均覆盖率: {normal_days['two_model_coverage'].mean():.2f}%")
|
||||||
|
|
||||||
|
# ========== 分类统计 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📈 准确率分类统计")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 双模型交集分类
|
||||||
|
two_model_high = daily_stats[daily_stats['two_model_accuracy'] >= 60]
|
||||||
|
two_model_medium = daily_stats[(daily_stats['two_model_accuracy'] >= 45) &
|
||||||
|
(daily_stats['two_model_accuracy'] < 60)]
|
||||||
|
two_model_low = daily_stats[(daily_stats['two_model_accuracy'] > 0) &
|
||||||
|
(daily_stats['two_model_accuracy'] < 45)]
|
||||||
|
|
||||||
|
# 三模型投票分类
|
||||||
|
three_model_high = daily_stats[daily_stats['three_model_accuracy'] >= 60]
|
||||||
|
three_model_medium = daily_stats[(daily_stats['three_model_accuracy'] >= 45) &
|
||||||
|
(daily_stats['three_model_accuracy'] < 60)]
|
||||||
|
three_model_low = daily_stats[daily_stats['three_model_accuracy'] < 45]
|
||||||
|
|
||||||
|
print(f"\n🔵 双模型交集:")
|
||||||
|
print(f" 高准确率 (>=60%): {len(two_model_high)}天")
|
||||||
|
if len(two_model_high) > 0:
|
||||||
|
print(f" 日期: {', '.join([str(d) for d in two_model_high.index])}")
|
||||||
|
print(f" 平均准确率: {two_model_high['two_model_accuracy'].mean():.1f}%")
|
||||||
|
|
||||||
|
print(f"\n 中等准确率 (45-60%): {len(two_model_medium)}天")
|
||||||
|
if len(two_model_medium) > 0:
|
||||||
|
print(f" 日期: {', '.join([str(d) for d in two_model_medium.index])}")
|
||||||
|
print(f" 平均准确率: {two_model_medium['two_model_accuracy'].mean():.1f}%")
|
||||||
|
|
||||||
|
print(f"\n 低准确率 (<45%): {len(two_model_low)}天")
|
||||||
|
if len(two_model_low) > 0:
|
||||||
|
print(f" 日期: {', '.join([str(d) for d in two_model_low.index])}")
|
||||||
|
print(f" 平均准确率: {two_model_low['two_model_accuracy'].mean():.1f}%")
|
||||||
|
|
||||||
|
print(f"\n🟢 三模型投票:")
|
||||||
|
print(f" 高准确率 (>=60%): {len(three_model_high)}天")
|
||||||
|
if len(three_model_high) > 0:
|
||||||
|
print(f" 日期: {', '.join([str(d) for d in three_model_high.index])}")
|
||||||
|
print(f" 平均准确率: {three_model_high['three_model_accuracy'].mean():.1f}%")
|
||||||
|
|
||||||
|
print(f"\n 中等准确率 (45-60%): {len(three_model_medium)}天")
|
||||||
|
if len(three_model_medium) > 0:
|
||||||
|
print(f" 日期: {', '.join([str(d) for d in three_model_medium.index])}")
|
||||||
|
print(f" 平均准确率: {three_model_medium['three_model_accuracy'].mean():.1f}%")
|
||||||
|
|
||||||
|
print(f"\n 低准确率 (<45%): {len(three_model_low)}天")
|
||||||
|
if len(three_model_low) > 0:
|
||||||
|
print(f" 日期: {', '.join([str(d) for d in three_model_low.index])}")
|
||||||
|
print(f" 平均准确率: {three_model_low['three_model_accuracy'].mean():.1f}%")
|
||||||
|
|
||||||
|
# ========== 总结 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 总体统计")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print(f"\n双模型交集:")
|
||||||
|
print(f" 总体准确率: {(daily_stats['two_model_correct'].sum() / daily_stats['two_model_valid'].sum() * 100):.2f}%")
|
||||||
|
print(f" 总体覆盖率: {(daily_stats['two_model_valid'].sum() / daily_stats['total_samples'].sum() * 100):.2f}%")
|
||||||
|
print(f" 最高日准确率: {daily_stats['two_model_accuracy'].max():.1f}% ({daily_stats['two_model_accuracy'].idxmax()})")
|
||||||
|
print(f" 最低日准确率: {daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].min():.1f}% ({daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].idxmin()})")
|
||||||
|
|
||||||
|
print(f"\n三模型投票:")
|
||||||
|
print(f" 总体准确率: {(daily_stats['three_model_correct'].sum() / daily_stats['total_samples'].sum() * 100):.2f}%")
|
||||||
|
print(f" 覆盖率: 100%")
|
||||||
|
print(f" 最高日准确率: {daily_stats['three_model_accuracy'].max():.1f}% ({daily_stats['three_model_accuracy'].idxmax()})")
|
||||||
|
print(f" 最低日准确率: {daily_stats['three_model_accuracy'].min():.1f}% ({daily_stats['three_model_accuracy'].idxmin()})")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 每日准确率统计完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
"""分析DLinear输出概率值的偏向性及其对集成策略的影响"""
|
||||||
|
|
||||||
|
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 data.fetcher import BinanceDataFetcher
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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("DLinear输出概率值分布分析 - 以0.1阶梯统计")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 加载模型 ==========
|
||||||
|
print("\n[1/4] 加载模型...")
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
|
||||||
|
# 加载DLinear
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 加载N-BEATS
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 加载TTM-R2
|
||||||
|
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(f"✓ 三个模型已加载")
|
||||||
|
|
||||||
|
# ========== 获取数据 ==========
|
||||||
|
print("\n[2/4] 获取2026年测试数据...")
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
|
||||||
|
try:
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
||||||
|
end_date='2026-12-31', use_cache=False)
|
||||||
|
except:
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', use_cache=False)
|
||||||
|
|
||||||
|
print(f"✓ 测试数据: {len(df_2026)} 行")
|
||||||
|
|
||||||
|
# ========== 预处理 ==========
|
||||||
|
print("\n[3/4] 预处理数据...")
|
||||||
|
|
||||||
|
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]
|
||||||
|
y_nbeats = y_nbeats[:min_samples]
|
||||||
|
|
||||||
|
# ========== 预测并获取DLinear概率值 ==========
|
||||||
|
print("\n[4/4] 执行预测并分析DLinear输出概率...")
|
||||||
|
|
||||||
|
# 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())) # sigmoid概率
|
||||||
|
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_valid = two_model_agree_up | two_model_agree_down
|
||||||
|
|
||||||
|
# 三模型多数投票
|
||||||
|
vote_sum = dlinear_preds + nbeats_preds + ttm_preds
|
||||||
|
three_model_preds = np.where(vote_sum >= 2, 1, 0)
|
||||||
|
|
||||||
|
# 真实标签
|
||||||
|
true_labels = y_dlinear
|
||||||
|
|
||||||
|
print(f"✓ 预测完成,开始分析DLinear概率分布...")
|
||||||
|
|
||||||
|
# ========== 按0.1阶梯分析 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 DLinear输出概率分布分析(0.1阶梯)")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 定义概率区间
|
||||||
|
prob_bins = np.arange(0, 1.1, 0.1)
|
||||||
|
prob_labels = [f"{i/10:.1f}-{(i+1)/10:.1f}" for i in range(10)]
|
||||||
|
|
||||||
|
# 为每个样本分配到概率区间
|
||||||
|
prob_bin_indices = np.digitize(dlinear_probs, prob_bins) - 1
|
||||||
|
prob_bin_indices = np.clip(prob_bin_indices, 0, 9) # 确保在0-9范围内
|
||||||
|
|
||||||
|
# 初始化统计
|
||||||
|
stats = []
|
||||||
|
|
||||||
|
for i, bin_label in enumerate(prob_labels):
|
||||||
|
mask = prob_bin_indices == i
|
||||||
|
count = mask.sum()
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 该区间的真实标签
|
||||||
|
bin_true = true_labels[mask]
|
||||||
|
|
||||||
|
# 该区间DLinear的预测
|
||||||
|
bin_dlinear_pred = dlinear_preds[mask]
|
||||||
|
|
||||||
|
# 该区间在双模型交集中的表现
|
||||||
|
bin_two_model_valid = two_model_valid[mask]
|
||||||
|
bin_two_model_pred = np.full(count, -1)
|
||||||
|
bin_two_model_pred[two_model_agree_up[mask]] = 1
|
||||||
|
bin_two_model_pred[two_model_agree_down[mask]] = 0
|
||||||
|
|
||||||
|
# 该区间在三模型投票中的表现
|
||||||
|
bin_three_model_pred = three_model_preds[mask]
|
||||||
|
|
||||||
|
# 计算统计
|
||||||
|
dlinear_acc = (bin_dlinear_pred == bin_true).mean() * 100 if count > 0 else 0
|
||||||
|
|
||||||
|
two_model_valid_count = bin_two_model_valid.sum()
|
||||||
|
two_model_acc = ((bin_two_model_pred == bin_true) & (bin_two_model_pred != -1)).sum() / two_model_valid_count * 100 if two_model_valid_count > 0 else 0
|
||||||
|
two_model_coverage = two_model_valid_count / count * 100
|
||||||
|
|
||||||
|
three_model_acc = (bin_three_model_pred == bin_true).mean() * 100 if count > 0 else 0
|
||||||
|
|
||||||
|
# 平均概率值
|
||||||
|
avg_prob = dlinear_probs[mask].mean()
|
||||||
|
|
||||||
|
# 预测倾向
|
||||||
|
pred_up = bin_dlinear_pred.mean() * 100
|
||||||
|
|
||||||
|
stats.append({
|
||||||
|
'bin': bin_label,
|
||||||
|
'count': count,
|
||||||
|
'avg_prob': avg_prob,
|
||||||
|
'pred_up_pct': pred_up,
|
||||||
|
'dlinear_acc': dlinear_acc,
|
||||||
|
'two_model_acc': two_model_acc,
|
||||||
|
'two_model_cov': two_model_coverage,
|
||||||
|
'three_model_acc': three_model_acc
|
||||||
|
})
|
||||||
|
|
||||||
|
# 转换为DataFrame
|
||||||
|
stats_df = pd.DataFrame(stats)
|
||||||
|
|
||||||
|
# 显示统计表
|
||||||
|
print("\n概率区间分析:")
|
||||||
|
print(f"{'区间':<10} {'样本数':>6} {'平均概率':>8} {'预测↑%':>7} "
|
||||||
|
f"{'DL准确率':>9} {'双模准确率':>10} {'双模覆盖':>8} {'三模准确率':>10}")
|
||||||
|
print("-" * 95)
|
||||||
|
|
||||||
|
for _, row in stats_df.iterrows():
|
||||||
|
# 根据准确率添加标记
|
||||||
|
dl_mark = "🔥" if row['dlinear_acc'] >= 70 else ("✅" if row['dlinear_acc'] >= 60 else ("⚠️" if row['dlinear_acc'] < 45 else ""))
|
||||||
|
two_mark = "🔥" if row['two_model_acc'] >= 70 else ("✅" if row['two_model_acc'] >= 60 else ("⚠️" if row['two_model_acc'] < 45 else ""))
|
||||||
|
three_mark = "🔥" if row['three_model_acc'] >= 70 else ("✅" if row['three_model_acc'] >= 60 else ("⚠️" if row['three_model_acc'] < 45 else ""))
|
||||||
|
|
||||||
|
print(f"{row['bin']:<10} {row['count']:>6} {row['avg_prob']:>8.3f} {row['pred_up_pct']:>6.1f}% "
|
||||||
|
f"{row['dlinear_acc']:>8.1f}{dl_mark} "
|
||||||
|
f"{row['two_model_acc']:>9.1f}{two_mark} "
|
||||||
|
f"{row['two_model_cov']:>7.1f}% "
|
||||||
|
f"{row['three_model_acc']:>9.1f}{three_mark}")
|
||||||
|
|
||||||
|
# ========== 关键发现 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("🔍 关键发现")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 极端概率区间(高置信度)
|
||||||
|
high_conf_up = stats_df[stats_df['avg_prob'] >= 0.65]
|
||||||
|
high_conf_down = stats_df[stats_df['avg_prob'] <= 0.35]
|
||||||
|
|
||||||
|
print(f"\n📈 高置信度看涨区间 (概率≥0.65):")
|
||||||
|
if len(high_conf_up) > 0:
|
||||||
|
total_samples = high_conf_up['count'].sum()
|
||||||
|
avg_dl_acc = (high_conf_up['dlinear_acc'] * high_conf_up['count']).sum() / total_samples
|
||||||
|
avg_two_acc = (high_conf_up['two_model_acc'] * high_conf_up['count']).sum() / total_samples
|
||||||
|
avg_three_acc = (high_conf_up['three_model_acc'] * high_conf_up['count']).sum() / total_samples
|
||||||
|
|
||||||
|
print(f" 样本数: {total_samples} ({total_samples/len(dlinear_probs)*100:.1f}%)")
|
||||||
|
print(f" DLinear平均准确率: {avg_dl_acc:.2f}%")
|
||||||
|
print(f" 双模型平均准确率: {avg_two_acc:.2f}%")
|
||||||
|
print(f" 三模型平均准确率: {avg_three_acc:.2f}%")
|
||||||
|
else:
|
||||||
|
print(" 无数据")
|
||||||
|
|
||||||
|
print(f"\n📉 高置信度看跌区间 (概率≤0.35):")
|
||||||
|
if len(high_conf_down) > 0:
|
||||||
|
total_samples = high_conf_down['count'].sum()
|
||||||
|
avg_dl_acc = (high_conf_down['dlinear_acc'] * high_conf_down['count']).sum() / total_samples
|
||||||
|
avg_two_acc = (high_conf_down['two_model_acc'] * high_conf_down['count']).sum() / total_samples
|
||||||
|
avg_three_acc = (high_conf_down['three_model_acc'] * high_conf_down['count']).sum() / total_samples
|
||||||
|
|
||||||
|
print(f" 样本数: {total_samples} ({total_samples/len(dlinear_probs)*100:.1f}%)")
|
||||||
|
print(f" DLinear平均准确率: {avg_dl_acc:.2f}%")
|
||||||
|
print(f" 双模型平均准确率: {avg_two_acc:.2f}%")
|
||||||
|
print(f" 三模型平均准确率: {avg_three_acc:.2f}%")
|
||||||
|
else:
|
||||||
|
print(" 无数据")
|
||||||
|
|
||||||
|
# 中等置信度区间
|
||||||
|
mid_conf = stats_df[(stats_df['avg_prob'] > 0.45) & (stats_df['avg_prob'] < 0.55)]
|
||||||
|
|
||||||
|
print(f"\n⚖️ 中等置信度区间 (0.45-0.55):")
|
||||||
|
if len(mid_conf) > 0:
|
||||||
|
total_samples = mid_conf['count'].sum()
|
||||||
|
avg_dl_acc = (mid_conf['dlinear_acc'] * mid_conf['count']).sum() / total_samples
|
||||||
|
avg_two_acc = (mid_conf['two_model_acc'] * mid_conf['count']).sum() / total_samples if total_samples > 0 else 0
|
||||||
|
avg_three_acc = (mid_conf['three_model_acc'] * mid_conf['count']).sum() / total_samples
|
||||||
|
|
||||||
|
print(f" 样本数: {total_samples} ({total_samples/len(dlinear_probs)*100:.1f}%)")
|
||||||
|
print(f" DLinear平均准确率: {avg_dl_acc:.2f}%")
|
||||||
|
print(f" 双模型平均准确率: {avg_two_acc:.2f}%")
|
||||||
|
print(f" 三模型平均准确率: {avg_three_acc:.2f}%")
|
||||||
|
print(f" ⚠️ 建议: 低置信度区间,考虑弃权或减仓")
|
||||||
|
else:
|
||||||
|
print(" 无数据")
|
||||||
|
|
||||||
|
# 找出表现最好的概率区间
|
||||||
|
best_bin = stats_df.loc[stats_df['dlinear_acc'].idxmax()]
|
||||||
|
worst_bin = stats_df.loc[stats_df['dlinear_acc'].idxmin()]
|
||||||
|
|
||||||
|
print(f"\n🏆 DLinear表现最佳区间:")
|
||||||
|
print(f" 区间: {best_bin['bin']}")
|
||||||
|
print(f" 准确率: {best_bin['dlinear_acc']:.1f}%")
|
||||||
|
print(f" 样本数: {best_bin['count']}")
|
||||||
|
|
||||||
|
print(f"\n💀 DLinear表现最差区间:")
|
||||||
|
print(f" 区间: {worst_bin['bin']}")
|
||||||
|
print(f" 准确率: {worst_bin['dlinear_acc']:.1f}%")
|
||||||
|
print(f" 样本数: {worst_bin['count']}")
|
||||||
|
|
||||||
|
# ========== 概率分布可视化 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 DLinear概率分布直方图")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print("\n样本数分布:")
|
||||||
|
max_count = stats_df['count'].max()
|
||||||
|
for _, row in stats_df.iterrows():
|
||||||
|
bar_length = int(row['count'] / max_count * 40)
|
||||||
|
bar = "█" * bar_length
|
||||||
|
print(f"{row['bin']:<10} {bar} {row['count']}")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ DLinear概率分布分析完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
"""四模型集成每日准确率分析 - 多时间段测试(2019年1月、2020年1月、2026年1月)"""
|
||||||
|
|
||||||
|
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 data.fetcher import BinanceDataFetcher
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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 predict_period(period_name, start_date, end_date, dlinear_model, nbeats_model, ttm_model, timesfm_model,
|
||||||
|
dlinear_preprocessor, nbeats_preprocessor, device):
|
||||||
|
"""预测单个时间段的数据"""
|
||||||
|
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"📅 {period_name} ({start_date} 至 {end_date})")
|
||||||
|
print(f"{'='*80}")
|
||||||
|
|
||||||
|
# 获取数据
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
df = fetcher.fetch_ohlcv('BTC', '1h', start_date=start_date, end_date=end_date, use_cache=False)
|
||||||
|
|
||||||
|
print(f"✓ 数据: {len(df)} 行")
|
||||||
|
|
||||||
|
# 预处理
|
||||||
|
X_dlinear, y_dlinear, df_dlinear = dlinear_preprocessor.preprocess(df, fit=False)
|
||||||
|
X_nbeats, y_nbeats, df_nbeats = nbeats_preprocessor.preprocess(df, 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]
|
||||||
|
|
||||||
|
print(f"✓ 样本数: {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))
|
||||||
|
dlinear_preds = (dlinear_probs > 0.5).astype(int).flatten()
|
||||||
|
|
||||||
|
# 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['close'].values
|
||||||
|
all_open_prices = df['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)
|
||||||
|
|
||||||
|
# TimesFM预测
|
||||||
|
timesfm_preds_list = []
|
||||||
|
|
||||||
|
for i in range(min_samples):
|
||||||
|
start_idx = i
|
||||||
|
end_idx = start_idx + 512
|
||||||
|
|
||||||
|
if end_idx > len(all_prices):
|
||||||
|
context_prices = all_prices[-512:]
|
||||||
|
current_open_price = all_open_prices[min(168 + i, len(all_open_prices) - 1)]
|
||||||
|
else:
|
||||||
|
context_prices = all_prices[start_idx:end_idx]
|
||||||
|
current_open_price = all_open_prices[end_idx] if end_idx < len(all_open_prices) else all_open_prices[-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
point_forecast, _ = timesfm_model.forecast(horizon=1, inputs=[context_prices])
|
||||||
|
pred_price = point_forecast[0, 0]
|
||||||
|
timesfm_preds_list.append(1 if pred_price > current_open_price else 0)
|
||||||
|
except:
|
||||||
|
timesfm_preds_list.append(np.random.randint(0, 2))
|
||||||
|
|
||||||
|
timesfm_preds = np.array(timesfm_preds_list)
|
||||||
|
|
||||||
|
# 集成策略
|
||||||
|
true_labels = y_dlinear
|
||||||
|
|
||||||
|
# 双模型交集
|
||||||
|
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_3 = dlinear_preds + nbeats_preds + ttm_preds
|
||||||
|
three_model_preds = np.where(vote_sum_3 >= 2, 1, 0)
|
||||||
|
|
||||||
|
# 四模型投票
|
||||||
|
vote_sum_4 = dlinear_preds + nbeats_preds + ttm_preds + timesfm_preds
|
||||||
|
four_model_preds = np.where(vote_sum_4 >= 3, 1, 0)
|
||||||
|
|
||||||
|
# 构建每日结果DataFrame
|
||||||
|
timestamps = df['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
|
||||||
|
|
||||||
|
results_df = pd.DataFrame({
|
||||||
|
'date': dates,
|
||||||
|
'timestamp_et': timestamps_et,
|
||||||
|
'true_label': true_labels,
|
||||||
|
'dlinear_pred': dlinear_preds,
|
||||||
|
'nbeats_pred': nbeats_preds,
|
||||||
|
'ttm_pred': ttm_preds,
|
||||||
|
'timesfm_pred': timesfm_preds,
|
||||||
|
'two_model_pred': two_model_preds,
|
||||||
|
'three_model_pred': three_model_preds,
|
||||||
|
'four_model_pred': four_model_preds,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 按天统计
|
||||||
|
daily_stats = results_df.groupby('date').agg({
|
||||||
|
'true_label': 'count',
|
||||||
|
'dlinear_pred': lambda x: (x == results_df.loc[x.index, 'true_label']).sum(),
|
||||||
|
'nbeats_pred': lambda x: (x == results_df.loc[x.index, 'true_label']).sum(),
|
||||||
|
'ttm_pred': lambda x: (x == results_df.loc[x.index, 'true_label']).sum(),
|
||||||
|
'timesfm_pred': lambda x: (x == results_df.loc[x.index, 'true_label']).sum(),
|
||||||
|
'two_model_pred': lambda x: ((x == results_df.loc[x.index, 'true_label']) & (x != -1)).sum(),
|
||||||
|
'three_model_pred': lambda x: (x == results_df.loc[x.index, 'true_label']).sum(),
|
||||||
|
'four_model_pred': lambda x: (x == results_df.loc[x.index, 'true_label']).sum(),
|
||||||
|
})
|
||||||
|
|
||||||
|
daily_stats.columns = ['total', 'dlinear_correct', 'nbeats_correct', 'ttm_correct',
|
||||||
|
'timesfm_correct', 'two_model_correct', 'three_model_correct', 'four_model_correct']
|
||||||
|
|
||||||
|
# 计算每天的有效预测数(双模型)
|
||||||
|
two_model_valid = results_df.groupby('date')['two_model_pred'].apply(lambda x: (x != -1).sum())
|
||||||
|
daily_stats['two_model_valid'] = two_model_valid
|
||||||
|
|
||||||
|
# 计算准确率
|
||||||
|
daily_stats['dlinear_acc'] = daily_stats['dlinear_correct'] / daily_stats['total'] * 100
|
||||||
|
daily_stats['nbeats_acc'] = daily_stats['nbeats_correct'] / daily_stats['total'] * 100
|
||||||
|
daily_stats['ttm_acc'] = daily_stats['ttm_correct'] / daily_stats['total'] * 100
|
||||||
|
daily_stats['timesfm_acc'] = daily_stats['timesfm_correct'] / daily_stats['total'] * 100
|
||||||
|
daily_stats['two_model_acc'] = daily_stats['two_model_correct'] / daily_stats['two_model_valid'] * 100
|
||||||
|
daily_stats['three_model_acc'] = daily_stats['three_model_correct'] / daily_stats['total'] * 100
|
||||||
|
daily_stats['four_model_acc'] = daily_stats['four_model_correct'] / daily_stats['total'] * 100
|
||||||
|
|
||||||
|
daily_stats['two_model_acc'] = daily_stats['two_model_acc'].fillna(0)
|
||||||
|
|
||||||
|
return daily_stats, results_df
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("="*80)
|
||||||
|
print("四模型集成每日准确率分析 - 多时间段测试")
|
||||||
|
print("测试时段: 2019年1月、2020年1月、2026年1月")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 加载模型 ==========
|
||||||
|
print("\n[1/2] 加载模型...")
|
||||||
|
|
||||||
|
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, 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)
|
||||||
|
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, weights_only=False
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
|
||||||
|
import timesfm
|
||||||
|
timesfm_model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
||||||
|
"google/timesfm-2.5-200m-pytorch",
|
||||||
|
torch_compile=False
|
||||||
|
)
|
||||||
|
timesfm_model.compile(
|
||||||
|
timesfm.ForecastConfig(
|
||||||
|
max_context=512,
|
||||||
|
max_horizon=1,
|
||||||
|
normalize_inputs=True,
|
||||||
|
use_continuous_quantile_head=True,
|
||||||
|
force_flip_invariance=True,
|
||||||
|
infer_is_positive=True,
|
||||||
|
fix_quantile_crossing=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✓ 四个模型已加载 (DLinear, N-BEATS, TTM-R2, TimesFM)")
|
||||||
|
|
||||||
|
# ========== 多时间段预测 ==========
|
||||||
|
print("\n[2/2] 多时间段预测...")
|
||||||
|
|
||||||
|
periods = [
|
||||||
|
("2019年1月", "2019-01-01", "2019-01-31"),
|
||||||
|
("2020年1月", "2020-01-01", "2020-01-31"),
|
||||||
|
("2026年1月", "2026-01-01", "2026-01-31"),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_period_stats = {}
|
||||||
|
|
||||||
|
for period_name, start_date, end_date in periods:
|
||||||
|
daily_stats, results_df = predict_period(
|
||||||
|
period_name, start_date, end_date,
|
||||||
|
dlinear_model, nbeats_model, ttm_model, timesfm_model,
|
||||||
|
dlinear_preprocessor, nbeats_preprocessor, device
|
||||||
|
)
|
||||||
|
all_period_stats[period_name] = daily_stats
|
||||||
|
|
||||||
|
# 显示每日准确率
|
||||||
|
print(f"\n每日准确率明细:")
|
||||||
|
print(f"{'日期':<12} {'样本':>4} {'DLinear':>8} {'N-BEATS':>8} {'TTM-R2':>8} {'TimesFM':>8} {'双模型':>8} {'三模型':>8} {'四模型':>8}")
|
||||||
|
print("-" * 95)
|
||||||
|
|
||||||
|
for date, row in daily_stats.iterrows():
|
||||||
|
print(f"{date} {row['total']:>4.0f} "
|
||||||
|
f"{row['dlinear_acc']:>7.1f}% "
|
||||||
|
f"{row['nbeats_acc']:>7.1f}% "
|
||||||
|
f"{row['ttm_acc']:>7.1f}% "
|
||||||
|
f"{row['timesfm_acc']:>7.1f}% "
|
||||||
|
f"{row['two_model_acc']:>7.1f}% "
|
||||||
|
f"{row['three_model_acc']:>7.1f}% "
|
||||||
|
f"{row['four_model_acc']:>7.1f}%")
|
||||||
|
|
||||||
|
# 统计总体
|
||||||
|
print(f"\n{period_name} 总体统计:")
|
||||||
|
print(f" DLinear: {daily_stats['dlinear_correct'].sum() / daily_stats['total'].sum() * 100:.2f}%")
|
||||||
|
print(f" N-BEATS: {daily_stats['nbeats_correct'].sum() / daily_stats['total'].sum() * 100:.2f}%")
|
||||||
|
print(f" TTM-R2: {daily_stats['ttm_correct'].sum() / daily_stats['total'].sum() * 100:.2f}%")
|
||||||
|
print(f" TimesFM: {daily_stats['timesfm_correct'].sum() / daily_stats['total'].sum() * 100:.2f}%")
|
||||||
|
print(f" 双模型: {daily_stats['two_model_correct'].sum() / daily_stats['two_model_valid'].sum() * 100:.2f}% (覆盖率: {daily_stats['two_model_valid'].sum() / daily_stats['total'].sum() * 100:.1f}%)")
|
||||||
|
print(f" 三模型: {daily_stats['three_model_correct'].sum() / daily_stats['total'].sum() * 100:.2f}%")
|
||||||
|
print(f" 四模型: {daily_stats['four_model_correct'].sum() / daily_stats['total'].sum() * 100:.2f}%")
|
||||||
|
|
||||||
|
# ========== 跨时期对比 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 跨时期对比总结")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print(f"\n{'时期':<12} {'DLinear':>9} {'N-BEATS':>9} {'TTM-R2':>9} {'TimesFM':>9} {'双模型':>9} {'三模型':>9} {'四模型':>9}")
|
||||||
|
print("-" * 95)
|
||||||
|
|
||||||
|
for period_name in ["2019年1月", "2020年1月", "2026年1月"]:
|
||||||
|
stats = all_period_stats[period_name]
|
||||||
|
print(f"{period_name:<12} "
|
||||||
|
f"{stats['dlinear_correct'].sum() / stats['total'].sum() * 100:>8.2f}% "
|
||||||
|
f"{stats['nbeats_correct'].sum() / stats['total'].sum() * 100:>8.2f}% "
|
||||||
|
f"{stats['ttm_correct'].sum() / stats['total'].sum() * 100:>8.2f}% "
|
||||||
|
f"{stats['timesfm_correct'].sum() / stats['total'].sum() * 100:>8.2f}% "
|
||||||
|
f"{stats['two_model_correct'].sum() / stats['two_model_valid'].sum() * 100:>8.2f}% "
|
||||||
|
f"{stats['three_model_correct'].sum() / stats['total'].sum() * 100:>8.2f}% "
|
||||||
|
f"{stats['four_model_correct'].sum() / stats['total'].sum() * 100:>8.2f}%")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 多时间段每日准确率分析完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
"""五模型集成预测:DLinear + N-BEATS + TTM-R2 + TimesFM 2.5 + CatBoost"""
|
||||||
|
|
||||||
|
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 catboost import CatBoostClassifier
|
||||||
|
from data.fetcher import BinanceDataFetcher
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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 add_catboost_features(df):
|
||||||
|
"""为CatBoost添加丰富的技术指标特征"""
|
||||||
|
df = df.copy()
|
||||||
|
|
||||||
|
# 基础价格特征
|
||||||
|
df['price_change'] = df['close'] - df['open']
|
||||||
|
df['price_change_pct'] = (df['close'] - df['open']) / df['open']
|
||||||
|
df['high_low_range'] = df['high'] - df['low']
|
||||||
|
df['high_low_range_pct'] = (df['high'] - df['low']) / df['low']
|
||||||
|
|
||||||
|
# 移动平均线
|
||||||
|
df['sma_7'] = ta.sma(df['close'], length=7)
|
||||||
|
df['sma_25'] = ta.sma(df['close'], length=25)
|
||||||
|
df['sma_99'] = ta.sma(df['close'], length=99)
|
||||||
|
df['ema_12'] = ta.ema(df['close'], length=12)
|
||||||
|
df['ema_26'] = ta.ema(df['close'], length=26)
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
df['rsi_14'] = ta.rsi(df['close'], length=14)
|
||||||
|
df['rsi_7'] = ta.rsi(df['close'], length=7)
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
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]
|
||||||
|
|
||||||
|
# 布林带
|
||||||
|
bbands = ta.bbands(df['close'], length=20)
|
||||||
|
if bbands is not None and not bbands.empty:
|
||||||
|
df['bb_upper'] = bbands.iloc[:, 0]
|
||||||
|
df['bb_mid'] = bbands.iloc[:, 1]
|
||||||
|
df['bb_lower'] = bbands.iloc[:, 2]
|
||||||
|
df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['bb_mid']
|
||||||
|
df['bb_position'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
|
||||||
|
|
||||||
|
# ATR
|
||||||
|
df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14)
|
||||||
|
|
||||||
|
# 成交量特征
|
||||||
|
df['volume_sma_7'] = ta.sma(df['volume'], length=7)
|
||||||
|
df['volume_ratio'] = df['volume'] / df['volume_sma_7']
|
||||||
|
|
||||||
|
# OBV
|
||||||
|
df['obv'] = ta.obv(df['close'], df['volume'])
|
||||||
|
|
||||||
|
# Stochastic
|
||||||
|
stoch = ta.stoch(df['high'], df['low'], df['close'])
|
||||||
|
if stoch is not None and not stoch.empty:
|
||||||
|
df['stoch_k'] = stoch.iloc[:, 0]
|
||||||
|
df['stoch_d'] = stoch.iloc[:, 1]
|
||||||
|
|
||||||
|
# ADX
|
||||||
|
adx = ta.adx(df['high'], df['low'], df['close'])
|
||||||
|
if adx is not None and not adx.empty:
|
||||||
|
df['adx'] = adx.iloc[:, 0]
|
||||||
|
|
||||||
|
# 价格动量
|
||||||
|
df['momentum_1'] = df['close'].pct_change(1)
|
||||||
|
df['momentum_3'] = df['close'].pct_change(3)
|
||||||
|
df['momentum_7'] = df['close'].pct_change(7)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("="*80)
|
||||||
|
print("五模型集成预测:DLinear + N-BEATS + TTM-R2 + TimesFM + CatBoost")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 步骤1: 加载PyTorch模型 ==========
|
||||||
|
print("\n[1/8] 加载DLinear和N-BEATS模型...")
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
|
||||||
|
# 加载DLinear
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(f"✓ DLinear已加载")
|
||||||
|
|
||||||
|
# 加载N-BEATS
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(f"✓ N-BEATS已加载")
|
||||||
|
|
||||||
|
# ========== 步骤2: 加载TTM-R2 ==========
|
||||||
|
print("\n[2/8] 加载TTM-R2零样本模型...")
|
||||||
|
|
||||||
|
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(f"✓ TTM-R2已加载")
|
||||||
|
|
||||||
|
# ========== 步骤3: 加载TimesFM 2.5 ==========
|
||||||
|
print("\n[3/8] 加载TimesFM 2.5模型...")
|
||||||
|
|
||||||
|
import timesfm
|
||||||
|
|
||||||
|
timesfm_model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
||||||
|
"google/timesfm-2.5-200m-pytorch",
|
||||||
|
torch_compile=False
|
||||||
|
)
|
||||||
|
|
||||||
|
timesfm_model.compile(
|
||||||
|
timesfm.ForecastConfig(
|
||||||
|
max_context=512,
|
||||||
|
max_horizon=1,
|
||||||
|
normalize_inputs=True,
|
||||||
|
use_continuous_quantile_head=True,
|
||||||
|
force_flip_invariance=True,
|
||||||
|
infer_is_positive=True,
|
||||||
|
fix_quantile_crossing=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✓ TimesFM 2.5已加载")
|
||||||
|
|
||||||
|
# ========== 步骤4: 加载CatBoost ==========
|
||||||
|
print("\n[4/8] 加载CatBoost模型...")
|
||||||
|
|
||||||
|
catboost_model = CatBoostClassifier()
|
||||||
|
catboost_model.load_model('./models/checkpoints/catboost_classification_2026/catboost_model.cbm')
|
||||||
|
|
||||||
|
with open('./models/checkpoints/catboost_classification_2026/metadata.pkl', 'rb') as f:
|
||||||
|
catboost_metadata = pickle.load(f)
|
||||||
|
catboost_feature_cols = catboost_metadata['feature_cols']
|
||||||
|
|
||||||
|
print(f"✓ CatBoost已加载 (特征数: {len(catboost_feature_cols)})")
|
||||||
|
|
||||||
|
# ========== 步骤5: 获取测试数据 ==========
|
||||||
|
print("\n[5/8] 获取2026年1月测试数据...")
|
||||||
|
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
||||||
|
end_date='2026-01-31', use_cache=False)
|
||||||
|
|
||||||
|
print(f"✓ 数据获取完成: {len(df_2026)} 行")
|
||||||
|
|
||||||
|
# ========== 步骤6: 预处理 ==========
|
||||||
|
print("\n[6/8] 预处理数据...")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# CatBoost特征工程
|
||||||
|
df_catboost = add_catboost_features(df_2026)
|
||||||
|
df_catboost = df_catboost.dropna().reset_index(drop=True)
|
||||||
|
X_catboost = df_catboost[catboost_feature_cols]
|
||||||
|
|
||||||
|
# 确保样本数量一致
|
||||||
|
min_samples = min(len(X_dlinear), len(X_nbeats), len(X_catboost))
|
||||||
|
X_dlinear = X_dlinear[:min_samples]
|
||||||
|
y_dlinear = y_dlinear[:min_samples]
|
||||||
|
X_nbeats = X_nbeats[:min_samples]
|
||||||
|
X_catboost = X_catboost.iloc[:min_samples]
|
||||||
|
|
||||||
|
print(f"✓ 对齐样本数: {min_samples}")
|
||||||
|
|
||||||
|
# ========== 步骤7: 五个模型分别预测 ==========
|
||||||
|
print("\n[7/8] 五个模型分别预测...")
|
||||||
|
|
||||||
|
# 7.1 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))
|
||||||
|
dlinear_preds = (dlinear_probs > 0.5).astype(int).flatten()
|
||||||
|
|
||||||
|
print(f"✓ DLinear: 涨{(dlinear_preds==1).sum()} ({(dlinear_preds==1).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 7.2 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)
|
||||||
|
|
||||||
|
print(f"✓ N-BEATS: 涨{(nbeats_preds==1).sum()} ({(nbeats_preds==1).sum()/len(nbeats_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 7.3 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)
|
||||||
|
|
||||||
|
print(f"✓ TTM-R2: 涨{(ttm_preds==1).sum()} ({(ttm_preds==1).sum()/len(ttm_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 7.4 TimesFM 2.5预测
|
||||||
|
timesfm_preds_list = []
|
||||||
|
|
||||||
|
for i in range(min_samples):
|
||||||
|
start_idx = i
|
||||||
|
end_idx = start_idx + 512
|
||||||
|
|
||||||
|
if end_idx > len(all_prices):
|
||||||
|
context_prices = all_prices[-512:]
|
||||||
|
current_open_price = all_open_prices[min(168 + i, len(all_open_prices) - 1)]
|
||||||
|
else:
|
||||||
|
context_prices = all_prices[start_idx:end_idx]
|
||||||
|
current_open_price = all_open_prices[end_idx] if end_idx < len(all_open_prices) else all_open_prices[-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
point_forecast, _ = timesfm_model.forecast(
|
||||||
|
horizon=1,
|
||||||
|
inputs=[context_prices],
|
||||||
|
)
|
||||||
|
pred_price = point_forecast[0, 0]
|
||||||
|
timesfm_preds_list.append(1 if pred_price > current_open_price else 0)
|
||||||
|
except:
|
||||||
|
timesfm_preds_list.append(np.random.randint(0, 2))
|
||||||
|
|
||||||
|
timesfm_preds = np.array(timesfm_preds_list)
|
||||||
|
|
||||||
|
print(f"✓ TimesFM: 涨{(timesfm_preds==1).sum()} ({(timesfm_preds==1).sum()/len(timesfm_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 7.5 CatBoost预测
|
||||||
|
catboost_preds = catboost_model.predict(X_catboost).astype(int)
|
||||||
|
|
||||||
|
print(f"✓ CatBoost: 涨{(catboost_preds==1).sum()} ({(catboost_preds==1).sum()/len(catboost_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# ========== 步骤8: 集成策略评估 ==========
|
||||||
|
print("\n[8/8] 集成策略评估...")
|
||||||
|
|
||||||
|
true_labels = y_dlinear
|
||||||
|
|
||||||
|
# 策略1: 双模型交集(DLinear + N-BEATS)
|
||||||
|
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
|
||||||
|
|
||||||
|
two_model_valid = two_model_preds != -1
|
||||||
|
two_model_correct = (two_model_preds == true_labels) & two_model_valid
|
||||||
|
two_model_acc = two_model_correct.sum() / two_model_valid.sum() * 100 if two_model_valid.sum() > 0 else 0
|
||||||
|
two_model_coverage = two_model_valid.sum() / len(true_labels) * 100
|
||||||
|
|
||||||
|
# 策略2: 三模型投票(DLinear + N-BEATS + TTM-R2)
|
||||||
|
vote_sum_3 = dlinear_preds + nbeats_preds + ttm_preds
|
||||||
|
three_model_preds = np.where(vote_sum_3 >= 2, 1, 0)
|
||||||
|
three_model_acc = (three_model_preds == true_labels).mean() * 100
|
||||||
|
|
||||||
|
# 策略3: 四模型投票
|
||||||
|
vote_sum_4 = dlinear_preds + nbeats_preds + ttm_preds + timesfm_preds
|
||||||
|
four_model_preds = np.where(vote_sum_4 >= 3, 1, 0)
|
||||||
|
four_model_acc = (four_model_preds == true_labels).mean() * 100
|
||||||
|
|
||||||
|
# 策略4: 五模型强投票(≥3票)
|
||||||
|
vote_sum_5 = dlinear_preds + nbeats_preds + ttm_preds + timesfm_preds + catboost_preds
|
||||||
|
five_model_preds_strong = np.where(vote_sum_5 >= 3, 1, 0)
|
||||||
|
five_model_acc_strong = (five_model_preds_strong == true_labels).mean() * 100
|
||||||
|
|
||||||
|
# 策略5: 五模型弱投票(≥2票)
|
||||||
|
five_model_preds_weak = np.where(vote_sum_5 >= 2, 1, 0)
|
||||||
|
five_model_acc_weak = (five_model_preds_weak == true_labels).mean() * 100
|
||||||
|
|
||||||
|
# ========== 结果展示 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 五模型集成策略对比结果")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print(f"\n单模型性能:")
|
||||||
|
print(f" DLinear: {(dlinear_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
print(f" CatBoost: {(catboost_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
print(f" TimesFM 2.5: {(timesfm_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
print(f" TTM-R2: {(ttm_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
print(f" N-BEATS: {(nbeats_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
|
||||||
|
print(f"\n集成策略性能:")
|
||||||
|
print(f" 双模型交集 (DL+NB): {two_model_acc:.2f}% (覆盖率: {two_model_coverage:.2f}%)")
|
||||||
|
print(f" 三模型投票 (DL+NB+TTM, ≥2票): {three_model_acc:.2f}% (覆盖率: 100%)")
|
||||||
|
print(f" 四模型投票 (前4, ≥3票): {four_model_acc:.2f}% (覆盖率: 100%)")
|
||||||
|
print(f" 五模型强投票 (所有, ≥3票): {five_model_acc_strong:.2f}% (覆盖率: 100%)")
|
||||||
|
print(f" 五模型弱投票 (所有, ≥2票): {five_model_acc_weak:.2f}% (覆盖率: 100%)")
|
||||||
|
|
||||||
|
# 投票分布分析
|
||||||
|
print(f"\n五模型投票分布:")
|
||||||
|
for votes in range(6):
|
||||||
|
count = (vote_sum_5 == votes).sum()
|
||||||
|
pct = count / len(vote_sum_5) * 100
|
||||||
|
bars = "█" * int(pct / 2)
|
||||||
|
print(f" {votes}票同意: {count:3d}次 ({pct:5.1f}%) {bars}")
|
||||||
|
|
||||||
|
# 模型一致性分析
|
||||||
|
print(f"\n模型一致性分析:")
|
||||||
|
all_agree_up = (dlinear_preds == 1) & (nbeats_preds == 1) & (ttm_preds == 1) & (timesfm_preds == 1) & (catboost_preds == 1)
|
||||||
|
all_agree_down = (dlinear_preds == 0) & (nbeats_preds == 0) & (ttm_preds == 0) & (timesfm_preds == 0) & (catboost_preds == 0)
|
||||||
|
all_agree = all_agree_up | all_agree_down
|
||||||
|
|
||||||
|
print(f" 五模型完全一致: {all_agree.sum()}次 ({all_agree.sum()/len(all_agree)*100:.1f}%)")
|
||||||
|
if all_agree.sum() > 0:
|
||||||
|
all_agree_acc = ((five_model_preds_strong[all_agree] == true_labels[all_agree]).mean() * 100)
|
||||||
|
print(f" 一致时准确率: {all_agree_acc:.2f}%")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 五模型集成预测完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
"""四模型集成预测:DLinear + N-BEATS + TTM-R2 + TimesFM 2.5"""
|
||||||
|
|
||||||
|
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 data.fetcher import BinanceDataFetcher
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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("四模型集成预测:DLinear + N-BEATS + TTM-R2 + TimesFM 2.5")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 步骤1: 加载DLinear和N-BEATS模型 ==========
|
||||||
|
print("\n[1/7] 加载DLinear和N-BEATS模型...")
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
|
||||||
|
# 加载DLinear
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(f"✓ DLinear已加载")
|
||||||
|
|
||||||
|
# 加载N-BEATS
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(f"✓ N-BEATS已加载")
|
||||||
|
|
||||||
|
# ========== 步骤2: 加载TTM-R2 ==========
|
||||||
|
print("\n[2/7] 加载TTM-R2零样本模型...")
|
||||||
|
|
||||||
|
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(f"✓ TTM-R2已加载 (参数量: {sum(p.numel() for p in ttm_model.parameters())})")
|
||||||
|
|
||||||
|
# ========== 步骤3: 加载TimesFM 2.5 ==========
|
||||||
|
print("\n[3/7] 加载TimesFM 2.5模型...")
|
||||||
|
|
||||||
|
import timesfm
|
||||||
|
|
||||||
|
timesfm_model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
||||||
|
"google/timesfm-2.5-200m-pytorch",
|
||||||
|
torch_compile=False # 推理时不使用torch.compile
|
||||||
|
)
|
||||||
|
|
||||||
|
# 配置TimesFM用于BTC 1小时预测
|
||||||
|
timesfm_model.compile(
|
||||||
|
timesfm.ForecastConfig(
|
||||||
|
max_context=512, # 使用512小时历史数据
|
||||||
|
max_horizon=1, # 预测未来1小时
|
||||||
|
normalize_inputs=True,
|
||||||
|
use_continuous_quantile_head=True,
|
||||||
|
force_flip_invariance=True,
|
||||||
|
infer_is_positive=True,
|
||||||
|
fix_quantile_crossing=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✓ TimesFM 2.5已加载并配置")
|
||||||
|
print(f" 上下文长度: 512小时")
|
||||||
|
print(f" 预测长度: 1小时")
|
||||||
|
|
||||||
|
# ========== 步骤4: 获取测试数据 ==========
|
||||||
|
print("\n[4/7] 获取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)} 行")
|
||||||
|
print(f"✓ 时间范围: {df_2026['timestamp'].min()} 至 {df_2026['timestamp'].max()}")
|
||||||
|
|
||||||
|
# ========== 步骤5: 预处理 ==========
|
||||||
|
print("\n[5/7] 预处理数据...")
|
||||||
|
|
||||||
|
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]
|
||||||
|
y_nbeats = y_nbeats[:min_samples]
|
||||||
|
|
||||||
|
print(f"✓ 对齐样本数: {min_samples}")
|
||||||
|
|
||||||
|
# ========== 步骤6: 四个模型分别预测 ==========
|
||||||
|
print("\n[6/7] 四个模型分别预测...")
|
||||||
|
|
||||||
|
# 6.1 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)) # sigmoid
|
||||||
|
dlinear_preds = (dlinear_probs > 0.5).astype(int).flatten() # 1=涨, 0=跌
|
||||||
|
|
||||||
|
print(f"✓ DLinear预测完成")
|
||||||
|
print(f" 预测上涨: {(dlinear_preds==1).sum()} ({(dlinear_preds==1).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(dlinear_preds==0).sum()} ({(dlinear_preds==0).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 6.2 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()
|
||||||
|
|
||||||
|
# 还原真实价格并使用当前K线open价格判断
|
||||||
|
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) # 1=涨, 0=跌
|
||||||
|
|
||||||
|
print(f"✓ N-BEATS预测完成 (使用当前K线open价格判断)")
|
||||||
|
print(f" 预测上涨: {(nbeats_preds==1).sum()} ({(nbeats_preds==1).sum()/len(nbeats_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(nbeats_preds==0).sum()} ({(nbeats_preds==0).sum()/len(nbeats_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 6.3 TTM-R2预测
|
||||||
|
print(f"✓ 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)
|
||||||
|
|
||||||
|
print(f"✓ TTM-R2预测完成")
|
||||||
|
print(f" 预测上涨: {(ttm_preds==1).sum()} ({(ttm_preds==1).sum()/len(ttm_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(ttm_preds==0).sum()} ({(ttm_preds==0).sum()/len(ttm_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 6.4 TimesFM 2.5预测
|
||||||
|
print(f"✓ TimesFM 2.5预测中...")
|
||||||
|
|
||||||
|
timesfm_preds_list = []
|
||||||
|
|
||||||
|
for i in range(min_samples):
|
||||||
|
# TimesFM使用512小时上下文
|
||||||
|
start_idx = i
|
||||||
|
end_idx = start_idx + 512
|
||||||
|
|
||||||
|
if end_idx > len(all_prices):
|
||||||
|
# 如果数据不足,使用最后512个数据点
|
||||||
|
context_prices = all_prices[-512:]
|
||||||
|
current_open_price = all_open_prices[min(168 + i, len(all_open_prices) - 1)]
|
||||||
|
else:
|
||||||
|
context_prices = all_prices[start_idx:end_idx]
|
||||||
|
current_open_price = all_open_prices[end_idx] if end_idx < len(all_open_prices) else all_open_prices[-1]
|
||||||
|
|
||||||
|
# TimesFM的forecast方法接受list of arrays作为inputs
|
||||||
|
try:
|
||||||
|
point_forecast, _ = timesfm_model.forecast(
|
||||||
|
horizon=1,
|
||||||
|
inputs=[context_prices], # list包含单个序列
|
||||||
|
)
|
||||||
|
|
||||||
|
# point_forecast形状: (1, 1) - (num_series, horizon)
|
||||||
|
pred_price = point_forecast[0, 0]
|
||||||
|
|
||||||
|
# 与当前K线open价格比较
|
||||||
|
timesfm_preds_list.append(1 if pred_price > current_open_price else 0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ TimesFM预测第{i}个样本失败: {e}")
|
||||||
|
# 使用随机预测作为fallback
|
||||||
|
timesfm_preds_list.append(np.random.randint(0, 2))
|
||||||
|
|
||||||
|
timesfm_preds = np.array(timesfm_preds_list)
|
||||||
|
|
||||||
|
print(f"✓ TimesFM 2.5预测完成")
|
||||||
|
print(f" 预测上涨: {(timesfm_preds==1).sum()} ({(timesfm_preds==1).sum()/len(timesfm_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(timesfm_preds==0).sum()} ({(timesfm_preds==0).sum()/len(timesfm_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# ========== 步骤7: 集成策略评估 ==========
|
||||||
|
print("\n[7/7] 集成策略评估...")
|
||||||
|
|
||||||
|
true_labels = y_dlinear
|
||||||
|
|
||||||
|
# 策略1: 双模型交集(DLinear + N-BEATS)
|
||||||
|
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
|
||||||
|
|
||||||
|
two_model_valid = two_model_preds != -1
|
||||||
|
two_model_correct = (two_model_preds == true_labels) & two_model_valid
|
||||||
|
two_model_acc = two_model_correct.sum() / two_model_valid.sum() * 100 if two_model_valid.sum() > 0 else 0
|
||||||
|
two_model_coverage = two_model_valid.sum() / len(true_labels) * 100
|
||||||
|
|
||||||
|
# 策略2: 三模型多数投票(DLinear + N-BEATS + TTM-R2)
|
||||||
|
vote_sum_3 = dlinear_preds + nbeats_preds + ttm_preds
|
||||||
|
three_model_preds = np.where(vote_sum_3 >= 2, 1, 0)
|
||||||
|
three_model_acc = (three_model_preds == true_labels).mean() * 100
|
||||||
|
|
||||||
|
# 策略3: 四模型多数投票(至少3个模型同意)
|
||||||
|
vote_sum_4 = dlinear_preds + nbeats_preds + ttm_preds + timesfm_preds
|
||||||
|
four_model_preds = np.where(vote_sum_4 >= 3, 1, 0)
|
||||||
|
four_model_acc = (four_model_preds == true_labels).mean() * 100
|
||||||
|
|
||||||
|
# 策略4: 四模型半数以上投票(至少2个模型同意)
|
||||||
|
four_model_preds_half = np.where(vote_sum_4 >= 2, 1, 0)
|
||||||
|
four_model_acc_half = (four_model_preds_half == true_labels).mean() * 100
|
||||||
|
|
||||||
|
# ========== 结果展示 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 集成策略对比结果")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print(f"\n单模型性能:")
|
||||||
|
print(f" DLinear: {(dlinear_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
print(f" N-BEATS: {(nbeats_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
print(f" TTM-R2: {(ttm_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
print(f" TimesFM 2.5: {(timesfm_preds == true_labels).mean() * 100:.2f}%")
|
||||||
|
|
||||||
|
print(f"\n集成策略性能:")
|
||||||
|
print(f" 双模型交集 (DL+NB): {two_model_acc:.2f}% (覆盖率: {two_model_coverage:.2f}%)")
|
||||||
|
print(f" 三模型投票 (DL+NB+TTM, ≥2票): {three_model_acc:.2f}% (覆盖率: 100%)")
|
||||||
|
print(f" 四模型强投票 (所有, ≥3票): {four_model_acc:.2f}% (覆盖率: 100%)")
|
||||||
|
print(f" 四模型弱投票 (所有, ≥2票): {four_model_acc_half:.2f}% (覆盖率: 100%)")
|
||||||
|
|
||||||
|
# 投票分布分析
|
||||||
|
print(f"\n四模型投票分布:")
|
||||||
|
for votes in range(5):
|
||||||
|
count = (vote_sum_4 == votes).sum()
|
||||||
|
pct = count / len(vote_sum_4) * 100
|
||||||
|
bars = "█" * int(pct / 2)
|
||||||
|
print(f" {votes}票同意: {count:3d}次 ({pct:5.1f}%) {bars}")
|
||||||
|
|
||||||
|
# 模型一致性分析
|
||||||
|
print(f"\n模型一致性分析:")
|
||||||
|
all_agree_up = (dlinear_preds == 1) & (nbeats_preds == 1) & (ttm_preds == 1) & (timesfm_preds == 1)
|
||||||
|
all_agree_down = (dlinear_preds == 0) & (nbeats_preds == 0) & (ttm_preds == 0) & (timesfm_preds == 0)
|
||||||
|
all_agree = all_agree_up | all_agree_down
|
||||||
|
|
||||||
|
print(f" 四模型完全一致: {all_agree.sum()}次 ({all_agree.sum()/len(all_agree)*100:.1f}%)")
|
||||||
|
if all_agree.sum() > 0:
|
||||||
|
all_agree_acc = ((four_model_preds[all_agree] == true_labels[all_agree]).mean() * 100)
|
||||||
|
print(f" 一致时准确率: {all_agree_acc:.2f}%")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 四模型集成预测完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
"""N-BEATS + DLinear 模型集成预测 - 交集策略"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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("模型集成预测:N-BEATS + DLinear 交集策略")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 步骤1: 加载两个模型 ==========
|
||||||
|
print("\n[1/5] 加载两个模型...")
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
|
||||||
|
# 加载DLinear分类模型
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(f"✓ DLinear分类模型已加载 (准确率: 53.10%)")
|
||||||
|
|
||||||
|
# 加载N-BEATS回归模型
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(f"✓ N-BEATS回归模型已加载 (准确率: 50.83%)")
|
||||||
|
|
||||||
|
# ========== 步骤2: 获取2026年测试数据 ==========
|
||||||
|
print("\n[2/5] 获取2026年测试数据...")
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
|
||||||
|
try:
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
||||||
|
end_date='2026-12-31', use_cache=False)
|
||||||
|
except:
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', use_cache=False)
|
||||||
|
|
||||||
|
print(f"✓ 测试数据: {len(df_2026)} 行")
|
||||||
|
print(f"✓ 时间: {df_2026['timestamp'].min()} 至 {df_2026['timestamp'].max()}")
|
||||||
|
|
||||||
|
# ========== 步骤3: 预处理数据 ==========
|
||||||
|
print("\n[3/5] 预处理数据...")
|
||||||
|
|
||||||
|
# DLinear预处理
|
||||||
|
X_dlinear, y_dlinear, df_dlinear = dlinear_preprocessor.preprocess(df_2026, fit=False)
|
||||||
|
|
||||||
|
# N-BEATS预处理
|
||||||
|
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]
|
||||||
|
y_nbeats = y_nbeats[:min_samples]
|
||||||
|
|
||||||
|
print(f"✓ 对齐样本数: {min_samples}")
|
||||||
|
|
||||||
|
# ========== 步骤4: 两个模型分别预测 ==========
|
||||||
|
print("\n[4/5] 两个模型分别预测...")
|
||||||
|
|
||||||
|
# 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)) # sigmoid
|
||||||
|
dlinear_preds = (dlinear_probs > 0.5).astype(int).flatten() # 1=涨, 0=跌
|
||||||
|
|
||||||
|
print(f"✓ DLinear预测完成")
|
||||||
|
print(f" 预测上涨: {(dlinear_preds==1).sum()} ({(dlinear_preds==1).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(dlinear_preds==0).sum()} ({(dlinear_preds==0).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 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_prices = df_nbeats['close'].iloc[168:168+len(nbeats_pred_prices)].values
|
||||||
|
nbeats_preds = (nbeats_pred_prices > current_prices).astype(int) # 1=涨, 0=跌
|
||||||
|
|
||||||
|
print(f"✓ N-BEATS预测完成")
|
||||||
|
print(f" 预测上涨: {(nbeats_preds==1).sum()} ({(nbeats_preds==1).sum()/len(nbeats_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(nbeats_preds==0).sum()} ({(nbeats_preds==0).sum()/len(nbeats_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# ========== 步骤5: 交集策略 ==========
|
||||||
|
print("\n[5/5] 应用交集策略...")
|
||||||
|
|
||||||
|
# 真实标签
|
||||||
|
true_labels = y_dlinear
|
||||||
|
|
||||||
|
# 策略1: 两个模型都预测上涨 → 上涨
|
||||||
|
# 策略2: 两个模型都预测下跌 → 下跌
|
||||||
|
# 策略3: 两个模型预测不一致 → 不做决策(弃权)
|
||||||
|
|
||||||
|
both_agree_up = (dlinear_preds == 1) & (nbeats_preds == 1)
|
||||||
|
both_agree_down = (dlinear_preds == 0) & (nbeats_preds == 0)
|
||||||
|
disagree = (dlinear_preds != nbeats_preds)
|
||||||
|
|
||||||
|
n_agree_up = both_agree_up.sum()
|
||||||
|
n_agree_down = both_agree_down.sum()
|
||||||
|
n_disagree = disagree.sum()
|
||||||
|
|
||||||
|
print(f"\n📊 预测一致性分析:")
|
||||||
|
print(f" 双方都预测上涨: {n_agree_up} ({n_agree_up/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 双方都预测下跌: {n_agree_down} ({n_agree_down/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 双方预测不一致: {n_disagree} ({n_disagree/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 只在两个模型一致时做决策
|
||||||
|
agree_mask = both_agree_up | both_agree_down
|
||||||
|
ensemble_preds = np.full(len(dlinear_preds), -1) # -1表示弃权
|
||||||
|
ensemble_preds[both_agree_up] = 1
|
||||||
|
ensemble_preds[both_agree_down] = 0
|
||||||
|
|
||||||
|
# 有效预测(非弃权)
|
||||||
|
valid_mask = ensemble_preds != -1
|
||||||
|
valid_preds = ensemble_preds[valid_mask]
|
||||||
|
valid_labels = true_labels[valid_mask]
|
||||||
|
|
||||||
|
print(f"\n📊 集成策略结果:")
|
||||||
|
print(f" 有效预测数: {valid_mask.sum()} ({valid_mask.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 弃权数: {n_disagree} ({n_disagree/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
if len(valid_preds) > 0:
|
||||||
|
ensemble_accuracy = (valid_preds == valid_labels).mean()
|
||||||
|
print(f"\n✨ 交集策略准确率: {ensemble_accuracy*100:.2f}%")
|
||||||
|
|
||||||
|
# 混淆矩阵
|
||||||
|
cm = confusion_matrix(valid_labels, valid_preds)
|
||||||
|
tn, fp, fn, tp = cm.ravel()
|
||||||
|
|
||||||
|
print(f"\n详细分析(仅有效预测):")
|
||||||
|
print(f" ✓ 预测涨且真涨: {tp} ({tp/len(valid_preds)*100:.1f}%)")
|
||||||
|
print(f" ✓ 预测跌且真跌: {tn} ({tn/len(valid_preds)*100:.1f}%)")
|
||||||
|
print(f" ✗ 预测涨但真跌: {fp} ({fp/len(valid_preds)*100:.1f}%)")
|
||||||
|
print(f" ✗ 预测跌但真涨: {fn} ({fn/len(valid_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# ========== 对比分析 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 性能对比")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
dlinear_acc = (dlinear_preds == true_labels).mean()
|
||||||
|
nbeats_acc = (nbeats_preds == true_labels).mean()
|
||||||
|
|
||||||
|
print(f"\n单模型性能:")
|
||||||
|
print(f" DLinear分类: {dlinear_acc*100:.2f}%")
|
||||||
|
print(f" N-BEATS回归: {nbeats_acc*100:.2f}%")
|
||||||
|
|
||||||
|
if len(valid_preds) > 0:
|
||||||
|
print(f"\n集成策略性能:")
|
||||||
|
print(f" 交集准确率: {ensemble_accuracy*100:.2f}%")
|
||||||
|
print(f" 覆盖率: {valid_mask.sum()/len(dlinear_preds)*100:.1f}%")
|
||||||
|
print(f" 弃权率: {n_disagree/len(dlinear_preds)*100:.1f}%")
|
||||||
|
|
||||||
|
# 性能评估
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("🎯 策略评估")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
if ensemble_accuracy > max(dlinear_acc, nbeats_acc):
|
||||||
|
improvement = ensemble_accuracy - max(dlinear_acc, nbeats_acc)
|
||||||
|
print(f"\n ✅ 交集策略优于单模型!")
|
||||||
|
print(f" 准确率提升: {improvement*100:.2f}个百分点")
|
||||||
|
print(f" 代价: 弃权{n_disagree}次预测 ({n_disagree/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
else:
|
||||||
|
print(f"\n ⚠️ 交集策略未能提升准确率")
|
||||||
|
print(f" 可能原因: 两个模型一致时也可能都错")
|
||||||
|
|
||||||
|
# 计算如果弃权时不交易的盈利
|
||||||
|
print(f"\n💰 实际收益分析:")
|
||||||
|
print(f" 如果只在两个模型都同意时交易:")
|
||||||
|
print(f" - 交易次数: {valid_mask.sum()} ({valid_mask.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" - 准确率: {ensemble_accuracy*100:.2f}%")
|
||||||
|
print(f" - 避免交易: {n_disagree}次 (规避不确定性)")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 集成预测分析完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
"""三模型集成预测 - DLinear + N-BEATS + TTM-R2 交集策略"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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("三模型集成预测:DLinear + N-BEATS + TTM-R2 交集策略")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 步骤1: 加载三个模型 ==========
|
||||||
|
print("\n[1/6] 加载三个模型...")
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
|
||||||
|
# 1. 加载DLinear分类模型
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(f"✓ DLinear分类模型已加载 (准确率: 53.10%)")
|
||||||
|
|
||||||
|
# 2. 加载N-BEATS回归模型
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(f"✓ N-BEATS回归模型已加载 (准确率: 50.83%)")
|
||||||
|
|
||||||
|
# 3. 加载TTM-R2模型
|
||||||
|
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(f"✓ TTM-R2模型已加载 (准确率: 37.50%)")
|
||||||
|
|
||||||
|
# ========== 步骤2: 获取2026年测试数据 ==========
|
||||||
|
print("\n[2/6] 获取2026年测试数据...")
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
|
||||||
|
try:
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
||||||
|
end_date='2026-02-02', use_cache=False)
|
||||||
|
except:
|
||||||
|
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', use_cache=False)
|
||||||
|
|
||||||
|
print(f"✓ 测试数据: {len(df_2026)} 行")
|
||||||
|
print(f"✓ 时间: {df_2026['timestamp'].min()} 至 {df_2026['timestamp'].max()}")
|
||||||
|
|
||||||
|
# ========== 步骤3: 预处理数据 ==========
|
||||||
|
print("\n[3/6] 预处理数据...")
|
||||||
|
|
||||||
|
# DLinear预处理
|
||||||
|
X_dlinear, y_dlinear, df_dlinear = dlinear_preprocessor.preprocess(df_2026, fit=False)
|
||||||
|
|
||||||
|
# N-BEATS预处理
|
||||||
|
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]
|
||||||
|
y_nbeats = y_nbeats[:min_samples]
|
||||||
|
|
||||||
|
print(f"✓ 对齐样本数: {min_samples}")
|
||||||
|
|
||||||
|
# ========== 步骤4: 三个模型分别预测 ==========
|
||||||
|
print("\n[4/6] 三个模型分别预测...")
|
||||||
|
|
||||||
|
# 4.1 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)) # sigmoid
|
||||||
|
dlinear_preds = (dlinear_probs > 0.5).astype(int).flatten() # 1=涨, 0=跌
|
||||||
|
|
||||||
|
print(f"✓ DLinear预测完成")
|
||||||
|
print(f" 预测上涨: {(dlinear_preds==1).sum()} ({(dlinear_preds==1).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(dlinear_preds==0).sum()} ({(dlinear_preds==0).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 4.2 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)
|
||||||
|
|
||||||
|
# 【修改】获取当前K线的open价格(未来可交易价格)用于判断方向
|
||||||
|
# 第i个样本对应的是第168+i根K线,其open价格是真正可以交易的价格
|
||||||
|
current_open_prices = df_nbeats['open'].iloc[168:168+len(nbeats_pred_prices)].values
|
||||||
|
nbeats_preds = (nbeats_pred_prices > current_open_prices).astype(int) # 1=涨, 0=跌
|
||||||
|
|
||||||
|
print(f"✓ N-BEATS预测完成 (使用当前K线open价格判断)")
|
||||||
|
print(f" 预测上涨: {(nbeats_preds==1).sum()} ({(nbeats_preds==1).sum()/len(nbeats_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(nbeats_preds==0).sum()} ({(nbeats_preds==0).sum()/len(nbeats_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 4.3 TTM-R2预测
|
||||||
|
print(f"✓ TTM-R2预测中...")
|
||||||
|
|
||||||
|
# TTM-R2需要特定格式: (batch_size, context_length, num_features)
|
||||||
|
context_length = 512
|
||||||
|
ttm_preds_list = []
|
||||||
|
|
||||||
|
# 从df_2026获取价格数据
|
||||||
|
all_prices = df_2026['close'].values
|
||||||
|
all_open_prices = df_2026['open'].values
|
||||||
|
|
||||||
|
# 为每个样本创建预测
|
||||||
|
for i in range(min_samples):
|
||||||
|
# 计算当前样本在原始数据中的位置
|
||||||
|
# DLinear的lookback是168,所以第i个样本对应df_2026的第168+i根K线
|
||||||
|
start_idx = i
|
||||||
|
end_idx = start_idx + context_length
|
||||||
|
|
||||||
|
if end_idx > len(all_prices):
|
||||||
|
# 如果超出范围,使用最后512个价格
|
||||||
|
context_data = all_prices[-context_length:].reshape(1, -1, 1)
|
||||||
|
# 【修改】使用对应K线的open价格作为当前可交易价格
|
||||||
|
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)
|
||||||
|
# 【修改】使用第end_idx根K线的open价格(即当前K线的开盘价)
|
||||||
|
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]
|
||||||
|
|
||||||
|
# 【修改】判断方向: 预测价格 vs 当前K线open价格
|
||||||
|
ttm_preds_list.append(1 if pred_price > current_open_price else 0)
|
||||||
|
|
||||||
|
ttm_preds = np.array(ttm_preds_list)
|
||||||
|
|
||||||
|
print(f"✓ TTM-R2预测完成 (使用当前K线open价格判断)")
|
||||||
|
print(f" 预测上涨: {(ttm_preds==1).sum()} ({(ttm_preds==1).sum()/len(ttm_preds)*100:.1f}%)")
|
||||||
|
print(f" 预测下跌: {(ttm_preds==0).sum()} ({(ttm_preds==0).sum()/len(ttm_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# ========== 步骤5: 三模型多数投票策略(两方相同) ==========
|
||||||
|
print("\n[5/6] 应用三模型多数投票策略(两方相同)...")
|
||||||
|
|
||||||
|
# 真实标签
|
||||||
|
true_labels = y_dlinear
|
||||||
|
|
||||||
|
# 多数投票:至少两个模型预测上涨 → 上涨,至少两个预测下跌 → 下跌
|
||||||
|
vote_sum = dlinear_preds + nbeats_preds + ttm_preds
|
||||||
|
|
||||||
|
# vote_sum >= 2 表示至少两个模型预测上涨(1)
|
||||||
|
# vote_sum <= 1 表示至少两个模型预测下跌(0)
|
||||||
|
majority_up = vote_sum >= 2
|
||||||
|
majority_down = vote_sum <= 1
|
||||||
|
|
||||||
|
# 统计各种情况
|
||||||
|
all_agree_up = (dlinear_preds == 1) & (nbeats_preds == 1) & (ttm_preds == 1)
|
||||||
|
all_agree_down = (dlinear_preds == 0) & (nbeats_preds == 0) & (ttm_preds == 0)
|
||||||
|
|
||||||
|
# 恰好两方同意(不包括三方都同意)
|
||||||
|
exactly_two_up = majority_up & ~all_agree_up
|
||||||
|
exactly_two_down = majority_down & ~all_agree_down
|
||||||
|
|
||||||
|
n_all_agree_up = all_agree_up.sum()
|
||||||
|
n_all_agree_down = all_agree_down.sum()
|
||||||
|
n_exactly_two_up = exactly_two_up.sum()
|
||||||
|
n_exactly_two_down = exactly_two_down.sum()
|
||||||
|
|
||||||
|
print(f"\n📊 预测投票分析:")
|
||||||
|
print(f" 三方都预测上涨: {n_all_agree_up} ({n_all_agree_up/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 三方都预测下跌: {n_all_agree_down} ({n_all_agree_down/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 恰好两方预测上涨: {n_exactly_two_up} ({n_exactly_two_up/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 恰好两方预测下跌: {n_exactly_two_down} ({n_exactly_two_down/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 详细显示是哪两方同意
|
||||||
|
dl_nb_up = (dlinear_preds == 1) & (nbeats_preds == 1) & (ttm_preds == 0)
|
||||||
|
dl_ttm_up = (dlinear_preds == 1) & (nbeats_preds == 0) & (ttm_preds == 1)
|
||||||
|
nb_ttm_up = (dlinear_preds == 0) & (nbeats_preds == 1) & (ttm_preds == 1)
|
||||||
|
|
||||||
|
dl_nb_down = (dlinear_preds == 0) & (nbeats_preds == 0) & (ttm_preds == 1)
|
||||||
|
dl_ttm_down = (dlinear_preds == 0) & (nbeats_preds == 1) & (ttm_preds == 0)
|
||||||
|
nb_ttm_down = (dlinear_preds == 1) & (nbeats_preds == 0) & (ttm_preds == 0)
|
||||||
|
|
||||||
|
print(f"\n 详细组合:")
|
||||||
|
print(f" DLinear+NBEATS同意看涨: {dl_nb_up.sum()} ({dl_nb_up.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" DLinear+TTM同意看涨: {dl_ttm_up.sum()} ({dl_ttm_up.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" NBEATS+TTM同意看涨: {nb_ttm_up.sum()} ({nb_ttm_up.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" DLinear+NBEATS同意看跌: {dl_nb_down.sum()} ({dl_nb_down.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" DLinear+TTM同意看跌: {dl_ttm_down.sum()} ({dl_ttm_down.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" NBEATS+TTM同意看跌: {nb_ttm_down.sum()} ({nb_ttm_down.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 多数投票结果
|
||||||
|
ensemble_preds = np.where(majority_up, 1, 0)
|
||||||
|
|
||||||
|
print(f"\n📊 三模型多数投票策略结果:")
|
||||||
|
print(f" 最终预测上涨: {(ensemble_preds==1).sum()} ({(ensemble_preds==1).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 最终预测下跌: {(ensemble_preds==0).sum()} ({(ensemble_preds==0).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" 覆盖率: 100% (多数投票无弃权)")
|
||||||
|
|
||||||
|
# ========== 步骤6: 性能对比 ==========
|
||||||
|
print("\n[6/6] 性能对比分析...")
|
||||||
|
|
||||||
|
# 计算多数投票准确率
|
||||||
|
ensemble_accuracy = (ensemble_preds == true_labels).mean()
|
||||||
|
print(f"\n✨ 三模型多数投票准确率: {ensemble_accuracy*100:.2f}%")
|
||||||
|
|
||||||
|
# 混淆矩阵
|
||||||
|
cm = confusion_matrix(true_labels, ensemble_preds)
|
||||||
|
tn, fp, fn, tp = cm.ravel()
|
||||||
|
|
||||||
|
print(f"\n详细分析:")
|
||||||
|
print(f" ✓ 预测涨且真涨: {tp} ({tp/len(ensemble_preds)*100:.1f}%)")
|
||||||
|
print(f" ✓ 预测跌且真跌: {tn} ({tn/len(ensemble_preds)*100:.1f}%)")
|
||||||
|
print(f" ✗ 预测涨但真跌: {fp} ({fp/len(ensemble_preds)*100:.1f}%)")
|
||||||
|
print(f" ✗ 预测跌但真涨: {fn} ({fn/len(ensemble_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 对比分析
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 性能对比")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
dlinear_acc = (dlinear_preds == true_labels).mean()
|
||||||
|
nbeats_acc = (nbeats_preds == true_labels).mean()
|
||||||
|
ttm_acc = (ttm_preds == true_labels).mean()
|
||||||
|
|
||||||
|
print(f"\n单模型性能:")
|
||||||
|
print(f" DLinear分类: {dlinear_acc*100:.2f}%")
|
||||||
|
print(f" N-BEATS回归: {nbeats_acc*100:.2f}%")
|
||||||
|
print(f" TTM-R2零样本: {ttm_acc*100:.2f}%")
|
||||||
|
|
||||||
|
# 两模型交集(DLinear + N-BEATS)
|
||||||
|
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
|
||||||
|
two_model_valid = two_model_preds != -1
|
||||||
|
two_model_acc = (two_model_preds[two_model_valid] == true_labels[two_model_valid]).mean()
|
||||||
|
|
||||||
|
print(f"\n集成策略性能:")
|
||||||
|
print(f" 双模型交集 (DLinear+NBEATS): {two_model_acc*100:.2f}% (覆盖率: {two_model_valid.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
if len(valid_preds) > 0:
|
||||||
|
print(f" 三模型交集 (ALL): {ensemble_accuracy*100:.2f}% (覆盖率: {valid_mask.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
|
||||||
|
# 性能评估
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("🎯 策略评估")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
best_single = max(dlinear_acc, nbeats_acc, ttm_acc)
|
||||||
|
|
||||||
|
if ensemble_accuracy > best_single:
|
||||||
|
improvement = ensemble_accuracy - best_single
|
||||||
|
print(f"\n ✅ 三模型交集优于所有单模型!")
|
||||||
|
print(f" 准确率提升: {improvement*100:.2f}个百分点")
|
||||||
|
print(f" 代价: 弃权{(~valid_mask).sum()}次预测 ({(~valid_mask).sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
elif ensemble_accuracy > two_model_acc:
|
||||||
|
improvement = ensemble_accuracy - two_model_acc
|
||||||
|
print(f"\n ✅ 三模型交集优于双模型交集!")
|
||||||
|
print(f" 准确率提升: {improvement*100:.2f}个百分点")
|
||||||
|
print(f" 但覆盖率下降: {(two_model_valid.sum() - valid_mask.sum())/len(dlinear_preds)*100:.1f}%")
|
||||||
|
else:
|
||||||
|
print(f"\n ⚠️ 三模型交集未能提升准确率")
|
||||||
|
print(f" 可能原因: TTM-R2表现太差({ttm_acc*100:.2f}%),拖累了整体性能")
|
||||||
|
print(f" 建议: 使用双模型交集(DLinear+NBEATS)")
|
||||||
|
|
||||||
|
# 实际收益分析
|
||||||
|
print(f"\n💰 实际收益分析:")
|
||||||
|
print(f" 如果只在三个模型都同意时交易:")
|
||||||
|
print(f" - 交易次数: {valid_mask.sum()} ({valid_mask.sum()/len(dlinear_preds)*100:.1f}%)")
|
||||||
|
print(f" - 准确率: {ensemble_accuracy*100:.2f}%")
|
||||||
|
print(f" - 避免交易: {(~valid_mask).sum()}次 (规避不确定性)")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 三模型集成预测分析完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
"""将Google TimesFM 2.5模型导出为ONNX格式 - 使用正确的API"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def export_timesfm_to_onnx():
|
||||||
|
print("="*80)
|
||||||
|
print("导出TimesFM 2.5 (200M) 模型为ONNX格式")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 步骤1: 加载TimesFM模型 ==========
|
||||||
|
print("\n[1/6] 加载TimesFM 2.5模型...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import timesfm
|
||||||
|
|
||||||
|
# 使用正确的from_pretrained方法加载模型
|
||||||
|
print(" 从HuggingFace加载预训练模型...")
|
||||||
|
print(" 模型ID: google/timesfm-2.5-200m-pytorch")
|
||||||
|
|
||||||
|
model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
|
||||||
|
"google/timesfm-2.5-200m-pytorch",
|
||||||
|
torch_compile=False # 导出ONNX时不使用torch.compile
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✓ TimesFM 2.5模型已加载")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ TimesFM未安装或导入失败: {e}")
|
||||||
|
print(" 请运行: pip install git+https://github.com/google-research/timesfm.git")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 加载模型失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ========== 步骤2: 编译模型配置 ==========
|
||||||
|
print("\n[2/6] 配置模型...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 配置模型
|
||||||
|
model.compile(
|
||||||
|
timesfm.ForecastConfig(
|
||||||
|
max_context=512, # BTC历史数据512小时
|
||||||
|
max_horizon=24, # 预测未来24小时
|
||||||
|
normalize_inputs=True,
|
||||||
|
use_continuous_quantile_head=True,
|
||||||
|
force_flip_invariance=True,
|
||||||
|
infer_is_positive=True,
|
||||||
|
fix_quantile_crossing=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✓ 模型配置完成")
|
||||||
|
print(f" 最大上下文长度: 512")
|
||||||
|
print(f" 最大预测长度: 24")
|
||||||
|
print(f" 归一化输入: True")
|
||||||
|
print(f" 使用分位数预测: True")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 配置失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ========== 步骤3: 测试模型 ==========
|
||||||
|
print("\n[3/6] 测试模型推理...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 创建测试数据
|
||||||
|
test_input = [
|
||||||
|
np.linspace(85000, 90000, 512), # 模拟BTC价格从85k到90k
|
||||||
|
np.sin(np.linspace(0, 20, 512)) * 1000 + 87000, # 模拟震荡价格
|
||||||
|
]
|
||||||
|
|
||||||
|
# 测试推理
|
||||||
|
point_forecast, quantile_forecast = model.forecast(
|
||||||
|
horizon=24,
|
||||||
|
inputs=test_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✓ 模型推理成功")
|
||||||
|
print(f" 点预测形状: {point_forecast.shape}") # (2, 24)
|
||||||
|
print(f" 分位数预测形状: {quantile_forecast.shape}") # (2, 24, 11)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 模型测试失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
print(" 继续尝试导出...")
|
||||||
|
|
||||||
|
# ========== 步骤4: 提取核心模型并导出ONNX ==========
|
||||||
|
print("\n[4/6] 提取核心模型...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# TimesFM的核心模型在model.model中
|
||||||
|
core_model = model.model
|
||||||
|
core_model.eval()
|
||||||
|
|
||||||
|
print("✓ 核心模型已提取")
|
||||||
|
print(f" 模型类型: {type(core_model)}")
|
||||||
|
|
||||||
|
# 创建ONNX导出包装器
|
||||||
|
class TimesFMONNXWrapper(torch.nn.Module):
|
||||||
|
"""TimesFM ONNX导出包装器"""
|
||||||
|
def __init__(self, timesfm_core_model):
|
||||||
|
super().__init__()
|
||||||
|
self.model = timesfm_core_model
|
||||||
|
|
||||||
|
def forward(self, inputs, masks=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
inputs: (batch_size, context_length) - 输入时间序列
|
||||||
|
masks: (batch_size, context_length) - 可选mask,1=有效,0=无效
|
||||||
|
Returns:
|
||||||
|
point_forecast: (batch_size, horizon_length) - 点预测
|
||||||
|
"""
|
||||||
|
# 如果没有提供masks,创建全1的mask
|
||||||
|
if masks is None:
|
||||||
|
masks = torch.ones_like(inputs)
|
||||||
|
|
||||||
|
# 调用TimesFM核心模型
|
||||||
|
# 注意:这里需要根据实际的forward签名调整
|
||||||
|
outputs, _ = self.model(inputs, masks, decode_caches=None)
|
||||||
|
|
||||||
|
# outputs包含: (input_embeddings, output_embeddings, output_ts, output_quantile_spread)
|
||||||
|
# output_ts是点预测
|
||||||
|
point_forecast = outputs[2]
|
||||||
|
|
||||||
|
return point_forecast
|
||||||
|
|
||||||
|
wrapped_model = TimesFMONNXWrapper(core_model)
|
||||||
|
wrapped_model.eval()
|
||||||
|
|
||||||
|
print("✓ ONNX包装器已创建")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 提取核心模型失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ========== 步骤5: 导出为ONNX ==========
|
||||||
|
print("\n[5/6] 导出模型为ONNX格式...")
|
||||||
|
|
||||||
|
onnx_path = "./models/timesfm_2p5_200m.onnx"
|
||||||
|
os.makedirs("./models", exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 准备示例输入
|
||||||
|
batch_size = 1
|
||||||
|
context_len = 512
|
||||||
|
|
||||||
|
dummy_input = torch.randn(batch_size, context_len, dtype=torch.float32)
|
||||||
|
dummy_mask = torch.ones(batch_size, context_len, dtype=torch.float32)
|
||||||
|
|
||||||
|
print(f" 示例输入形状: inputs={dummy_input.shape}, masks={dummy_mask.shape}")
|
||||||
|
print(" 开始导出ONNX...")
|
||||||
|
|
||||||
|
# 导出为ONNX
|
||||||
|
torch.onnx.export(
|
||||||
|
wrapped_model,
|
||||||
|
(dummy_input, dummy_mask),
|
||||||
|
onnx_path,
|
||||||
|
export_params=True,
|
||||||
|
opset_version=14,
|
||||||
|
do_constant_folding=True,
|
||||||
|
input_names=['inputs', 'masks'],
|
||||||
|
output_names=['point_forecast'],
|
||||||
|
dynamic_axes={
|
||||||
|
'inputs': {0: 'batch_size', 1: 'context_length'},
|
||||||
|
'masks': {0: 'batch_size', 1: 'context_length'},
|
||||||
|
'point_forecast': {0: 'batch_size', 1: 'horizon_length'}
|
||||||
|
},
|
||||||
|
verbose=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✓ ONNX模型已导出")
|
||||||
|
print(f" 保存路径: {onnx_path}")
|
||||||
|
print(f" 文件大小: {os.path.getsize(onnx_path) / (1024**2):.2f} MB")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ ONNX导出失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# 尝试简化版本
|
||||||
|
print("\n尝试简化导出方案...")
|
||||||
|
try:
|
||||||
|
# 只导出单输入版本
|
||||||
|
torch.onnx.export(
|
||||||
|
wrapped_model,
|
||||||
|
dummy_input,
|
||||||
|
onnx_path,
|
||||||
|
export_params=True,
|
||||||
|
opset_version=14,
|
||||||
|
input_names=['inputs'],
|
||||||
|
output_names=['predictions'],
|
||||||
|
)
|
||||||
|
print(f"✓ 简化版ONNX模型已导出: {onnx_path}")
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"✗ 简化导出也失败: {e2}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ========== 步骤6: 验证ONNX模型 ==========
|
||||||
|
print("\n[6/6] 验证ONNX模型...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import onnx
|
||||||
|
import onnxruntime as ort
|
||||||
|
|
||||||
|
# 加载并检查ONNX模型
|
||||||
|
onnx_model = onnx.load(onnx_path)
|
||||||
|
onnx.checker.check_model(onnx_model)
|
||||||
|
print("✓ ONNX模型结构验证通过")
|
||||||
|
|
||||||
|
# 显示模型信息
|
||||||
|
print(f"\n模型信息:")
|
||||||
|
print(f" 输入:")
|
||||||
|
for input_tensor in onnx_model.graph.input:
|
||||||
|
dims = [d.dim_value if d.dim_value > 0 else 'dynamic' for d in input_tensor.type.tensor_type.shape.dim]
|
||||||
|
print(f" - {input_tensor.name}: {dims}")
|
||||||
|
|
||||||
|
print(f" 输出:")
|
||||||
|
for output_tensor in onnx_model.graph.output:
|
||||||
|
dims = [d.dim_value if d.dim_value > 0 else 'dynamic' for d in output_tensor.type.tensor_type.shape.dim]
|
||||||
|
print(f" - {output_tensor.name}: {dims}")
|
||||||
|
|
||||||
|
# 创建ONNX Runtime推理会话
|
||||||
|
print("\n 创建ONNX Runtime推理会话...")
|
||||||
|
ort_session = ort.InferenceSession(onnx_path)
|
||||||
|
|
||||||
|
# 准备测试输入
|
||||||
|
test_input = np.random.randn(1, 512).astype(np.float32)
|
||||||
|
test_mask = np.ones((1, 512), dtype=np.float32)
|
||||||
|
|
||||||
|
# 运行推理
|
||||||
|
print(" 运行推理测试...")
|
||||||
|
outputs = ort_session.run(
|
||||||
|
None,
|
||||||
|
{'inputs': test_input, 'masks': test_mask}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✓ ONNX推理成功")
|
||||||
|
print(f" 输出数量: {len(outputs)}")
|
||||||
|
for i, output in enumerate(outputs):
|
||||||
|
print(f" 输出{i}形状: {output.shape}")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ 未安装onnx或onnxruntime,跳过验证")
|
||||||
|
print(" 安装命令: pip install onnx onnxruntime")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ ONNX验证失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
print(" 但模型文件已保存,可以尝试手动验证")
|
||||||
|
|
||||||
|
# ========== 总结 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ TimesFM 2.5模型导出完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
if os.path.exists(onnx_path):
|
||||||
|
print(f"\n保存位置: {onnx_path}")
|
||||||
|
print(f"文件大小: {os.path.getsize(onnx_path) / (1024**2):.2f} MB")
|
||||||
|
|
||||||
|
print(f"\n使用方法:")
|
||||||
|
print(f"```python")
|
||||||
|
print(f"import onnxruntime as ort")
|
||||||
|
print(f"import numpy as np")
|
||||||
|
print(f"")
|
||||||
|
print(f"# 加载模型")
|
||||||
|
print(f"session = ort.InferenceSession('{onnx_path}')")
|
||||||
|
print(f"")
|
||||||
|
print(f"# 准备输入(512小时BTC历史价格)")
|
||||||
|
print(f"btc_prices = np.array([85000, 85100, ...]) # 512个价格点")
|
||||||
|
print(f"inputs = btc_prices.reshape(1, -1).astype(np.float32)")
|
||||||
|
print(f"masks = np.ones((1, 512), dtype=np.float32)")
|
||||||
|
print(f"")
|
||||||
|
print(f"# 推理")
|
||||||
|
print(f"outputs = session.run(None, {{'inputs': inputs, 'masks': masks}})")
|
||||||
|
print(f"predictions = outputs[0] # 未来24小时价格预测")
|
||||||
|
print(f"```")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
export_timesfm_to_onnx()
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 安装三个时间序列基础模型
|
||||||
|
|
||||||
|
echo "=================================="
|
||||||
|
echo "安装时间序列基础模型"
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# 激活conda环境
|
||||||
|
source /Users/xxz/miniforge3/bin/activate
|
||||||
|
conda activate crypto_pred_312
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[1/3] 安装 Google TimesFM 2.5..."
|
||||||
|
pip install git+https://github.com/google-research/timesfm.git
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[2/3] 安装 IBM TTM-R2 (Granite TSFM)..."
|
||||||
|
pip install tsfm_public
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[3/3] 安装 Lag-Llama 依赖..."
|
||||||
|
pip install gluonts[torch] torch
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=================================="
|
||||||
|
echo "✓ 所有模型安装完成!"
|
||||||
|
echo "=================================="
|
||||||
|
echo ""
|
||||||
|
echo "模型位置:"
|
||||||
|
echo " - TimesFM 2.5: HuggingFace自动下载"
|
||||||
|
echo " - TTM-R2: ibm-granite/granite-timeseries-ttm-r2"
|
||||||
|
echo " - Lag-Llama: time-series-foundation-models/lag-llama"
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"price_mae": 434.07877457530066,
|
||||||
|
"price_rmse": 633.7116744507372,
|
||||||
|
"price_mape": 0.4112793819634811,
|
||||||
|
"direction_accuracy": 0.5083031112807788,
|
||||||
|
"best_val_loss": 0.00040675073481596463
|
||||||
|
}
|
||||||
Binary file not shown.
Whitespace-only changes.
@@ -0,0 +1,265 @@
|
|||||||
|
"""N-BEATS模型实现:预测未来1小时BTC价格"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class NBeatsBlock(nn.Module):
|
||||||
|
"""N-BEATS基础块"""
|
||||||
|
|
||||||
|
def __init__(self, input_size, theta_size, basis_function, layers, layer_size):
|
||||||
|
super().__init__()
|
||||||
|
self.input_size = input_size
|
||||||
|
self.theta_size = theta_size
|
||||||
|
self.basis_function = basis_function
|
||||||
|
|
||||||
|
# 全连接层栈
|
||||||
|
self.fc_stack = nn.ModuleList([
|
||||||
|
nn.Linear(input_size if i == 0 else layer_size, layer_size)
|
||||||
|
for i in range(layers)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Theta层(用于生成基函数参数)
|
||||||
|
self.theta_b_fc = nn.Linear(layer_size, theta_size, bias=False) # backcast
|
||||||
|
self.theta_f_fc = nn.Linear(layer_size, theta_size, bias=False) # forecast
|
||||||
|
|
||||||
|
self.relu = nn.ReLU()
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
x: (batch_size, input_size)
|
||||||
|
Returns:
|
||||||
|
backcast: (batch_size, input_size)
|
||||||
|
forecast: (batch_size, forecast_size)
|
||||||
|
"""
|
||||||
|
# 通过FC层栈
|
||||||
|
for fc in self.fc_stack:
|
||||||
|
x = self.relu(fc(x))
|
||||||
|
|
||||||
|
# 生成theta参数
|
||||||
|
theta_b = self.theta_b_fc(x) # backcast coefficients
|
||||||
|
theta_f = self.theta_f_fc(x) # forecast coefficients
|
||||||
|
|
||||||
|
# 应用基函数
|
||||||
|
backcast = self.basis_function(theta_b, is_forecast=False)
|
||||||
|
forecast = self.basis_function(theta_f, is_forecast=True)
|
||||||
|
|
||||||
|
return backcast, forecast
|
||||||
|
|
||||||
|
|
||||||
|
class GenericBasis(nn.Module):
|
||||||
|
"""通用基函数"""
|
||||||
|
|
||||||
|
def __init__(self, backcast_size, forecast_size):
|
||||||
|
super().__init__()
|
||||||
|
self.backcast_size = backcast_size
|
||||||
|
self.forecast_size = forecast_size
|
||||||
|
|
||||||
|
def forward(self, theta, is_forecast):
|
||||||
|
if is_forecast:
|
||||||
|
return theta[:, :self.forecast_size]
|
||||||
|
else:
|
||||||
|
return theta[:, :self.backcast_size]
|
||||||
|
|
||||||
|
|
||||||
|
class TrendBasis(nn.Module):
|
||||||
|
"""趋势基函数(多项式)"""
|
||||||
|
|
||||||
|
def __init__(self, degree, backcast_size, forecast_size):
|
||||||
|
super().__init__()
|
||||||
|
self.backcast_size = backcast_size
|
||||||
|
self.forecast_size = forecast_size
|
||||||
|
|
||||||
|
# 多项式时间点
|
||||||
|
backcast_time = torch.arange(backcast_size, dtype=torch.float32) / backcast_size
|
||||||
|
forecast_time = torch.arange(forecast_size, dtype=torch.float32) / forecast_size
|
||||||
|
|
||||||
|
# 构建多项式基
|
||||||
|
self.backcast_basis = torch.stack([backcast_time ** i for i in range(degree)], dim=1)
|
||||||
|
self.forecast_basis = torch.stack([forecast_time ** i for i in range(degree)], dim=1)
|
||||||
|
|
||||||
|
def forward(self, theta, is_forecast):
|
||||||
|
basis = self.forecast_basis if is_forecast else self.backcast_basis
|
||||||
|
basis = basis.to(theta.device)
|
||||||
|
return torch.matmul(theta, basis.T)
|
||||||
|
|
||||||
|
|
||||||
|
class SeasonalityBasis(nn.Module):
|
||||||
|
"""季节性基函数(傅里叶级数)"""
|
||||||
|
|
||||||
|
def __init__(self, harmonics, backcast_size, forecast_size):
|
||||||
|
super().__init__()
|
||||||
|
self.backcast_size = backcast_size
|
||||||
|
self.forecast_size = forecast_size
|
||||||
|
|
||||||
|
backcast_time = 2 * np.pi * torch.arange(backcast_size, dtype=torch.float32) / backcast_size
|
||||||
|
forecast_time = 2 * np.pi * torch.arange(forecast_size, dtype=torch.float32) / forecast_size
|
||||||
|
|
||||||
|
# 构建傅里叶基(sin和cos)
|
||||||
|
backcast_basis = []
|
||||||
|
forecast_basis = []
|
||||||
|
for i in range(1, harmonics + 1):
|
||||||
|
backcast_basis.extend([torch.cos(i * backcast_time), torch.sin(i * backcast_time)])
|
||||||
|
forecast_basis.extend([torch.cos(i * forecast_time), torch.sin(i * forecast_time)])
|
||||||
|
|
||||||
|
self.backcast_basis = torch.stack(backcast_basis, dim=1)
|
||||||
|
self.forecast_basis = torch.stack(forecast_basis, dim=1)
|
||||||
|
|
||||||
|
def forward(self, theta, is_forecast):
|
||||||
|
basis = self.forecast_basis if is_forecast else self.backcast_basis
|
||||||
|
basis = basis.to(theta.device)
|
||||||
|
return torch.matmul(theta, basis.T)
|
||||||
|
|
||||||
|
|
||||||
|
class NBeatsStack(nn.Module):
|
||||||
|
"""N-BEATS栈(多个块的组合)"""
|
||||||
|
|
||||||
|
def __init__(self, input_size, forecast_size, num_blocks, block_layers,
|
||||||
|
layer_size, theta_size, basis_function):
|
||||||
|
super().__init__()
|
||||||
|
self.blocks = nn.ModuleList([
|
||||||
|
NBeatsBlock(
|
||||||
|
input_size=input_size,
|
||||||
|
theta_size=theta_size,
|
||||||
|
basis_function=basis_function,
|
||||||
|
layers=block_layers,
|
||||||
|
layer_size=layer_size
|
||||||
|
)
|
||||||
|
for _ in range(num_blocks)
|
||||||
|
])
|
||||||
|
self.forecast_size = forecast_size
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
x: (batch_size, input_size)
|
||||||
|
Returns:
|
||||||
|
forecast: (batch_size, forecast_size)
|
||||||
|
"""
|
||||||
|
residual = x
|
||||||
|
forecast = torch.zeros(x.size(0), self.forecast_size, device=x.device)
|
||||||
|
|
||||||
|
for block in self.blocks:
|
||||||
|
backcast, block_forecast = block(residual)
|
||||||
|
residual = residual - backcast
|
||||||
|
forecast = forecast + block_forecast
|
||||||
|
|
||||||
|
return forecast
|
||||||
|
|
||||||
|
|
||||||
|
class NBeatsConfig:
|
||||||
|
"""N-BEATS模型配置"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
input_size=168, # 输入序列长度
|
||||||
|
forecast_size=1, # 预测长度
|
||||||
|
stacks=['trend', 'seasonality', 'generic'], # 使用的栈类型
|
||||||
|
num_blocks_per_stack=3, # 每个栈的块数
|
||||||
|
block_layers=4, # 每个块的FC层数
|
||||||
|
layer_size=256, # FC层大小
|
||||||
|
trend_degree=2, # 趋势多项式阶数
|
||||||
|
seasonality_harmonics=4, # 季节性谐波数
|
||||||
|
):
|
||||||
|
self.input_size = input_size
|
||||||
|
self.forecast_size = forecast_size
|
||||||
|
self.stacks = stacks
|
||||||
|
self.num_blocks_per_stack = num_blocks_per_stack
|
||||||
|
self.block_layers = block_layers
|
||||||
|
self.layer_size = layer_size
|
||||||
|
self.trend_degree = trend_degree
|
||||||
|
self.seasonality_harmonics = seasonality_harmonics
|
||||||
|
|
||||||
|
|
||||||
|
class NBeats(nn.Module):
|
||||||
|
"""N-BEATS模型"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
super().__init__()
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
# 创建多个栈
|
||||||
|
self.stacks = nn.ModuleList()
|
||||||
|
|
||||||
|
for stack_type in config.stacks:
|
||||||
|
if stack_type == 'trend':
|
||||||
|
basis_function = TrendBasis(
|
||||||
|
degree=config.trend_degree,
|
||||||
|
backcast_size=config.input_size,
|
||||||
|
forecast_size=config.forecast_size
|
||||||
|
)
|
||||||
|
theta_size = config.trend_degree
|
||||||
|
|
||||||
|
elif stack_type == 'seasonality':
|
||||||
|
basis_function = SeasonalityBasis(
|
||||||
|
harmonics=config.seasonality_harmonics,
|
||||||
|
backcast_size=config.input_size,
|
||||||
|
forecast_size=config.forecast_size
|
||||||
|
)
|
||||||
|
theta_size = 2 * config.seasonality_harmonics
|
||||||
|
|
||||||
|
else: # generic
|
||||||
|
basis_function = GenericBasis(
|
||||||
|
backcast_size=config.input_size,
|
||||||
|
forecast_size=config.forecast_size
|
||||||
|
)
|
||||||
|
theta_size = max(config.input_size, config.forecast_size)
|
||||||
|
|
||||||
|
stack = NBeatsStack(
|
||||||
|
input_size=config.input_size,
|
||||||
|
forecast_size=config.forecast_size,
|
||||||
|
num_blocks=config.num_blocks_per_stack,
|
||||||
|
block_layers=config.block_layers,
|
||||||
|
layer_size=config.layer_size,
|
||||||
|
theta_size=theta_size,
|
||||||
|
basis_function=basis_function
|
||||||
|
)
|
||||||
|
self.stacks.append(stack)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
x: (batch_size, lookback, features) 或 (batch_size, lookback)
|
||||||
|
Returns:
|
||||||
|
forecast: (batch_size, forecast_size)
|
||||||
|
"""
|
||||||
|
# 如果输入是多特征,取最后一个特征(close price)
|
||||||
|
if len(x.shape) == 3:
|
||||||
|
x = x[:, :, -1] # (batch_size, lookback)
|
||||||
|
|
||||||
|
# 确保是2D
|
||||||
|
if len(x.shape) == 1:
|
||||||
|
x = x.unsqueeze(0)
|
||||||
|
|
||||||
|
# 通过所有栈
|
||||||
|
forecast = torch.zeros(x.size(0), self.config.forecast_size, device=x.device)
|
||||||
|
residual = x
|
||||||
|
|
||||||
|
for stack in self.stacks:
|
||||||
|
stack_forecast = stack(residual)
|
||||||
|
forecast = forecast + stack_forecast
|
||||||
|
# 更新残差(用于下一个栈)
|
||||||
|
# 注意:这里简化了,标准N-BEATS会从每个栈获取backcast
|
||||||
|
|
||||||
|
return forecast.squeeze(-1) # (batch_size,)
|
||||||
|
|
||||||
|
def count_parameters(self):
|
||||||
|
"""统计模型参数数量"""
|
||||||
|
return sum(p.numel() for p in self.parameters() if p.requires_grad)
|
||||||
|
|
||||||
|
def save(self, path):
|
||||||
|
"""保存模型"""
|
||||||
|
torch.save({
|
||||||
|
'model_state_dict': self.state_dict(),
|
||||||
|
'config': self.config
|
||||||
|
}, path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, path, device='cpu'):
|
||||||
|
"""加载模型"""
|
||||||
|
checkpoint = torch.load(path, map_location=device)
|
||||||
|
model = cls(checkpoint['config'])
|
||||||
|
model.load_state_dict(checkpoint['model_state_dict'])
|
||||||
|
return model
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# 加密货币价格预测模型测试报告
|
||||||
|
|
||||||
|
## 测试概况
|
||||||
|
|
||||||
|
- **测试期间**: 2026年1月1日 - 2026年1月30日
|
||||||
|
- **总天数**: 22天
|
||||||
|
- **总样本数**: 495个小时
|
||||||
|
- **生成时间**: 2026-01-31 16:47:08
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 单模型性能
|
||||||
|
|
||||||
|
### DLinear分类模型
|
||||||
|
- **准确率**: 53.33%
|
||||||
|
- **预测上涨比例**: 47.1%
|
||||||
|
- **预测下跌比例**: 52.9%
|
||||||
|
|
||||||
|
### N-BEATS回归模型
|
||||||
|
- **准确率**: 49.90%
|
||||||
|
- **预测上涨比例**: 73.5%
|
||||||
|
- **预测下跌比例**: 26.5%
|
||||||
|
|
||||||
|
### TTM-R2零样本模型
|
||||||
|
- **准确率**: 51.31%
|
||||||
|
- **预测上涨比例**: 23.6%
|
||||||
|
- **预测下跌比例**: 76.4%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 集成策略性能
|
||||||
|
|
||||||
|
### 双模型交集(DLinear + N-BEATS)
|
||||||
|
- **准确率**: 53.39%
|
||||||
|
- **覆盖率**: 47.68%
|
||||||
|
- **有效预测数**: 236次
|
||||||
|
|
||||||
|
### 三模型多数投票(DLinear + N-BEATS + TTM-R2)
|
||||||
|
- **准确率**: 52.93%
|
||||||
|
- **覆盖率**: 100.00%
|
||||||
|
- **有效预测数**: 495次
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DLinear概率分布分析
|
||||||
|
|
||||||
|
| 概率区间 | 样本数 | 占比 |
|
||||||
|
|---------|--------|------|
|
||||||
|
| 0.4-0.5 | 262 | 52.9% |
|
||||||
|
| 0.5-0.6 | 229 | 46.3% |
|
||||||
|
| 0.6-0.7 | 4 | 0.8% |
|
||||||
|
|
||||||
|
**平均概率**: 0.501
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳/最差交易日
|
||||||
|
|
||||||
|
### 双模型交集
|
||||||
|
- **最佳日**: 2026-01-11 (83.3%)
|
||||||
|
- **最差日**: 2026-01-23 (28.6%)
|
||||||
|
|
||||||
|
### 三模型投票
|
||||||
|
- **最佳日**: 2026-01-28 (80.0%)
|
||||||
|
- **最差日**: 2026-01-23 (29.2%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
1. **最佳单模型**: DLinear (53.33%)
|
||||||
|
2. **最佳集成策略**: 双模型交集 (53.39%)
|
||||||
|
3. **DLinear置信度**: 99.2%的预测集中在0.45-0.55区间,缺乏高置信度预测
|
||||||
|
|
||||||
|
## 数据文件
|
||||||
|
|
||||||
|
- 逐小时预测结果: `hourly_predictions_2026_01.csv`
|
||||||
|
- 每日统计数据: `daily_statistics_2026_01.csv`
|
||||||
|
- 总结报告: `summary_report_2026_01.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
生成时间: 2026-01-31 16:47:08
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
date,total_samples,two_model_valid,two_model_correct,three_model_correct,dlinear_correct,dlinear_accuracy,two_model_accuracy,three_model_accuracy,two_model_coverage
|
||||||
|
2026-01-07,5,3,2,3,4,80.0,66.66666666666666,60.0,60.0
|
||||||
|
2026-01-08,24,13,6,12,11,45.83333333333333,46.15384615384615,50.0,54.166666666666664
|
||||||
|
2026-01-09,24,12,6,15,13,54.166666666666664,50.0,62.5,50.0
|
||||||
|
2026-01-10,24,12,6,12,13,54.166666666666664,50.0,50.0,50.0
|
||||||
|
2026-01-11,24,6,5,12,11,45.83333333333333,83.33333333333334,50.0,25.0
|
||||||
|
2026-01-12,24,6,3,8,13,54.166666666666664,50.0,33.33333333333333,25.0
|
||||||
|
2026-01-13,24,12,6,12,12,50.0,50.0,50.0,50.0
|
||||||
|
2026-01-14,24,12,6,13,14,58.333333333333336,50.0,54.166666666666664,50.0
|
||||||
|
2026-01-15,24,14,8,12,14,58.333333333333336,57.14285714285714,50.0,58.333333333333336
|
||||||
|
2026-01-16,24,13,6,13,10,41.66666666666667,46.15384615384615,54.166666666666664,54.166666666666664
|
||||||
|
2026-01-17,24,8,5,13,11,45.83333333333333,62.5,54.166666666666664,33.33333333333333
|
||||||
|
2026-01-18,24,17,9,15,12,50.0,52.94117647058824,62.5,70.83333333333334
|
||||||
|
2026-01-19,24,17,12,17,16,66.66666666666666,70.58823529411765,70.83333333333334,70.83333333333334
|
||||||
|
2026-01-20,24,6,2,9,11,45.83333333333333,33.33333333333333,37.5,25.0
|
||||||
|
2026-01-21,24,9,6,15,15,62.5,66.66666666666666,62.5,37.5
|
||||||
|
2026-01-22,24,10,6,13,14,58.333333333333336,60.0,54.166666666666664,41.66666666666667
|
||||||
|
2026-01-23,24,14,4,7,7,29.166666666666668,28.57142857142857,29.166666666666668,58.333333333333336
|
||||||
|
2026-01-24,24,14,7,13,14,58.333333333333336,50.0,54.166666666666664,58.333333333333336
|
||||||
|
2026-01-25,24,4,0,11,10,41.66666666666667,0.0,45.83333333333333,16.666666666666664
|
||||||
|
2026-01-26,24,13,8,13,15,62.5,61.53846153846154,54.166666666666664,54.166666666666664
|
||||||
|
2026-01-27,24,13,7,16,16,66.66666666666666,53.84615384615385,66.66666666666666,54.166666666666664
|
||||||
|
2026-01-28,10,8,6,8,8,80.0,75.0,80.0,80.0
|
||||||
|
@@ -0,0 +1,496 @@
|
|||||||
|
timestamp_utc,timestamp_et,date,true_label,dlinear_prob,dlinear_pred,nbeats_pred,ttm_pred,two_model_pred,three_model_pred,dlinear_correct,two_model_correct,three_model_correct
|
||||||
|
2026-01-08 00:00:00,2026-01-07 19:00:00-05:00,2026-01-07,1,0.57705873,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-08 01:00:00,2026-01-07 20:00:00-05:00,2026-01-07,0,0.49304762,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-08 02:00:00,2026-01-07 21:00:00-05:00,2026-01-07,0,0.47215617,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-08 03:00:00,2026-01-07 22:00:00-05:00,2026-01-07,1,0.46118113,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-08 04:00:00,2026-01-07 23:00:00-05:00,2026-01-07,0,0.45929378,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-08 05:00:00,2026-01-08 00:00:00-05:00,2026-01-08,1,0.47178885,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-08 06:00:00,2026-01-08 01:00:00-05:00,2026-01-08,1,0.5306643,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-08 07:00:00,2026-01-08 02:00:00-05:00,2026-01-08,0,0.4891925,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-08 08:00:00,2026-01-08 03:00:00-05:00,2026-01-08,0,0.5191637,1,0,1,-1,1,False,False,False
|
||||||
|
2026-01-08 09:00:00,2026-01-08 04:00:00-05:00,2026-01-08,0,0.47131988,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-08 10:00:00,2026-01-08 05:00:00-05:00,2026-01-08,0,0.47046715,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-08 11:00:00,2026-01-08 06:00:00-05:00,2026-01-08,1,0.5125343,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-08 12:00:00,2026-01-08 07:00:00-05:00,2026-01-08,1,0.5334059,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-08 13:00:00,2026-01-08 08:00:00-05:00,2026-01-08,1,0.49018916,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-08 14:00:00,2026-01-08 09:00:00-05:00,2026-01-08,0,0.50079376,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-08 15:00:00,2026-01-08 10:00:00-05:00,2026-01-08,1,0.46582156,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-08 16:00:00,2026-01-08 11:00:00-05:00,2026-01-08,1,0.46240616,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-08 17:00:00,2026-01-08 12:00:00-05:00,2026-01-08,0,0.48750654,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-08 18:00:00,2026-01-08 13:00:00-05:00,2026-01-08,0,0.47014055,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-08 19:00:00,2026-01-08 14:00:00-05:00,2026-01-08,1,0.49040872,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-08 20:00:00,2026-01-08 15:00:00-05:00,2026-01-08,0,0.5273022,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-08 21:00:00,2026-01-08 16:00:00-05:00,2026-01-08,1,0.5106931,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-08 22:00:00,2026-01-08 17:00:00-05:00,2026-01-08,0,0.5778609,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-08 23:00:00,2026-01-08 18:00:00-05:00,2026-01-08,1,0.54889923,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-09 00:00:00,2026-01-08 19:00:00-05:00,2026-01-08,1,0.51507455,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-09 01:00:00,2026-01-08 20:00:00-05:00,2026-01-08,0,0.5170575,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-09 02:00:00,2026-01-08 21:00:00-05:00,2026-01-08,1,0.4717993,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-09 03:00:00,2026-01-08 22:00:00-05:00,2026-01-08,0,0.5043716,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-09 04:00:00,2026-01-08 23:00:00-05:00,2026-01-08,0,0.51360196,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-09 05:00:00,2026-01-09 00:00:00-05:00,2026-01-09,1,0.469029,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-09 06:00:00,2026-01-09 01:00:00-05:00,2026-01-09,1,0.50669587,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-09 07:00:00,2026-01-09 02:00:00-05:00,2026-01-09,0,0.5564795,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-09 08:00:00,2026-01-09 03:00:00-05:00,2026-01-09,1,0.5820567,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-09 09:00:00,2026-01-09 04:00:00-05:00,2026-01-09,1,0.5504754,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-09 10:00:00,2026-01-09 05:00:00-05:00,2026-01-09,0,0.53064233,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-09 11:00:00,2026-01-09 06:00:00-05:00,2026-01-09,0,0.50407845,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-09 12:00:00,2026-01-09 07:00:00-05:00,2026-01-09,0,0.4576875,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-09 13:00:00,2026-01-09 08:00:00-05:00,2026-01-09,1,0.5189501,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-09 14:00:00,2026-01-09 09:00:00-05:00,2026-01-09,1,0.50425255,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-09 15:00:00,2026-01-09 10:00:00-05:00,2026-01-09,0,0.45628017,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-09 16:00:00,2026-01-09 11:00:00-05:00,2026-01-09,1,0.44883052,0,0,1,0,0,False,False,False
|
||||||
|
2026-01-09 17:00:00,2026-01-09 12:00:00-05:00,2026-01-09,1,0.46907404,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-09 18:00:00,2026-01-09 13:00:00-05:00,2026-01-09,1,0.48777947,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-09 19:00:00,2026-01-09 14:00:00-05:00,2026-01-09,0,0.49411413,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-09 20:00:00,2026-01-09 15:00:00-05:00,2026-01-09,0,0.47928023,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-09 21:00:00,2026-01-09 16:00:00-05:00,2026-01-09,0,0.49988702,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-09 22:00:00,2026-01-09 17:00:00-05:00,2026-01-09,1,0.4896841,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-09 23:00:00,2026-01-09 18:00:00-05:00,2026-01-09,0,0.49358237,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-10 00:00:00,2026-01-09 19:00:00-05:00,2026-01-09,0,0.50367767,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-10 01:00:00,2026-01-09 20:00:00-05:00,2026-01-09,1,0.52877015,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-10 02:00:00,2026-01-09 21:00:00-05:00,2026-01-09,1,0.51958245,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-10 03:00:00,2026-01-09 22:00:00-05:00,2026-01-09,0,0.53057724,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-10 04:00:00,2026-01-09 23:00:00-05:00,2026-01-09,1,0.45753565,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-10 05:00:00,2026-01-10 00:00:00-05:00,2026-01-10,0,0.447833,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-10 06:00:00,2026-01-10 01:00:00-05:00,2026-01-10,1,0.4577814,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-10 07:00:00,2026-01-10 02:00:00-05:00,2026-01-10,0,0.47578683,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-10 08:00:00,2026-01-10 03:00:00-05:00,2026-01-10,0,0.4506602,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-10 09:00:00,2026-01-10 04:00:00-05:00,2026-01-10,0,0.5478148,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-10 10:00:00,2026-01-10 05:00:00-05:00,2026-01-10,1,0.5990233,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-10 11:00:00,2026-01-10 06:00:00-05:00,2026-01-10,1,0.5706127,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-10 12:00:00,2026-01-10 07:00:00-05:00,2026-01-10,0,0.5276616,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-10 13:00:00,2026-01-10 08:00:00-05:00,2026-01-10,1,0.51404405,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-10 14:00:00,2026-01-10 09:00:00-05:00,2026-01-10,1,0.50078404,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-10 15:00:00,2026-01-10 10:00:00-05:00,2026-01-10,1,0.45550296,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-10 16:00:00,2026-01-10 11:00:00-05:00,2026-01-10,1,0.44787562,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-10 17:00:00,2026-01-10 12:00:00-05:00,2026-01-10,0,0.44727677,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-10 18:00:00,2026-01-10 13:00:00-05:00,2026-01-10,1,0.44855183,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-10 19:00:00,2026-01-10 14:00:00-05:00,2026-01-10,0,0.45485023,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-10 20:00:00,2026-01-10 15:00:00-05:00,2026-01-10,0,0.47032186,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-10 21:00:00,2026-01-10 16:00:00-05:00,2026-01-10,0,0.48025206,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-10 22:00:00,2026-01-10 17:00:00-05:00,2026-01-10,0,0.5247036,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-10 23:00:00,2026-01-10 18:00:00-05:00,2026-01-10,0,0.58018094,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-11 00:00:00,2026-01-10 19:00:00-05:00,2026-01-10,0,0.59668726,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-11 01:00:00,2026-01-10 20:00:00-05:00,2026-01-10,1,0.5909982,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-11 02:00:00,2026-01-10 21:00:00-05:00,2026-01-10,1,0.5297047,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-11 03:00:00,2026-01-10 22:00:00-05:00,2026-01-10,0,0.5227216,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-11 04:00:00,2026-01-10 23:00:00-05:00,2026-01-10,1,0.4885675,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-11 05:00:00,2026-01-11 00:00:00-05:00,2026-01-11,1,0.47107595,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-11 06:00:00,2026-01-11 01:00:00-05:00,2026-01-11,1,0.4610141,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-11 07:00:00,2026-01-11 02:00:00-05:00,2026-01-11,0,0.4598382,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-11 08:00:00,2026-01-11 03:00:00-05:00,2026-01-11,1,0.48207,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-11 09:00:00,2026-01-11 04:00:00-05:00,2026-01-11,1,0.513094,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-11 10:00:00,2026-01-11 05:00:00-05:00,2026-01-11,0,0.47068593,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-11 11:00:00,2026-01-11 06:00:00-05:00,2026-01-11,0,0.4895874,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-11 12:00:00,2026-01-11 07:00:00-05:00,2026-01-11,1,0.46443766,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-11 13:00:00,2026-01-11 08:00:00-05:00,2026-01-11,1,0.4845122,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-11 14:00:00,2026-01-11 09:00:00-05:00,2026-01-11,1,0.50560534,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-11 15:00:00,2026-01-11 10:00:00-05:00,2026-01-11,0,0.47123846,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-11 16:00:00,2026-01-11 11:00:00-05:00,2026-01-11,0,0.45919827,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-11 17:00:00,2026-01-11 12:00:00-05:00,2026-01-11,1,0.52895916,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-11 18:00:00,2026-01-11 13:00:00-05:00,2026-01-11,1,0.5049388,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-11 19:00:00,2026-01-11 14:00:00-05:00,2026-01-11,1,0.48020154,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-11 20:00:00,2026-01-11 15:00:00-05:00,2026-01-11,1,0.49915588,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-11 21:00:00,2026-01-11 16:00:00-05:00,2026-01-11,0,0.5144025,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-11 22:00:00,2026-01-11 17:00:00-05:00,2026-01-11,1,0.49443942,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-11 23:00:00,2026-01-11 18:00:00-05:00,2026-01-11,1,0.4918472,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-12 00:00:00,2026-01-11 19:00:00-05:00,2026-01-11,0,0.4723591,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-12 01:00:00,2026-01-11 20:00:00-05:00,2026-01-11,0,0.45115232,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-12 02:00:00,2026-01-11 21:00:00-05:00,2026-01-11,1,0.47599584,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-12 03:00:00,2026-01-11 22:00:00-05:00,2026-01-11,0,0.51635396,1,0,1,-1,1,False,False,False
|
||||||
|
2026-01-12 04:00:00,2026-01-11 23:00:00-05:00,2026-01-11,1,0.47296882,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-12 05:00:00,2026-01-12 00:00:00-05:00,2026-01-12,1,0.48586744,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-12 06:00:00,2026-01-12 01:00:00-05:00,2026-01-12,0,0.47855884,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-12 07:00:00,2026-01-12 02:00:00-05:00,2026-01-12,0,0.4667405,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-12 08:00:00,2026-01-12 03:00:00-05:00,2026-01-12,1,0.47844118,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-12 09:00:00,2026-01-12 04:00:00-05:00,2026-01-12,1,0.49896824,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-12 10:00:00,2026-01-12 05:00:00-05:00,2026-01-12,1,0.4889443,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-12 11:00:00,2026-01-12 06:00:00-05:00,2026-01-12,0,0.45522135,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-12 12:00:00,2026-01-12 07:00:00-05:00,2026-01-12,1,0.45169547,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-12 13:00:00,2026-01-12 08:00:00-05:00,2026-01-12,0,0.48637372,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-12 14:00:00,2026-01-12 09:00:00-05:00,2026-01-12,0,0.45615003,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-12 15:00:00,2026-01-12 10:00:00-05:00,2026-01-12,1,0.45153883,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-12 16:00:00,2026-01-12 11:00:00-05:00,2026-01-12,1,0.47393757,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-12 17:00:00,2026-01-12 12:00:00-05:00,2026-01-12,1,0.47199413,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-12 18:00:00,2026-01-12 13:00:00-05:00,2026-01-12,0,0.43410957,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-12 19:00:00,2026-01-12 14:00:00-05:00,2026-01-12,0,0.44708502,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-12 20:00:00,2026-01-12 15:00:00-05:00,2026-01-12,1,0.58962935,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-12 21:00:00,2026-01-12 16:00:00-05:00,2026-01-12,1,0.5799872,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-12 22:00:00,2026-01-12 17:00:00-05:00,2026-01-12,0,0.5379354,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-12 23:00:00,2026-01-12 18:00:00-05:00,2026-01-12,1,0.52044976,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-13 00:00:00,2026-01-12 19:00:00-05:00,2026-01-12,1,0.546228,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-13 01:00:00,2026-01-12 20:00:00-05:00,2026-01-12,0,0.50185496,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-13 02:00:00,2026-01-12 21:00:00-05:00,2026-01-12,1,0.5131573,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-13 03:00:00,2026-01-12 22:00:00-05:00,2026-01-12,0,0.49922633,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-13 04:00:00,2026-01-12 23:00:00-05:00,2026-01-12,1,0.47469753,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-13 05:00:00,2026-01-13 00:00:00-05:00,2026-01-13,1,0.45794666,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-13 06:00:00,2026-01-13 01:00:00-05:00,2026-01-13,1,0.45379728,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-13 07:00:00,2026-01-13 02:00:00-05:00,2026-01-13,0,0.45295683,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-13 08:00:00,2026-01-13 03:00:00-05:00,2026-01-13,0,0.44573298,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-13 09:00:00,2026-01-13 04:00:00-05:00,2026-01-13,1,0.4709209,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-13 10:00:00,2026-01-13 05:00:00-05:00,2026-01-13,1,0.54900587,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-13 11:00:00,2026-01-13 06:00:00-05:00,2026-01-13,0,0.54181457,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-13 12:00:00,2026-01-13 07:00:00-05:00,2026-01-13,0,0.47640413,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-13 13:00:00,2026-01-13 08:00:00-05:00,2026-01-13,1,0.5774929,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-13 14:00:00,2026-01-13 09:00:00-05:00,2026-01-13,0,0.55843365,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-13 15:00:00,2026-01-13 10:00:00-05:00,2026-01-13,0,0.53291476,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-13 16:00:00,2026-01-13 11:00:00-05:00,2026-01-13,0,0.5417355,1,0,1,-1,1,False,False,False
|
||||||
|
2026-01-13 17:00:00,2026-01-13 12:00:00-05:00,2026-01-13,0,0.526461,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-13 18:00:00,2026-01-13 13:00:00-05:00,2026-01-13,1,0.5094685,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-13 19:00:00,2026-01-13 14:00:00-05:00,2026-01-13,1,0.50502,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-13 20:00:00,2026-01-13 15:00:00-05:00,2026-01-13,0,0.5074931,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-13 21:00:00,2026-01-13 16:00:00-05:00,2026-01-13,1,0.50496596,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-13 22:00:00,2026-01-13 17:00:00-05:00,2026-01-13,1,0.5311341,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-13 23:00:00,2026-01-13 18:00:00-05:00,2026-01-13,1,0.5194955,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-14 00:00:00,2026-01-13 19:00:00-05:00,2026-01-13,0,0.51150465,1,0,1,-1,1,False,False,False
|
||||||
|
2026-01-14 01:00:00,2026-01-13 20:00:00-05:00,2026-01-13,0,0.53702235,1,0,1,-1,1,False,False,False
|
||||||
|
2026-01-14 02:00:00,2026-01-13 21:00:00-05:00,2026-01-13,1,0.4907134,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-14 03:00:00,2026-01-13 22:00:00-05:00,2026-01-13,0,0.4840499,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-14 04:00:00,2026-01-13 23:00:00-05:00,2026-01-13,0,0.4611074,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-14 05:00:00,2026-01-14 00:00:00-05:00,2026-01-14,1,0.45327646,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-14 06:00:00,2026-01-14 01:00:00-05:00,2026-01-14,1,0.49665686,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-14 07:00:00,2026-01-14 02:00:00-05:00,2026-01-14,0,0.4882262,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-14 08:00:00,2026-01-14 03:00:00-05:00,2026-01-14,0,0.46823135,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-14 09:00:00,2026-01-14 04:00:00-05:00,2026-01-14,0,0.47908562,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-14 10:00:00,2026-01-14 05:00:00-05:00,2026-01-14,0,0.55422133,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-14 11:00:00,2026-01-14 06:00:00-05:00,2026-01-14,1,0.610168,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-14 12:00:00,2026-01-14 07:00:00-05:00,2026-01-14,0,0.5614474,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-14 13:00:00,2026-01-14 08:00:00-05:00,2026-01-14,1,0.56697947,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-14 14:00:00,2026-01-14 09:00:00-05:00,2026-01-14,1,0.52636105,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-14 15:00:00,2026-01-14 10:00:00-05:00,2026-01-14,1,0.50316924,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-14 16:00:00,2026-01-14 11:00:00-05:00,2026-01-14,0,0.5363478,1,0,1,-1,1,False,False,False
|
||||||
|
2026-01-14 17:00:00,2026-01-14 12:00:00-05:00,2026-01-14,0,0.47355476,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-14 18:00:00,2026-01-14 13:00:00-05:00,2026-01-14,0,0.48269007,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-14 19:00:00,2026-01-14 14:00:00-05:00,2026-01-14,1,0.50739765,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-14 20:00:00,2026-01-14 15:00:00-05:00,2026-01-14,1,0.501837,1,0,1,-1,1,True,False,True
|
||||||
|
2026-01-14 21:00:00,2026-01-14 16:00:00-05:00,2026-01-14,0,0.4608701,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-14 22:00:00,2026-01-14 17:00:00-05:00,2026-01-14,1,0.4631222,0,0,1,0,0,False,False,False
|
||||||
|
2026-01-14 23:00:00,2026-01-14 18:00:00-05:00,2026-01-14,0,0.5486243,1,0,1,-1,1,False,False,False
|
||||||
|
2026-01-15 00:00:00,2026-01-14 19:00:00-05:00,2026-01-14,0,0.55234075,1,0,1,-1,1,False,False,False
|
||||||
|
2026-01-15 01:00:00,2026-01-14 20:00:00-05:00,2026-01-14,1,0.4942968,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-15 02:00:00,2026-01-14 21:00:00-05:00,2026-01-14,1,0.5293442,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-15 03:00:00,2026-01-14 22:00:00-05:00,2026-01-14,0,0.5131931,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-15 04:00:00,2026-01-14 23:00:00-05:00,2026-01-14,0,0.48758185,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-15 05:00:00,2026-01-15 00:00:00-05:00,2026-01-15,0,0.4538339,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-15 06:00:00,2026-01-15 01:00:00-05:00,2026-01-15,1,0.48841763,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-15 07:00:00,2026-01-15 02:00:00-05:00,2026-01-15,1,0.5323837,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-15 08:00:00,2026-01-15 03:00:00-05:00,2026-01-15,0,0.5621298,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-15 09:00:00,2026-01-15 04:00:00-05:00,2026-01-15,1,0.46453536,0,1,1,-1,1,False,False,True
|
||||||
|
2026-01-15 10:00:00,2026-01-15 05:00:00-05:00,2026-01-15,1,0.5253816,1,1,1,1,1,True,True,True
|
||||||
|
2026-01-15 11:00:00,2026-01-15 06:00:00-05:00,2026-01-15,0,0.5148865,1,1,1,1,1,False,False,False
|
||||||
|
2026-01-15 12:00:00,2026-01-15 07:00:00-05:00,2026-01-15,0,0.4899683,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-15 13:00:00,2026-01-15 08:00:00-05:00,2026-01-15,1,0.4946566,0,0,1,0,0,False,False,False
|
||||||
|
2026-01-15 14:00:00,2026-01-15 09:00:00-05:00,2026-01-15,0,0.4987126,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-15 15:00:00,2026-01-15 10:00:00-05:00,2026-01-15,0,0.48068026,0,1,1,-1,1,True,False,False
|
||||||
|
2026-01-15 16:00:00,2026-01-15 11:00:00-05:00,2026-01-15,0,0.48770115,0,0,1,0,0,True,True,True
|
||||||
|
2026-01-15 17:00:00,2026-01-15 12:00:00-05:00,2026-01-15,1,0.4844149,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-15 18:00:00,2026-01-15 13:00:00-05:00,2026-01-15,0,0.4631365,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-15 19:00:00,2026-01-15 14:00:00-05:00,2026-01-15,1,0.45610657,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-15 20:00:00,2026-01-15 15:00:00-05:00,2026-01-15,0,0.4881056,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-15 21:00:00,2026-01-15 16:00:00-05:00,2026-01-15,0,0.46482596,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-15 22:00:00,2026-01-15 17:00:00-05:00,2026-01-15,1,0.51136947,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-15 23:00:00,2026-01-15 18:00:00-05:00,2026-01-15,1,0.55429316,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-16 00:00:00,2026-01-15 19:00:00-05:00,2026-01-15,0,0.5584868,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-16 01:00:00,2026-01-15 20:00:00-05:00,2026-01-15,1,0.51578057,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-16 02:00:00,2026-01-15 21:00:00-05:00,2026-01-15,0,0.5562782,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-16 03:00:00,2026-01-15 22:00:00-05:00,2026-01-15,1,0.5080268,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-16 04:00:00,2026-01-15 23:00:00-05:00,2026-01-15,1,0.49140474,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-16 05:00:00,2026-01-16 00:00:00-05:00,2026-01-16,1,0.46221796,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-16 06:00:00,2026-01-16 01:00:00-05:00,2026-01-16,0,0.4448804,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-16 07:00:00,2026-01-16 02:00:00-05:00,2026-01-16,1,0.4457024,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-16 08:00:00,2026-01-16 03:00:00-05:00,2026-01-16,1,0.49323055,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-16 09:00:00,2026-01-16 04:00:00-05:00,2026-01-16,0,0.49757826,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-16 10:00:00,2026-01-16 05:00:00-05:00,2026-01-16,1,0.5067139,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-16 11:00:00,2026-01-16 06:00:00-05:00,2026-01-16,0,0.5102587,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-16 12:00:00,2026-01-16 07:00:00-05:00,2026-01-16,0,0.5843353,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-16 13:00:00,2026-01-16 08:00:00-05:00,2026-01-16,0,0.5425463,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-16 14:00:00,2026-01-16 09:00:00-05:00,2026-01-16,0,0.5690049,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-16 15:00:00,2026-01-16 10:00:00-05:00,2026-01-16,0,0.54829246,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-16 16:00:00,2026-01-16 11:00:00-05:00,2026-01-16,1,0.5046336,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-16 17:00:00,2026-01-16 12:00:00-05:00,2026-01-16,1,0.5086898,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-16 18:00:00,2026-01-16 13:00:00-05:00,2026-01-16,0,0.4823461,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-16 19:00:00,2026-01-16 14:00:00-05:00,2026-01-16,1,0.4856351,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-16 20:00:00,2026-01-16 15:00:00-05:00,2026-01-16,0,0.4558552,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-16 21:00:00,2026-01-16 16:00:00-05:00,2026-01-16,0,0.4780688,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-16 22:00:00,2026-01-16 17:00:00-05:00,2026-01-16,1,0.48367676,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-16 23:00:00,2026-01-16 18:00:00-05:00,2026-01-16,1,0.5118516,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-17 00:00:00,2026-01-16 19:00:00-05:00,2026-01-16,0,0.5440252,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-17 01:00:00,2026-01-16 20:00:00-05:00,2026-01-16,0,0.52165735,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-17 02:00:00,2026-01-16 21:00:00-05:00,2026-01-16,0,0.57716566,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-17 03:00:00,2026-01-16 22:00:00-05:00,2026-01-16,1,0.53289396,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-17 04:00:00,2026-01-16 23:00:00-05:00,2026-01-16,0,0.5077436,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-17 05:00:00,2026-01-17 00:00:00-05:00,2026-01-17,1,0.49432057,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-17 06:00:00,2026-01-17 01:00:00-05:00,2026-01-17,1,0.50985175,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-17 07:00:00,2026-01-17 02:00:00-05:00,2026-01-17,1,0.4898242,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-17 08:00:00,2026-01-17 03:00:00-05:00,2026-01-17,1,0.45491692,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-17 09:00:00,2026-01-17 04:00:00-05:00,2026-01-17,0,0.45771605,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-17 10:00:00,2026-01-17 05:00:00-05:00,2026-01-17,0,0.45357528,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-17 11:00:00,2026-01-17 06:00:00-05:00,2026-01-17,1,0.4550014,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-17 12:00:00,2026-01-17 07:00:00-05:00,2026-01-17,1,0.46508005,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-17 13:00:00,2026-01-17 08:00:00-05:00,2026-01-17,0,0.4753723,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-17 14:00:00,2026-01-17 09:00:00-05:00,2026-01-17,0,0.49003577,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-17 15:00:00,2026-01-17 10:00:00-05:00,2026-01-17,0,0.60734934,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-17 16:00:00,2026-01-17 11:00:00-05:00,2026-01-17,0,0.5786392,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-17 17:00:00,2026-01-17 12:00:00-05:00,2026-01-17,1,0.5754632,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-17 18:00:00,2026-01-17 13:00:00-05:00,2026-01-17,0,0.59382933,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-17 19:00:00,2026-01-17 14:00:00-05:00,2026-01-17,0,0.5593247,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-17 20:00:00,2026-01-17 15:00:00-05:00,2026-01-17,1,0.5365127,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-17 21:00:00,2026-01-17 16:00:00-05:00,2026-01-17,1,0.50084764,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-17 22:00:00,2026-01-17 17:00:00-05:00,2026-01-17,1,0.46421826,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-17 23:00:00,2026-01-17 18:00:00-05:00,2026-01-17,0,0.48463318,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-18 00:00:00,2026-01-17 19:00:00-05:00,2026-01-17,1,0.46844795,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-18 01:00:00,2026-01-17 20:00:00-05:00,2026-01-17,1,0.48266262,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-18 02:00:00,2026-01-17 21:00:00-05:00,2026-01-17,0,0.49029487,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-18 03:00:00,2026-01-17 22:00:00-05:00,2026-01-17,0,0.45225728,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-18 04:00:00,2026-01-17 23:00:00-05:00,2026-01-17,0,0.5144419,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-18 05:00:00,2026-01-18 00:00:00-05:00,2026-01-18,1,0.5160817,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-18 06:00:00,2026-01-18 01:00:00-05:00,2026-01-18,1,0.50346553,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-18 07:00:00,2026-01-18 02:00:00-05:00,2026-01-18,0,0.44028476,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-18 08:00:00,2026-01-18 03:00:00-05:00,2026-01-18,0,0.46246836,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-18 09:00:00,2026-01-18 04:00:00-05:00,2026-01-18,1,0.51622605,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-18 10:00:00,2026-01-18 05:00:00-05:00,2026-01-18,1,0.51112276,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-18 11:00:00,2026-01-18 06:00:00-05:00,2026-01-18,0,0.45434904,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-18 12:00:00,2026-01-18 07:00:00-05:00,2026-01-18,0,0.52511775,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-18 13:00:00,2026-01-18 08:00:00-05:00,2026-01-18,1,0.5778929,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-18 14:00:00,2026-01-18 09:00:00-05:00,2026-01-18,1,0.61074215,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-18 15:00:00,2026-01-18 10:00:00-05:00,2026-01-18,0,0.57336044,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-18 16:00:00,2026-01-18 11:00:00-05:00,2026-01-18,0,0.51572573,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-18 17:00:00,2026-01-18 12:00:00-05:00,2026-01-18,0,0.5084302,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-18 18:00:00,2026-01-18 13:00:00-05:00,2026-01-18,0,0.53620636,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-18 19:00:00,2026-01-18 14:00:00-05:00,2026-01-18,0,0.58746594,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-18 20:00:00,2026-01-18 15:00:00-05:00,2026-01-18,0,0.53558993,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-18 21:00:00,2026-01-18 16:00:00-05:00,2026-01-18,1,0.4962998,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-18 22:00:00,2026-01-18 17:00:00-05:00,2026-01-18,0,0.5750347,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-18 23:00:00,2026-01-18 18:00:00-05:00,2026-01-18,0,0.58212554,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-19 00:00:00,2026-01-18 19:00:00-05:00,2026-01-18,1,0.5023791,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-19 01:00:00,2026-01-18 20:00:00-05:00,2026-01-18,0,0.51338583,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-19 02:00:00,2026-01-18 21:00:00-05:00,2026-01-18,1,0.4790363,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-19 03:00:00,2026-01-18 22:00:00-05:00,2026-01-18,0,0.4659165,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-19 04:00:00,2026-01-18 23:00:00-05:00,2026-01-18,0,0.48015603,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-19 05:00:00,2026-01-19 00:00:00-05:00,2026-01-19,1,0.4719194,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-19 06:00:00,2026-01-19 01:00:00-05:00,2026-01-19,0,0.47719255,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-19 07:00:00,2026-01-19 02:00:00-05:00,2026-01-19,0,0.46883187,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-19 08:00:00,2026-01-19 03:00:00-05:00,2026-01-19,0,0.5041158,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-19 09:00:00,2026-01-19 04:00:00-05:00,2026-01-19,1,0.55327594,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-19 10:00:00,2026-01-19 05:00:00-05:00,2026-01-19,1,0.51431626,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-19 11:00:00,2026-01-19 06:00:00-05:00,2026-01-19,0,0.48845842,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-19 12:00:00,2026-01-19 07:00:00-05:00,2026-01-19,0,0.548243,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-19 13:00:00,2026-01-19 08:00:00-05:00,2026-01-19,0,0.56993884,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-19 14:00:00,2026-01-19 09:00:00-05:00,2026-01-19,1,0.570956,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-19 15:00:00,2026-01-19 10:00:00-05:00,2026-01-19,1,0.58591735,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-19 16:00:00,2026-01-19 11:00:00-05:00,2026-01-19,1,0.5654058,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-19 17:00:00,2026-01-19 12:00:00-05:00,2026-01-19,1,0.53851956,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-19 18:00:00,2026-01-19 13:00:00-05:00,2026-01-19,1,0.51866657,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-19 19:00:00,2026-01-19 14:00:00-05:00,2026-01-19,1,0.4805421,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-19 20:00:00,2026-01-19 15:00:00-05:00,2026-01-19,0,0.4309969,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-19 21:00:00,2026-01-19 16:00:00-05:00,2026-01-19,0,0.40472427,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-19 22:00:00,2026-01-19 17:00:00-05:00,2026-01-19,0,0.43165132,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-19 23:00:00,2026-01-19 18:00:00-05:00,2026-01-19,0,0.45249504,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-20 00:00:00,2026-01-19 19:00:00-05:00,2026-01-19,1,0.41881704,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-20 01:00:00,2026-01-19 20:00:00-05:00,2026-01-19,0,0.4950903,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-20 02:00:00,2026-01-19 21:00:00-05:00,2026-01-19,0,0.52289253,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-20 03:00:00,2026-01-19 22:00:00-05:00,2026-01-19,0,0.5152514,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-20 04:00:00,2026-01-19 23:00:00-05:00,2026-01-19,1,0.52619386,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-20 05:00:00,2026-01-20 00:00:00-05:00,2026-01-20,1,0.4920829,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-20 06:00:00,2026-01-20 01:00:00-05:00,2026-01-20,0,0.44386265,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-20 07:00:00,2026-01-20 02:00:00-05:00,2026-01-20,1,0.4548454,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-20 08:00:00,2026-01-20 03:00:00-05:00,2026-01-20,1,0.50409263,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-20 09:00:00,2026-01-20 04:00:00-05:00,2026-01-20,1,0.4486903,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-20 10:00:00,2026-01-20 05:00:00-05:00,2026-01-20,0,0.44015622,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-20 11:00:00,2026-01-20 06:00:00-05:00,2026-01-20,1,0.46409336,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-20 12:00:00,2026-01-20 07:00:00-05:00,2026-01-20,0,0.47176668,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-20 13:00:00,2026-01-20 08:00:00-05:00,2026-01-20,0,0.44548807,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-20 14:00:00,2026-01-20 09:00:00-05:00,2026-01-20,1,0.45053762,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-20 15:00:00,2026-01-20 10:00:00-05:00,2026-01-20,1,0.481055,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-20 16:00:00,2026-01-20 11:00:00-05:00,2026-01-20,0,0.5467605,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-20 17:00:00,2026-01-20 12:00:00-05:00,2026-01-20,0,0.5497743,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-20 18:00:00,2026-01-20 13:00:00-05:00,2026-01-20,1,0.531139,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-20 19:00:00,2026-01-20 14:00:00-05:00,2026-01-20,1,0.48030645,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-20 20:00:00,2026-01-20 15:00:00-05:00,2026-01-20,1,0.5283993,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-20 21:00:00,2026-01-20 16:00:00-05:00,2026-01-20,0,0.5042049,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-20 22:00:00,2026-01-20 17:00:00-05:00,2026-01-20,1,0.46221802,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-20 23:00:00,2026-01-20 18:00:00-05:00,2026-01-20,1,0.45490173,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-21 00:00:00,2026-01-20 19:00:00-05:00,2026-01-20,1,0.4481478,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-21 01:00:00,2026-01-20 20:00:00-05:00,2026-01-20,0,0.4967737,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-21 02:00:00,2026-01-20 21:00:00-05:00,2026-01-20,1,0.5263554,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-21 03:00:00,2026-01-20 22:00:00-05:00,2026-01-20,0,0.46894258,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-21 04:00:00,2026-01-20 23:00:00-05:00,2026-01-20,0,0.4519449,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-21 05:00:00,2026-01-21 00:00:00-05:00,2026-01-21,0,0.5209714,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-21 06:00:00,2026-01-21 01:00:00-05:00,2026-01-21,1,0.56909335,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-21 07:00:00,2026-01-21 02:00:00-05:00,2026-01-21,1,0.51435,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-21 08:00:00,2026-01-21 03:00:00-05:00,2026-01-21,0,0.47510275,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-21 09:00:00,2026-01-21 04:00:00-05:00,2026-01-21,1,0.47119588,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-21 10:00:00,2026-01-21 05:00:00-05:00,2026-01-21,0,0.45829907,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-21 11:00:00,2026-01-21 06:00:00-05:00,2026-01-21,0,0.46126115,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-21 12:00:00,2026-01-21 07:00:00-05:00,2026-01-21,1,0.49299902,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-21 13:00:00,2026-01-21 08:00:00-05:00,2026-01-21,1,0.4642857,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-21 14:00:00,2026-01-21 09:00:00-05:00,2026-01-21,0,0.46757126,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-21 15:00:00,2026-01-21 10:00:00-05:00,2026-01-21,1,0.51933724,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-21 16:00:00,2026-01-21 11:00:00-05:00,2026-01-21,0,0.52031684,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-21 17:00:00,2026-01-21 12:00:00-05:00,2026-01-21,1,0.4510923,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-21 18:00:00,2026-01-21 13:00:00-05:00,2026-01-21,1,0.47569612,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-21 19:00:00,2026-01-21 14:00:00-05:00,2026-01-21,0,0.52261287,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-21 20:00:00,2026-01-21 15:00:00-05:00,2026-01-21,0,0.5309036,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-21 21:00:00,2026-01-21 16:00:00-05:00,2026-01-21,1,0.5570105,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-21 22:00:00,2026-01-21 17:00:00-05:00,2026-01-21,0,0.49707052,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-21 23:00:00,2026-01-21 18:00:00-05:00,2026-01-21,0,0.49758816,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-22 00:00:00,2026-01-21 19:00:00-05:00,2026-01-21,1,0.53757817,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-22 01:00:00,2026-01-21 20:00:00-05:00,2026-01-21,1,0.57986003,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-22 02:00:00,2026-01-21 21:00:00-05:00,2026-01-21,1,0.53082293,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-22 03:00:00,2026-01-21 22:00:00-05:00,2026-01-21,1,0.50052387,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-22 04:00:00,2026-01-21 23:00:00-05:00,2026-01-21,0,0.47824442,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-22 05:00:00,2026-01-22 00:00:00-05:00,2026-01-22,1,0.4760107,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-22 06:00:00,2026-01-22 01:00:00-05:00,2026-01-22,1,0.50084823,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-22 07:00:00,2026-01-22 02:00:00-05:00,2026-01-22,1,0.45673624,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-22 08:00:00,2026-01-22 03:00:00-05:00,2026-01-22,0,0.44737244,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-22 09:00:00,2026-01-22 04:00:00-05:00,2026-01-22,0,0.4425855,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-22 10:00:00,2026-01-22 05:00:00-05:00,2026-01-22,0,0.41352358,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-22 11:00:00,2026-01-22 06:00:00-05:00,2026-01-22,0,0.47900316,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-22 12:00:00,2026-01-22 07:00:00-05:00,2026-01-22,1,0.5056432,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-22 13:00:00,2026-01-22 08:00:00-05:00,2026-01-22,0,0.5437085,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-22 14:00:00,2026-01-22 09:00:00-05:00,2026-01-22,0,0.57784945,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-22 15:00:00,2026-01-22 10:00:00-05:00,2026-01-22,1,0.55117744,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-22 16:00:00,2026-01-22 11:00:00-05:00,2026-01-22,1,0.5499753,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-22 17:00:00,2026-01-22 12:00:00-05:00,2026-01-22,1,0.48403728,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-22 18:00:00,2026-01-22 13:00:00-05:00,2026-01-22,1,0.5378152,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-22 19:00:00,2026-01-22 14:00:00-05:00,2026-01-22,0,0.5124629,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-22 20:00:00,2026-01-22 15:00:00-05:00,2026-01-22,0,0.4786928,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-22 21:00:00,2026-01-22 16:00:00-05:00,2026-01-22,0,0.47483838,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-22 22:00:00,2026-01-22 17:00:00-05:00,2026-01-22,1,0.4948368,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-22 23:00:00,2026-01-22 18:00:00-05:00,2026-01-22,0,0.50176257,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 00:00:00,2026-01-22 19:00:00-05:00,2026-01-22,1,0.49785018,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-23 01:00:00,2026-01-22 20:00:00-05:00,2026-01-22,0,0.4707971,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-23 02:00:00,2026-01-22 21:00:00-05:00,2026-01-22,0,0.4584682,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-23 03:00:00,2026-01-22 22:00:00-05:00,2026-01-22,0,0.47485125,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-23 04:00:00,2026-01-22 23:00:00-05:00,2026-01-22,1,0.48357454,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-23 05:00:00,2026-01-23 00:00:00-05:00,2026-01-23,0,0.5074494,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 06:00:00,2026-01-23 01:00:00-05:00,2026-01-23,1,0.49947768,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-23 07:00:00,2026-01-23 02:00:00-05:00,2026-01-23,1,0.5284809,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-23 08:00:00,2026-01-23 03:00:00-05:00,2026-01-23,0,0.49399173,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-23 09:00:00,2026-01-23 04:00:00-05:00,2026-01-23,0,0.45683447,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-23 10:00:00,2026-01-23 05:00:00-05:00,2026-01-23,1,0.44759712,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-23 11:00:00,2026-01-23 06:00:00-05:00,2026-01-23,1,0.46368843,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-23 12:00:00,2026-01-23 07:00:00-05:00,2026-01-23,1,0.4522303,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-23 13:00:00,2026-01-23 08:00:00-05:00,2026-01-23,0,0.5273792,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 14:00:00,2026-01-23 09:00:00-05:00,2026-01-23,1,0.49696228,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-23 15:00:00,2026-01-23 10:00:00-05:00,2026-01-23,0,0.5127735,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 16:00:00,2026-01-23 11:00:00-05:00,2026-01-23,0,0.5816189,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 17:00:00,2026-01-23 12:00:00-05:00,2026-01-23,0,0.58383,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 18:00:00,2026-01-23 13:00:00-05:00,2026-01-23,1,0.5591273,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-23 19:00:00,2026-01-23 14:00:00-05:00,2026-01-23,0,0.5854377,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 20:00:00,2026-01-23 15:00:00-05:00,2026-01-23,0,0.5666198,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 21:00:00,2026-01-23 16:00:00-05:00,2026-01-23,0,0.52965194,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 22:00:00,2026-01-23 17:00:00-05:00,2026-01-23,0,0.5488747,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-23 23:00:00,2026-01-23 18:00:00-05:00,2026-01-23,1,0.56132966,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-24 00:00:00,2026-01-23 19:00:00-05:00,2026-01-23,1,0.5149501,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-24 01:00:00,2026-01-23 20:00:00-05:00,2026-01-23,0,0.5064845,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-24 02:00:00,2026-01-23 21:00:00-05:00,2026-01-23,1,0.46972445,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-24 03:00:00,2026-01-23 22:00:00-05:00,2026-01-23,1,0.47380736,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-24 04:00:00,2026-01-23 23:00:00-05:00,2026-01-23,0,0.46636635,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-24 05:00:00,2026-01-24 00:00:00-05:00,2026-01-24,0,0.44455072,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-24 06:00:00,2026-01-24 01:00:00-05:00,2026-01-24,0,0.4378892,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-24 07:00:00,2026-01-24 02:00:00-05:00,2026-01-24,0,0.49291152,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-24 08:00:00,2026-01-24 03:00:00-05:00,2026-01-24,0,0.5153457,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-24 09:00:00,2026-01-24 04:00:00-05:00,2026-01-24,0,0.50489914,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-24 10:00:00,2026-01-24 05:00:00-05:00,2026-01-24,1,0.5075944,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-24 11:00:00,2026-01-24 06:00:00-05:00,2026-01-24,0,0.6229508,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-24 12:00:00,2026-01-24 07:00:00-05:00,2026-01-24,0,0.5855234,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-24 13:00:00,2026-01-24 08:00:00-05:00,2026-01-24,1,0.5532754,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-24 14:00:00,2026-01-24 09:00:00-05:00,2026-01-24,1,0.5289959,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-24 15:00:00,2026-01-24 10:00:00-05:00,2026-01-24,1,0.49748245,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-24 16:00:00,2026-01-24 11:00:00-05:00,2026-01-24,0,0.46435258,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-24 17:00:00,2026-01-24 12:00:00-05:00,2026-01-24,0,0.44683295,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-24 18:00:00,2026-01-24 13:00:00-05:00,2026-01-24,1,0.45363894,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-24 19:00:00,2026-01-24 14:00:00-05:00,2026-01-24,1,0.5047467,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-24 20:00:00,2026-01-24 15:00:00-05:00,2026-01-24,1,0.44000623,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-24 21:00:00,2026-01-24 16:00:00-05:00,2026-01-24,1,0.4814367,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-24 22:00:00,2026-01-24 17:00:00-05:00,2026-01-24,0,0.46426073,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-24 23:00:00,2026-01-24 18:00:00-05:00,2026-01-24,0,0.44996247,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-25 00:00:00,2026-01-24 19:00:00-05:00,2026-01-24,1,0.47704104,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 01:00:00,2026-01-24 20:00:00-05:00,2026-01-24,0,0.52445495,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-25 02:00:00,2026-01-24 21:00:00-05:00,2026-01-24,0,0.4987614,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 03:00:00,2026-01-24 22:00:00-05:00,2026-01-24,1,0.5457757,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-25 04:00:00,2026-01-24 23:00:00-05:00,2026-01-24,1,0.52091444,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-25 05:00:00,2026-01-25 00:00:00-05:00,2026-01-25,0,0.5040618,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-25 06:00:00,2026-01-25 01:00:00-05:00,2026-01-25,0,0.4632134,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 07:00:00,2026-01-25 02:00:00-05:00,2026-01-25,1,0.46248558,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 08:00:00,2026-01-25 03:00:00-05:00,2026-01-25,1,0.48469254,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 09:00:00,2026-01-25 04:00:00-05:00,2026-01-25,0,0.46292534,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 10:00:00,2026-01-25 05:00:00-05:00,2026-01-25,0,0.44414967,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 11:00:00,2026-01-25 06:00:00-05:00,2026-01-25,1,0.46307206,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 12:00:00,2026-01-25 07:00:00-05:00,2026-01-25,1,0.4829153,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 13:00:00,2026-01-25 08:00:00-05:00,2026-01-25,1,0.4569906,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 14:00:00,2026-01-25 09:00:00-05:00,2026-01-25,1,0.44465676,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 15:00:00,2026-01-25 10:00:00-05:00,2026-01-25,1,0.4582169,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 16:00:00,2026-01-25 11:00:00-05:00,2026-01-25,1,0.4422594,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-25 17:00:00,2026-01-25 12:00:00-05:00,2026-01-25,0,0.44241613,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 18:00:00,2026-01-25 13:00:00-05:00,2026-01-25,0,0.44766986,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 19:00:00,2026-01-25 14:00:00-05:00,2026-01-25,0,0.4483085,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 20:00:00,2026-01-25 15:00:00-05:00,2026-01-25,0,0.491638,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 21:00:00,2026-01-25 16:00:00-05:00,2026-01-25,0,0.49061048,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-25 22:00:00,2026-01-25 17:00:00-05:00,2026-01-25,0,0.5258992,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-25 23:00:00,2026-01-25 18:00:00-05:00,2026-01-25,0,0.520721,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-26 00:00:00,2026-01-25 19:00:00-05:00,2026-01-25,0,0.5159272,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-26 01:00:00,2026-01-25 20:00:00-05:00,2026-01-25,1,0.5431046,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-26 02:00:00,2026-01-25 21:00:00-05:00,2026-01-25,0,0.51656026,1,0,0,-1,0,False,False,True
|
||||||
|
2026-01-26 03:00:00,2026-01-25 22:00:00-05:00,2026-01-25,0,0.48795614,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-26 04:00:00,2026-01-25 23:00:00-05:00,2026-01-25,0,0.50463694,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-26 05:00:00,2026-01-26 00:00:00-05:00,2026-01-26,1,0.5487247,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-26 06:00:00,2026-01-26 01:00:00-05:00,2026-01-26,1,0.5424091,1,0,0,-1,0,True,False,False
|
||||||
|
2026-01-26 07:00:00,2026-01-26 02:00:00-05:00,2026-01-26,0,0.47699133,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-26 08:00:00,2026-01-26 03:00:00-05:00,2026-01-26,1,0.46807176,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-26 09:00:00,2026-01-26 04:00:00-05:00,2026-01-26,1,0.5724977,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-26 10:00:00,2026-01-26 05:00:00-05:00,2026-01-26,1,0.5247946,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-26 11:00:00,2026-01-26 06:00:00-05:00,2026-01-26,0,0.46081114,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-26 12:00:00,2026-01-26 07:00:00-05:00,2026-01-26,1,0.4425639,0,0,0,0,0,False,False,False
|
||||||
|
2026-01-26 13:00:00,2026-01-26 08:00:00-05:00,2026-01-26,1,0.47229946,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-26 14:00:00,2026-01-26 09:00:00-05:00,2026-01-26,1,0.49050438,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-26 15:00:00,2026-01-26 10:00:00-05:00,2026-01-26,0,0.49104828,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-26 16:00:00,2026-01-26 11:00:00-05:00,2026-01-26,0,0.45145187,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-26 17:00:00,2026-01-26 12:00:00-05:00,2026-01-26,0,0.4638353,0,0,0,0,0,True,True,True
|
||||||
|
2026-01-26 18:00:00,2026-01-26 13:00:00-05:00,2026-01-26,0,0.4618484,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-26 19:00:00,2026-01-26 14:00:00-05:00,2026-01-26,1,0.5018996,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-26 20:00:00,2026-01-26 15:00:00-05:00,2026-01-26,1,0.5166753,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-26 21:00:00,2026-01-26 16:00:00-05:00,2026-01-26,0,0.52520907,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-26 22:00:00,2026-01-26 17:00:00-05:00,2026-01-26,0,0.5878727,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-26 23:00:00,2026-01-26 18:00:00-05:00,2026-01-26,1,0.54239446,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-27 00:00:00,2026-01-26 19:00:00-05:00,2026-01-26,0,0.53454095,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-27 01:00:00,2026-01-26 20:00:00-05:00,2026-01-26,1,0.467306,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-27 02:00:00,2026-01-26 21:00:00-05:00,2026-01-26,0,0.46864188,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 03:00:00,2026-01-26 22:00:00-05:00,2026-01-26,1,0.46822935,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-27 04:00:00,2026-01-26 23:00:00-05:00,2026-01-26,0,0.49130705,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 05:00:00,2026-01-27 00:00:00-05:00,2026-01-27,0,0.46864477,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 06:00:00,2026-01-27 01:00:00-05:00,2026-01-27,1,0.486711,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-27 07:00:00,2026-01-27 02:00:00-05:00,2026-01-27,1,0.5504777,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-27 08:00:00,2026-01-27 03:00:00-05:00,2026-01-27,0,0.4907592,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 09:00:00,2026-01-27 04:00:00-05:00,2026-01-27,0,0.4668024,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 10:00:00,2026-01-27 05:00:00-05:00,2026-01-27,0,0.50845945,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-27 11:00:00,2026-01-27 06:00:00-05:00,2026-01-27,1,0.5608594,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-27 12:00:00,2026-01-27 07:00:00-05:00,2026-01-27,0,0.51024324,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-27 13:00:00,2026-01-27 08:00:00-05:00,2026-01-27,0,0.51232666,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-27 14:00:00,2026-01-27 09:00:00-05:00,2026-01-27,0,0.4854087,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 15:00:00,2026-01-27 10:00:00-05:00,2026-01-27,0,0.48897353,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 16:00:00,2026-01-27 11:00:00-05:00,2026-01-27,0,0.46034786,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 17:00:00,2026-01-27 12:00:00-05:00,2026-01-27,1,0.5265957,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-27 18:00:00,2026-01-27 13:00:00-05:00,2026-01-27,1,0.5300391,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-27 19:00:00,2026-01-27 14:00:00-05:00,2026-01-27,0,0.49372414,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-27 20:00:00,2026-01-27 15:00:00-05:00,2026-01-27,1,0.4988915,0,1,0,-1,0,False,False,False
|
||||||
|
2026-01-27 21:00:00,2026-01-27 16:00:00-05:00,2026-01-27,1,0.53798217,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-27 22:00:00,2026-01-27 17:00:00-05:00,2026-01-27,0,0.5018221,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-27 23:00:00,2026-01-27 18:00:00-05:00,2026-01-27,0,0.49853957,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-28 00:00:00,2026-01-27 19:00:00-05:00,2026-01-27,1,0.5263078,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-28 01:00:00,2026-01-27 20:00:00-05:00,2026-01-27,0,0.48831585,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-28 02:00:00,2026-01-27 21:00:00-05:00,2026-01-27,1,0.53863657,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-28 03:00:00,2026-01-27 22:00:00-05:00,2026-01-27,0,0.5574659,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-28 04:00:00,2026-01-27 23:00:00-05:00,2026-01-27,0,0.529262,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-28 05:00:00,2026-01-28 00:00:00-05:00,2026-01-28,0,0.46035758,0,1,0,-1,0,True,False,True
|
||||||
|
2026-01-28 06:00:00,2026-01-28 01:00:00-05:00,2026-01-28,1,0.5224445,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-28 07:00:00,2026-01-28 02:00:00-05:00,2026-01-28,0,0.51074743,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-28 08:00:00,2026-01-28 03:00:00-05:00,2026-01-28,0,0.519096,1,1,0,1,1,False,False,False
|
||||||
|
2026-01-28 09:00:00,2026-01-28 04:00:00-05:00,2026-01-28,1,0.53050256,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-28 10:00:00,2026-01-28 05:00:00-05:00,2026-01-28,1,0.54665875,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-28 11:00:00,2026-01-28 06:00:00-05:00,2026-01-28,1,0.5373869,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-28 12:00:00,2026-01-28 07:00:00-05:00,2026-01-28,1,0.53682816,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-28 13:00:00,2026-01-28 08:00:00-05:00,2026-01-28,1,0.50733596,1,1,0,1,1,True,True,True
|
||||||
|
2026-01-28 14:00:00,2026-01-28 09:00:00-05:00,2026-01-28,0,0.466285,0,1,0,-1,0,True,False,True
|
||||||
|
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"测试期间": {
|
||||||
|
"开始日期": "2026-01-01 00:00:00",
|
||||||
|
"结束日期": "2026-01-30 00:00:00",
|
||||||
|
"总天数": 22,
|
||||||
|
"总样本数": 495
|
||||||
|
},
|
||||||
|
"单模型性能": {
|
||||||
|
"DLinear": {
|
||||||
|
"准确率": "53.33%",
|
||||||
|
"预测上涨比例": "47.1%"
|
||||||
|
},
|
||||||
|
"N-BEATS": {
|
||||||
|
"准确率": "49.90%",
|
||||||
|
"预测上涨比例": "73.5%"
|
||||||
|
},
|
||||||
|
"TTM-R2": {
|
||||||
|
"准确率": "51.31%",
|
||||||
|
"预测上涨比例": "23.6%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"集成策略性能": {
|
||||||
|
"双模型交集": {
|
||||||
|
"准确率": "53.39%",
|
||||||
|
"覆盖率": "47.68%",
|
||||||
|
"有效预测数": 236
|
||||||
|
},
|
||||||
|
"三模型投票": {
|
||||||
|
"准确率": "52.93%",
|
||||||
|
"覆盖率": "100.00%",
|
||||||
|
"有效预测数": 495
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DLinear概率分布": {
|
||||||
|
"0.4-0.5区间": "262个 (52.9%)",
|
||||||
|
"0.5-0.6区间": "229个 (46.3%)",
|
||||||
|
"0.6-0.7区间": "4个 (0.8%)",
|
||||||
|
"平均概率": "0.501"
|
||||||
|
},
|
||||||
|
"最佳/最差交易日": {
|
||||||
|
"双模型最佳日": {
|
||||||
|
"日期": "2026-01-11",
|
||||||
|
"准确率": "83.3%"
|
||||||
|
},
|
||||||
|
"双模型最差日": {
|
||||||
|
"日期": "2026-01-23",
|
||||||
|
"准确率": "28.6%"
|
||||||
|
},
|
||||||
|
"三模型最佳日": {
|
||||||
|
"日期": "2026-01-28",
|
||||||
|
"准确率": "80.0%"
|
||||||
|
},
|
||||||
|
"三模型最差日": {
|
||||||
|
"日期": "2026-01-23",
|
||||||
|
"准确率": "29.2%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
"""保存所有测试结果到项目 - 生成完整报告"""
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
"""智能休息策略回测:基于历史准确率动态决定是否交易"""
|
||||||
|
|
||||||
|
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 data.fetcher import BinanceDataFetcher
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 预处理器定义 ====================
|
||||||
|
|
||||||
|
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)
|
||||||
|
print("\n策略规则:")
|
||||||
|
print(" 1. 如果前一天准确率超过70% → 当天休息")
|
||||||
|
print(" 2. 如果连续两天准确率超过60% → 第三天休息")
|
||||||
|
print(" 3. 其他情况 → 正常交易")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 加载模型 ==========
|
||||||
|
print("\n[1/4] 加载模型...")
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
|
||||||
|
# 加载DLinear
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 加载N-BEATS
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 加载TTM-R2
|
||||||
|
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(f"✓ 三个模型已加载")
|
||||||
|
|
||||||
|
# ========== 获取数据 ==========
|
||||||
|
print("\n[2/4] 获取2026年1月测试数据(1月1日-1月30日)...")
|
||||||
|
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)} 行")
|
||||||
|
print(f"✓ 时间范围: {df_2026['timestamp'].min()} 至 {df_2026['timestamp'].max()}")
|
||||||
|
|
||||||
|
# ========== 预处理 ==========
|
||||||
|
print("\n[3/4] 预处理数据...")
|
||||||
|
|
||||||
|
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]
|
||||||
|
y_nbeats = y_nbeats[:min_samples]
|
||||||
|
|
||||||
|
# ========== 预测 ==========
|
||||||
|
print("\n[4/4] 执行预测...")
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
dlinear_preds = (dlinear_probs > 0.5).astype(int).flatten()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# ========== 构建每日数据 ==========
|
||||||
|
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
|
||||||
|
|
||||||
|
results_df = pd.DataFrame({
|
||||||
|
'date': dates,
|
||||||
|
'timestamp_et': timestamps_et,
|
||||||
|
'true_label': true_labels,
|
||||||
|
'two_model_pred': two_model_preds,
|
||||||
|
'three_model_pred': three_model_preds,
|
||||||
|
'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'
|
||||||
|
})
|
||||||
|
|
||||||
|
daily_stats.columns = ['total_samples', 'two_model_valid', 'two_model_correct', 'three_model_correct']
|
||||||
|
|
||||||
|
# 计算每日准确率
|
||||||
|
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_accuracy'] = daily_stats['two_model_accuracy'].fillna(0)
|
||||||
|
|
||||||
|
# ========== 应用智能休息策略 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📊 应用智能休息策略")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 为每一天添加"是否休息"标记
|
||||||
|
daily_stats['two_model_rest'] = False
|
||||||
|
daily_stats['three_model_rest'] = False
|
||||||
|
daily_stats['rest_reason'] = ''
|
||||||
|
|
||||||
|
dates_list = daily_stats.index.tolist()
|
||||||
|
|
||||||
|
for i, date in enumerate(dates_list):
|
||||||
|
if i == 0:
|
||||||
|
continue # 第一天没有历史数据,不休息
|
||||||
|
|
||||||
|
# 获取前一天和前两天的数据
|
||||||
|
prev_date = dates_list[i-1]
|
||||||
|
prev2_date = dates_list[i-2] if i >= 2 else None
|
||||||
|
|
||||||
|
# 双模型休息逻辑
|
||||||
|
# 规则1:前一天准确率超过70%
|
||||||
|
if daily_stats.loc[prev_date, 'two_model_accuracy'] > 70:
|
||||||
|
daily_stats.loc[date, 'two_model_rest'] = True
|
||||||
|
daily_stats.loc[date, 'rest_reason'] = f'前日准确率{daily_stats.loc[prev_date, "two_model_accuracy"]:.1f}%>70%'
|
||||||
|
|
||||||
|
# 规则2:连续两天准确率超过60%
|
||||||
|
elif (prev2_date is not None and
|
||||||
|
daily_stats.loc[prev_date, 'two_model_accuracy'] > 60 and
|
||||||
|
daily_stats.loc[prev2_date, 'two_model_accuracy'] > 60):
|
||||||
|
daily_stats.loc[date, 'two_model_rest'] = True
|
||||||
|
daily_stats.loc[date, 'rest_reason'] = f'连续2天>60% ({daily_stats.loc[prev2_date, "two_model_accuracy"]:.1f}%, {daily_stats.loc[prev_date, "two_model_accuracy"]:.1f}%)'
|
||||||
|
|
||||||
|
# 三模型休息逻辑
|
||||||
|
# 规则1:前一天准确率超过70%
|
||||||
|
if daily_stats.loc[prev_date, 'three_model_accuracy'] > 70:
|
||||||
|
daily_stats.loc[date, 'three_model_rest'] = True
|
||||||
|
if not daily_stats.loc[date, 'rest_reason']:
|
||||||
|
daily_stats.loc[date, 'rest_reason'] = f'三模前日{daily_stats.loc[prev_date, "three_model_accuracy"]:.1f}%>70%'
|
||||||
|
|
||||||
|
# 规则2:连续两天准确率超过60%
|
||||||
|
elif (prev2_date is not None and
|
||||||
|
daily_stats.loc[prev_date, 'three_model_accuracy'] > 60 and
|
||||||
|
daily_stats.loc[prev2_date, 'three_model_accuracy'] > 60):
|
||||||
|
daily_stats.loc[date, 'three_model_rest'] = True
|
||||||
|
if not daily_stats.loc[date, 'rest_reason']:
|
||||||
|
daily_stats.loc[date, 'rest_reason'] = f'三模连续2天>60%'
|
||||||
|
|
||||||
|
# ========== 统计休息天数 ==========
|
||||||
|
two_model_rest_days = daily_stats['two_model_rest'].sum()
|
||||||
|
three_model_rest_days = daily_stats['three_model_rest'].sum()
|
||||||
|
total_days = len(daily_stats)
|
||||||
|
|
||||||
|
print(f"\n休息天数统计:")
|
||||||
|
print(f" 总交易日: {total_days}天")
|
||||||
|
print(f" 双模型休息: {two_model_rest_days}天 ({two_model_rest_days/total_days*100:.1f}%)")
|
||||||
|
print(f" 三模型休息: {three_model_rest_days}天 ({three_model_rest_days/total_days*100:.1f}%)")
|
||||||
|
|
||||||
|
# ========== 计算休息策略的实际表现 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📈 策略对比:正常交易 vs 智能休息")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 双模型策略
|
||||||
|
print(f"\n🔵 双模型交集策略:")
|
||||||
|
|
||||||
|
# 正常交易(不休息)
|
||||||
|
two_normal_correct = daily_stats['two_model_correct'].sum()
|
||||||
|
two_normal_total = daily_stats['two_model_valid'].sum()
|
||||||
|
two_normal_acc = two_normal_correct / two_normal_total * 100 if two_normal_total > 0 else 0
|
||||||
|
|
||||||
|
print(f"\n 📊 正常交易(不休息):")
|
||||||
|
print(f" 交易天数: {total_days}天")
|
||||||
|
print(f" 有效预测: {two_normal_total}次")
|
||||||
|
print(f" 正确预测: {two_normal_correct}次")
|
||||||
|
print(f" 准确率: {two_normal_acc:.2f}%")
|
||||||
|
|
||||||
|
# 智能休息
|
||||||
|
two_rest_mask = ~daily_stats['two_model_rest']
|
||||||
|
two_rest_correct = daily_stats.loc[two_rest_mask, 'two_model_correct'].sum()
|
||||||
|
two_rest_total = daily_stats.loc[two_rest_mask, 'two_model_valid'].sum()
|
||||||
|
two_rest_acc = two_rest_correct / two_rest_total * 100 if two_rest_total > 0 else 0
|
||||||
|
two_trade_days = two_rest_mask.sum()
|
||||||
|
|
||||||
|
print(f"\n 🛌 智能休息策略:")
|
||||||
|
print(f" 交易天数: {two_trade_days}天 (休息{two_model_rest_days}天)")
|
||||||
|
print(f" 有效预测: {two_rest_total}次")
|
||||||
|
print(f" 正确预测: {two_rest_correct}次")
|
||||||
|
print(f" 准确率: {two_rest_acc:.2f}%")
|
||||||
|
|
||||||
|
two_acc_diff = two_rest_acc - two_normal_acc
|
||||||
|
two_diff_mark = "✅" if two_acc_diff > 0 else ("⚠️" if two_acc_diff < 0 else "")
|
||||||
|
print(f"\n 准确率变化: {two_acc_diff:+.2f}% {two_diff_mark}")
|
||||||
|
|
||||||
|
# 休息日的实际表现
|
||||||
|
if two_model_rest_days > 0:
|
||||||
|
two_rest_days_mask = daily_stats['two_model_rest']
|
||||||
|
two_rest_day_correct = daily_stats.loc[two_rest_days_mask, 'two_model_correct'].sum()
|
||||||
|
two_rest_day_total = daily_stats.loc[two_rest_days_mask, 'two_model_valid'].sum()
|
||||||
|
two_rest_day_acc = two_rest_day_correct / two_rest_day_total * 100 if two_rest_day_total > 0 else 0
|
||||||
|
|
||||||
|
print(f"\n 🔍 被休息的{two_model_rest_days}天实际表现:")
|
||||||
|
print(f" 准确率: {two_rest_day_acc:.2f}%")
|
||||||
|
avoided_loss = "避免了低准确率" if two_rest_day_acc < two_normal_acc else "⚠️ 错过了好机会"
|
||||||
|
print(f" 评价: {avoided_loss}")
|
||||||
|
|
||||||
|
# 三模型策略
|
||||||
|
print(f"\n🟢 三模型投票策略:")
|
||||||
|
|
||||||
|
# 正常交易
|
||||||
|
three_normal_correct = daily_stats['three_model_correct'].sum()
|
||||||
|
three_normal_total = daily_stats['total_samples'].sum()
|
||||||
|
three_normal_acc = three_normal_correct / three_normal_total * 100
|
||||||
|
|
||||||
|
print(f"\n 📊 正常交易(不休息):")
|
||||||
|
print(f" 交易天数: {total_days}天")
|
||||||
|
print(f" 有效预测: {three_normal_total}次")
|
||||||
|
print(f" 正确预测: {three_normal_correct}次")
|
||||||
|
print(f" 准确率: {three_normal_acc:.2f}%")
|
||||||
|
|
||||||
|
# 智能休息
|
||||||
|
three_rest_mask = ~daily_stats['three_model_rest']
|
||||||
|
three_rest_correct = daily_stats.loc[three_rest_mask, 'three_model_correct'].sum()
|
||||||
|
three_rest_total = daily_stats.loc[three_rest_mask, 'total_samples'].sum()
|
||||||
|
three_rest_acc = three_rest_correct / three_rest_total * 100 if three_rest_total > 0 else 0
|
||||||
|
three_trade_days = three_rest_mask.sum()
|
||||||
|
|
||||||
|
print(f"\n 🛌 智能休息策略:")
|
||||||
|
print(f" 交易天数: {three_trade_days}天 (休息{three_model_rest_days}天)")
|
||||||
|
print(f" 有效预测: {three_rest_total}次")
|
||||||
|
print(f" 正确预测: {three_rest_correct}次")
|
||||||
|
print(f" 准确率: {three_rest_acc:.2f}%")
|
||||||
|
|
||||||
|
three_acc_diff = three_rest_acc - three_normal_acc
|
||||||
|
three_diff_mark = "✅" if three_acc_diff > 0 else ("⚠️" if three_acc_diff < 0 else "")
|
||||||
|
print(f"\n 准确率变化: {three_acc_diff:+.2f}% {three_diff_mark}")
|
||||||
|
|
||||||
|
# 休息日的实际表现
|
||||||
|
if three_model_rest_days > 0:
|
||||||
|
three_rest_days_mask = daily_stats['three_model_rest']
|
||||||
|
three_rest_day_correct = daily_stats.loc[three_rest_days_mask, 'three_model_correct'].sum()
|
||||||
|
three_rest_day_total = daily_stats.loc[three_rest_days_mask, 'total_samples'].sum()
|
||||||
|
three_rest_day_acc = three_rest_day_correct / three_rest_day_total * 100
|
||||||
|
|
||||||
|
print(f"\n 🔍 被休息的{three_model_rest_days}天实际表现:")
|
||||||
|
print(f" 准确率: {three_rest_day_acc:.2f}%")
|
||||||
|
avoided_loss = "避免了低准确率" if three_rest_day_acc < three_normal_acc else "⚠️ 错过了好机会"
|
||||||
|
print(f" 评价: {avoided_loss}")
|
||||||
|
|
||||||
|
# ========== 详细日志 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("📅 每日详细记录")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print(f"\n{'日期':<12} {'双模准确率':>10} {'三模准确率':>10} {'休息状态':<12} {'休息原因':<40}")
|
||||||
|
print("-" * 95)
|
||||||
|
|
||||||
|
for date, row in daily_stats.iterrows():
|
||||||
|
two_acc = row['two_model_accuracy']
|
||||||
|
three_acc = row['three_model_accuracy']
|
||||||
|
|
||||||
|
rest_status = ""
|
||||||
|
if row['two_model_rest']:
|
||||||
|
rest_status += "双休 "
|
||||||
|
if row['three_model_rest']:
|
||||||
|
rest_status += "三休"
|
||||||
|
|
||||||
|
if not rest_status:
|
||||||
|
rest_status = "正常交易"
|
||||||
|
|
||||||
|
# 准确率标记
|
||||||
|
two_mark = "🔥" if two_acc >= 70 else ("✅" if two_acc >= 60 else "")
|
||||||
|
three_mark = "🔥" if three_acc >= 70 else ("✅" if three_acc >= 60 else "")
|
||||||
|
|
||||||
|
print(f"{date} {two_acc:>9.1f}{two_mark} {three_acc:>9.1f}{three_mark} "
|
||||||
|
f"{rest_status:<12} {row['rest_reason']:<40}")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 智能休息策略回测完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
"""在2026年数据上测试分类模型"""
|
|
||||||
|
|
||||||
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, classification_report
|
|
||||||
from data.fetcher import BinanceDataFetcher
|
|
||||||
from models.dlinear.model import DLinear
|
|
||||||
|
|
||||||
class ClassificationPreprocessor:
|
|
||||||
"""分类任务预处理器"""
|
|
||||||
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:
|
|
||||||
if not self.is_fitted:
|
|
||||||
raise ValueError("Must fit before transform")
|
|
||||||
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
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("="*80)
|
|
||||||
print("2026年分类模型测试")
|
|
||||||
print("="*80)
|
|
||||||
|
|
||||||
# ========== 加载模型 ==========
|
|
||||||
print("\n[1/4] 加载模型...")
|
|
||||||
model_path = './models/checkpoints/dlinear_classification_2026/best_model.pt'
|
|
||||||
preprocessor_path = './models/checkpoints/dlinear_classification_2026/preprocessor.pkl'
|
|
||||||
|
|
||||||
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
|
||||||
checkpoint = torch.load(model_path, map_location=device, weights_only=False)
|
|
||||||
model = DLinear(checkpoint['config'])
|
|
||||||
model.load_state_dict(checkpoint['model_state_dict'])
|
|
||||||
model = model.to(device)
|
|
||||||
model.eval()
|
|
||||||
|
|
||||||
with open(preprocessor_path, 'rb') as f:
|
|
||||||
preprocessor = pickle.load(f)
|
|
||||||
|
|
||||||
print(f"✓ 模型加载成功")
|
|
||||||
print(f"✓ 任务类型: 分类(预测涨/跌)")
|
|
||||||
|
|
||||||
# ========== 获取2026年数据 ==========
|
|
||||||
print("\n[2/4] 获取2026年测试数据...")
|
|
||||||
fetcher = BinanceDataFetcher()
|
|
||||||
|
|
||||||
try:
|
|
||||||
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
|
||||||
end_date='2026-12-31', use_cache=False)
|
|
||||||
except:
|
|
||||||
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', use_cache=False)
|
|
||||||
|
|
||||||
print(f"✓ 测试数据: {len(df_2026)} 行")
|
|
||||||
print(f"✓ 时间: {df_2026['timestamp'].min()} 至 {df_2026['timestamp'].max()}")
|
|
||||||
print(f"✓ 价格: ${df_2026['close'].iloc[0]:,.2f} → ${df_2026['close'].iloc[-1]:,.2f}")
|
|
||||||
|
|
||||||
# ========== 预处理 ==========
|
|
||||||
print("\n[3/4] 预处理并预测...")
|
|
||||||
X_test, y_test, processed_df = preprocessor.preprocess(df_2026, fit=False)
|
|
||||||
|
|
||||||
if len(X_test) == 0:
|
|
||||||
print("❌ 数据不足")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"✓ 测试序列: {len(X_test)}")
|
|
||||||
|
|
||||||
# 真实标签统计
|
|
||||||
n_true_up = (y_test == 1).sum()
|
|
||||||
n_true_down = (y_test == 0).sum()
|
|
||||||
print(f"✓ 实际涨跌: 上涨{n_true_up}次 ({n_true_up/len(y_test)*100:.1f}%), "
|
|
||||||
f"下跌{n_true_down}次 ({n_true_down/len(y_test)*100:.1f}%)")
|
|
||||||
|
|
||||||
# 模型预测
|
|
||||||
X_tensor = torch.FloatTensor(X_test).to(device)
|
|
||||||
with torch.no_grad():
|
|
||||||
logits = model(X_tensor).squeeze().cpu().numpy()
|
|
||||||
probs = 1 / (1 + np.exp(-logits)) # sigmoid
|
|
||||||
preds = (probs > 0.5).astype(int)
|
|
||||||
|
|
||||||
# ========== 评估 ==========
|
|
||||||
print("\n[4/4] 评估结果...")
|
|
||||||
|
|
||||||
# 基础统计
|
|
||||||
n_pred_up = (preds == 1).sum()
|
|
||||||
n_pred_down = (preds == 0).sum()
|
|
||||||
accuracy = (preds == y_test).mean()
|
|
||||||
|
|
||||||
# 混淆矩阵
|
|
||||||
cm = confusion_matrix(y_test, preds)
|
|
||||||
tn, fp, fn, tp = cm.ravel()
|
|
||||||
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("📊 2026年测试结果(分类任务)")
|
|
||||||
print("="*80)
|
|
||||||
|
|
||||||
print(f"\n预测统计:")
|
|
||||||
print(f" 预测上涨: {n_pred_up} ({n_pred_up/len(preds)*100:.1f}%)")
|
|
||||||
print(f" 预测下跌: {n_pred_down} ({n_pred_down/len(preds)*100:.1f}%)")
|
|
||||||
|
|
||||||
print(f"\n实际统计:")
|
|
||||||
print(f" 实际上涨: {n_true_up} ({n_true_up/len(y_test)*100:.1f}%)")
|
|
||||||
print(f" 实际下跌: {n_true_down} ({n_true_down/len(y_test)*100:.1f}%)")
|
|
||||||
|
|
||||||
print(f"\n✨ 准确率: {accuracy*100:.2f}%")
|
|
||||||
|
|
||||||
print(f"\n详细分析:")
|
|
||||||
print(f" ✓ 预测涨且真涨 (TP): {tp} ({tp/len(preds)*100:.1f}%)")
|
|
||||||
print(f" ✓ 预测跌且真跌 (TN): {tn} ({tn/len(preds)*100:.1f}%)")
|
|
||||||
print(f" ✗ 预测涨但真跌 (FP): {fp} ({fp/len(preds)*100:.1f}%)")
|
|
||||||
print(f" ✗ 预测跌但真涨 (FN): {fn} ({fn/len(preds)*100:.1f}%)")
|
|
||||||
|
|
||||||
# 精确率、召回率、F1
|
|
||||||
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
|
|
||||||
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
|
|
||||||
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
|
|
||||||
|
|
||||||
print(f"\n分类指标:")
|
|
||||||
print(f" 精确率 (Precision): {precision*100:.2f}%")
|
|
||||||
print(f" 召回率 (Recall): {recall*100:.2f}%")
|
|
||||||
print(f" F1分数: {f1:.4f}")
|
|
||||||
|
|
||||||
# 预测偏差分析
|
|
||||||
bias = n_pred_up - n_true_up
|
|
||||||
print(f"\n预测偏差:")
|
|
||||||
if abs(bias) < len(preds) * 0.05:
|
|
||||||
print(f" ✅ 预测平衡(偏差{bias}个)")
|
|
||||||
elif bias > 0:
|
|
||||||
print(f" ⚠️ 偏向预测上涨(多预测{bias}次上涨)")
|
|
||||||
else:
|
|
||||||
print(f" ⚠️ 偏向预测下跌(多预测{-bias}次下跌)")
|
|
||||||
|
|
||||||
# 性能评估
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("🎯 性能评估")
|
|
||||||
print("="*80)
|
|
||||||
|
|
||||||
if accuracy > 0.55:
|
|
||||||
print(f"\n ✅ 准确率 {accuracy*100:.2f}% > 55%,模型有预测能力!")
|
|
||||||
elif accuracy > 0.52:
|
|
||||||
print(f"\n ⚠️ 准确率 {accuracy*100:.2f}% > 52%,略优于随机")
|
|
||||||
elif accuracy >= 0.48:
|
|
||||||
print(f"\n ⚠️ 准确率 {accuracy*100:.2f}% ≈ 50%,接近随机")
|
|
||||||
else:
|
|
||||||
print(f"\n ❌ 准确率 {accuracy*100:.2f}% < 48%,低于随机")
|
|
||||||
|
|
||||||
# 预测样本展示
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("📋 预测样本展示(前30个)")
|
|
||||||
print("="*80)
|
|
||||||
print(f"\n{'时间':<20} {'预测概率':<10} {'预测':<6} {'实际':<6} {'结果':<4}")
|
|
||||||
print("-"*60)
|
|
||||||
|
|
||||||
for i in range(min(30, len(preds))):
|
|
||||||
timestamp = processed_df['timestamp'].iloc[i + 168]
|
|
||||||
prob = probs[i]
|
|
||||||
pred = "上涨" if preds[i] == 1 else "下跌"
|
|
||||||
true = "上涨" if y_test[i] == 1 else "下跌"
|
|
||||||
result = "✓" if preds[i] == y_test[i] else "✗"
|
|
||||||
|
|
||||||
print(f"{str(timestamp):<20} {prob:>8.4f} {pred:<6} {true:<6} {result:<4}")
|
|
||||||
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print(f"总结: 在{len(preds)}次预测中,准确率 {accuracy*100:.2f}%")
|
|
||||||
print("="*80)
|
|
||||||
|
|
||||||
# 保存结果
|
|
||||||
results = {
|
|
||||||
'accuracy': float(accuracy),
|
|
||||||
'precision': float(precision),
|
|
||||||
'recall': float(recall),
|
|
||||||
'f1_score': float(f1),
|
|
||||||
'true_positives': int(tp),
|
|
||||||
'true_negatives': int(tn),
|
|
||||||
'false_positives': int(fp),
|
|
||||||
'false_negatives': int(fn),
|
|
||||||
'n_samples': int(len(preds))
|
|
||||||
}
|
|
||||||
|
|
||||||
import json
|
|
||||||
with open('./models/checkpoints/dlinear_classification_2026/test_2026_results.json', 'w') as f:
|
|
||||||
json.dump(results, f, indent=2)
|
|
||||||
|
|
||||||
print(f"\n✓ 结果已保存至: ./models/checkpoints/dlinear_classification_2026/test_2026_results.json")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
"""训练CatBoost二分类模型用于BTC 1小时涨跌预测"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pandas_ta as ta
|
||||||
|
from catboost import CatBoostClassifier, Pool
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from sklearn.metrics import classification_report, accuracy_score, confusion_matrix
|
||||||
|
from data.fetcher import BinanceDataFetcher
|
||||||
|
import pickle
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def add_technical_features(df):
|
||||||
|
"""添加丰富的技术指标特征"""
|
||||||
|
df = df.copy()
|
||||||
|
|
||||||
|
# 基础价格特征
|
||||||
|
df['price_change'] = df['close'] - df['open']
|
||||||
|
df['price_change_pct'] = (df['close'] - df['open']) / df['open']
|
||||||
|
df['high_low_range'] = df['high'] - df['low']
|
||||||
|
df['high_low_range_pct'] = (df['high'] - df['low']) / df['low']
|
||||||
|
|
||||||
|
# 移动平均线
|
||||||
|
df['sma_7'] = ta.sma(df['close'], length=7)
|
||||||
|
df['sma_25'] = ta.sma(df['close'], length=25)
|
||||||
|
df['sma_99'] = ta.sma(df['close'], length=99)
|
||||||
|
df['ema_12'] = ta.ema(df['close'], length=12)
|
||||||
|
df['ema_26'] = ta.ema(df['close'], length=26)
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
df['rsi_14'] = ta.rsi(df['close'], length=14)
|
||||||
|
df['rsi_7'] = ta.rsi(df['close'], length=7)
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
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]
|
||||||
|
|
||||||
|
# 布林带
|
||||||
|
bbands = ta.bbands(df['close'], length=20)
|
||||||
|
if bbands is not None and not bbands.empty:
|
||||||
|
df['bb_upper'] = bbands.iloc[:, 0]
|
||||||
|
df['bb_mid'] = bbands.iloc[:, 1]
|
||||||
|
df['bb_lower'] = bbands.iloc[:, 2]
|
||||||
|
df['bb_width'] = (df['bb_upper'] - df['bb_lower']) / df['bb_mid']
|
||||||
|
df['bb_position'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
|
||||||
|
|
||||||
|
# ATR (Average True Range)
|
||||||
|
df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14)
|
||||||
|
|
||||||
|
# 成交量特征
|
||||||
|
df['volume_sma_7'] = ta.sma(df['volume'], length=7)
|
||||||
|
df['volume_ratio'] = df['volume'] / df['volume_sma_7']
|
||||||
|
|
||||||
|
# OBV (On-Balance Volume)
|
||||||
|
df['obv'] = ta.obv(df['close'], df['volume'])
|
||||||
|
|
||||||
|
# Stochastic Oscillator
|
||||||
|
stoch = ta.stoch(df['high'], df['low'], df['close'])
|
||||||
|
if stoch is not None and not stoch.empty:
|
||||||
|
df['stoch_k'] = stoch.iloc[:, 0]
|
||||||
|
df['stoch_d'] = stoch.iloc[:, 1]
|
||||||
|
|
||||||
|
# ADX (Average Directional Index)
|
||||||
|
adx = ta.adx(df['high'], df['low'], df['close'])
|
||||||
|
if adx is not None and not adx.empty:
|
||||||
|
df['adx'] = adx.iloc[:, 0]
|
||||||
|
|
||||||
|
# 价格动量
|
||||||
|
df['momentum_1'] = df['close'].pct_change(1)
|
||||||
|
df['momentum_3'] = df['close'].pct_change(3)
|
||||||
|
df['momentum_7'] = df['close'].pct_change(7)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def create_labels(df, forecast_horizon=1):
|
||||||
|
"""创建二分类标签"""
|
||||||
|
df = df.copy()
|
||||||
|
df['future_price'] = df['close'].shift(-forecast_horizon)
|
||||||
|
df['future_return'] = (df['future_price'] - df['close']) / df['close']
|
||||||
|
df['label'] = (df['future_return'] > 0).astype(int) # 1=涨, 0=跌
|
||||||
|
return df[:-forecast_horizon]
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_data(df):
|
||||||
|
"""准备训练数据"""
|
||||||
|
# 添加技术指标
|
||||||
|
df = add_technical_features(df)
|
||||||
|
|
||||||
|
# 创建标签
|
||||||
|
df = create_labels(df, forecast_horizon=1)
|
||||||
|
|
||||||
|
# 删除NaN
|
||||||
|
df = df.dropna().reset_index(drop=True)
|
||||||
|
|
||||||
|
# 选择特征列(排除原始时间戳和标签列)
|
||||||
|
exclude_cols = ['timestamp', 'label', 'future_price', 'future_return']
|
||||||
|
feature_cols = [col for col in df.columns if col not in exclude_cols]
|
||||||
|
|
||||||
|
X = df[feature_cols]
|
||||||
|
y = df['label']
|
||||||
|
|
||||||
|
return X, y, feature_cols
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("="*80)
|
||||||
|
print("训练CatBoost二分类模型 - BTC 1小时涨跌预测")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 步骤1: 获取数据 ==========
|
||||||
|
print("\n[1/5] 获取历史数据...")
|
||||||
|
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
|
||||||
|
# 获取训练数据 (2020-2024)
|
||||||
|
df_train = fetcher.fetch_ohlcv('BTC', '1h', start_date='2020-01-01',
|
||||||
|
end_date='2024-12-31', use_cache=True)
|
||||||
|
|
||||||
|
# 获取验证数据 (2025)
|
||||||
|
df_val = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-01-01',
|
||||||
|
end_date='2025-12-31', use_cache=True)
|
||||||
|
|
||||||
|
print(f"✓ 训练数据: {len(df_train)} 行 ({df_train['timestamp'].min()} 至 {df_train['timestamp'].max()})")
|
||||||
|
print(f"✓ 验证数据: {len(df_val)} 行 ({df_val['timestamp'].min()} 至 {df_val['timestamp'].max()})")
|
||||||
|
|
||||||
|
# ========== 步骤2: 特征工程 ==========
|
||||||
|
print("\n[2/5] 特征工程...")
|
||||||
|
|
||||||
|
X_train, y_train, feature_cols = prepare_data(df_train)
|
||||||
|
X_val, y_val, _ = prepare_data(df_val)
|
||||||
|
|
||||||
|
print(f"✓ 特征数量: {len(feature_cols)}")
|
||||||
|
print(f"✓ 训练样本: {len(X_train)} (涨: {(y_train==1).sum()}, 跌: {(y_train==0).sum()})")
|
||||||
|
print(f"✓ 验证样本: {len(X_val)} (涨: {(y_val==1).sum()}, 跌: {(y_val==0).sum()})")
|
||||||
|
|
||||||
|
# 显示部分特征
|
||||||
|
print(f"\n特征列表 (前20个):")
|
||||||
|
for i, col in enumerate(feature_cols[:20], 1):
|
||||||
|
print(f" {i:2d}. {col}")
|
||||||
|
if len(feature_cols) > 20:
|
||||||
|
print(f" ... 还有 {len(feature_cols)-20} 个特征")
|
||||||
|
|
||||||
|
# ========== 步骤3: 训练CatBoost模型 ==========
|
||||||
|
print("\n[3/5] 训练CatBoost模型...")
|
||||||
|
|
||||||
|
# 创建CatBoost数据池
|
||||||
|
train_pool = Pool(X_train, y_train)
|
||||||
|
val_pool = Pool(X_val, y_val)
|
||||||
|
|
||||||
|
# 配置CatBoost参数
|
||||||
|
model = CatBoostClassifier(
|
||||||
|
iterations=1000,
|
||||||
|
learning_rate=0.05,
|
||||||
|
depth=6,
|
||||||
|
l2_leaf_reg=3,
|
||||||
|
loss_function='Logloss',
|
||||||
|
eval_metric='Accuracy',
|
||||||
|
random_seed=42,
|
||||||
|
verbose=100,
|
||||||
|
early_stopping_rounds=50,
|
||||||
|
task_type='CPU', # 如果有GPU可改为'GPU'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 训练模型
|
||||||
|
model.fit(
|
||||||
|
train_pool,
|
||||||
|
eval_set=val_pool,
|
||||||
|
use_best_model=True,
|
||||||
|
plot=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n✓ 训练完成")
|
||||||
|
print(f" 最佳迭代轮次: {model.get_best_iteration()}")
|
||||||
|
print(f" 最佳验证准确率: {model.get_best_score()['validation']['Accuracy']:.4f}")
|
||||||
|
|
||||||
|
# ========== 步骤4: 评估模型 ==========
|
||||||
|
print("\n[4/5] 评估模型性能...")
|
||||||
|
|
||||||
|
# 训练集预测
|
||||||
|
y_train_pred = model.predict(X_train)
|
||||||
|
train_acc = accuracy_score(y_train, y_train_pred)
|
||||||
|
|
||||||
|
# 验证集预测
|
||||||
|
y_val_pred = model.predict(X_val)
|
||||||
|
val_acc = accuracy_score(y_val, y_val_pred)
|
||||||
|
|
||||||
|
print(f"\n训练集性能:")
|
||||||
|
print(f" 准确率: {train_acc*100:.2f}%")
|
||||||
|
|
||||||
|
print(f"\n验证集性能:")
|
||||||
|
print(f" 准确率: {val_acc*100:.2f}%")
|
||||||
|
|
||||||
|
# 混淆矩阵
|
||||||
|
cm = confusion_matrix(y_val, y_val_pred)
|
||||||
|
print(f"\n混淆矩阵 (验证集):")
|
||||||
|
print(f" 预测↓ 预测↑")
|
||||||
|
print(f"真实↓ {cm[0,0]:4d} {cm[0,1]:4d}")
|
||||||
|
print(f"真实↑ {cm[1,0]:4d} {cm[1,1]:4d}")
|
||||||
|
|
||||||
|
# 分类报告
|
||||||
|
print(f"\n分类报告 (验证集):")
|
||||||
|
print(classification_report(y_val, y_val_pred, target_names=['下跌', '上涨']))
|
||||||
|
|
||||||
|
# 特征重要性 (Top 20)
|
||||||
|
feature_importance = model.get_feature_importance()
|
||||||
|
importance_df = pd.DataFrame({
|
||||||
|
'feature': feature_cols,
|
||||||
|
'importance': feature_importance
|
||||||
|
}).sort_values('importance', ascending=False)
|
||||||
|
|
||||||
|
print(f"\nTop 20 重要特征:")
|
||||||
|
for i, (idx, row) in enumerate(importance_df.head(20).iterrows(), 1):
|
||||||
|
print(f" {i:2d}. {row['feature']:<20s} {row['importance']:7.2f}")
|
||||||
|
|
||||||
|
# ========== 步骤5: 保存模型 ==========
|
||||||
|
print("\n[5/5] 保存模型...")
|
||||||
|
|
||||||
|
checkpoint_dir = './models/checkpoints/catboost_classification_2026'
|
||||||
|
os.makedirs(checkpoint_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 保存CatBoost模型
|
||||||
|
model_path = f'{checkpoint_dir}/catboost_model.cbm'
|
||||||
|
model.save_model(model_path, format='cbm')
|
||||||
|
|
||||||
|
# 保存特征列表和元数据
|
||||||
|
metadata = {
|
||||||
|
'feature_cols': feature_cols,
|
||||||
|
'train_acc': float(train_acc),
|
||||||
|
'val_acc': float(val_acc),
|
||||||
|
'best_iteration': int(model.get_best_iteration()),
|
||||||
|
'train_samples': len(X_train),
|
||||||
|
'val_samples': len(X_val),
|
||||||
|
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'train_date_range': f"{df_train['timestamp'].min()} - {df_train['timestamp'].max()}",
|
||||||
|
'val_date_range': f"{df_val['timestamp'].min()} - {df_val['timestamp'].max()}",
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(f'{checkpoint_dir}/metadata.pkl', 'wb') as f:
|
||||||
|
pickle.dump(metadata, f)
|
||||||
|
|
||||||
|
print(f"✓ 模型已保存到: {model_path}")
|
||||||
|
print(f"✓ 元数据已保存到: {checkpoint_dir}/metadata.pkl")
|
||||||
|
|
||||||
|
# ========== 总结 ==========
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ CatBoost二分类模型训练完成!")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print(f"\n模型性能总结:")
|
||||||
|
print(f" 训练集准确率: {train_acc*100:.2f}%")
|
||||||
|
print(f" 验证集准确率: {val_acc*100:.2f}%")
|
||||||
|
print(f" 特征数量: {len(feature_cols)}")
|
||||||
|
print(f" 最佳迭代: {model.get_best_iteration()}")
|
||||||
|
|
||||||
|
print(f"\n保存位置:")
|
||||||
|
print(f" {checkpoint_dir}/")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+283
@@ -0,0 +1,283 @@
|
|||||||
|
"""使用N-BEATS模型训练:预测未来1小时BTC价格"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pandas_ta as ta
|
||||||
|
from sklearn.preprocessing import StandardScaler
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
from torch.utils.data import DataLoader, TensorDataset
|
||||||
|
|
||||||
|
from data.fetcher import BinanceDataFetcher
|
||||||
|
from models.nbeats.model import NBeats, NBeatsConfig
|
||||||
|
import json
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
if not self.is_fitted:
|
||||||
|
raise ValueError("Must fit before transform")
|
||||||
|
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 train_nbeats():
|
||||||
|
print("="*80)
|
||||||
|
print("N-BEATS模型训练 - 预测未来1小时BTC价格")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# ========== 步骤1: 获取数据 ==========
|
||||||
|
print("\n[1/8] 获取BTC 2020-2025年数据...")
|
||||||
|
fetcher = BinanceDataFetcher()
|
||||||
|
df = fetcher.fetch_ohlcv(
|
||||||
|
symbol='BTC',
|
||||||
|
timeframe='1h',
|
||||||
|
start_date='2020-01-01',
|
||||||
|
end_date='2025-12-31',
|
||||||
|
use_cache=True
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✓ 数据量: {len(df):,} 行")
|
||||||
|
print(f"✓ 时间: {df['timestamp'].min()} 至 {df['timestamp'].max()}")
|
||||||
|
print(f"✓ 价格: ${df['close'].iloc[0]:,.2f} → ${df['close'].iloc[-1]:,.2f}")
|
||||||
|
|
||||||
|
# ========== 步骤2: 预处理 ==========
|
||||||
|
print("\n[2/8] 数据预处理...")
|
||||||
|
preprocessor = NBeatsPreprocessor(lookback=168, forecast_horizon=1)
|
||||||
|
X, y, processed_df = preprocessor.preprocess(df, fit=True)
|
||||||
|
|
||||||
|
print(f"✓ 序列数量: {len(X):,}")
|
||||||
|
print(f"✓ 输入形状: {X.shape}")
|
||||||
|
print(f"✓ 输出形状: {y.shape}")
|
||||||
|
|
||||||
|
# 还原真实价格看统计
|
||||||
|
y_real = preprocessor.inverse_transform_price(y)
|
||||||
|
print(f"\n✓ 目标价格统计:")
|
||||||
|
print(f" 最小值: ${y_real.min():,.2f}")
|
||||||
|
print(f" 最大值: ${y_real.max():,.2f}")
|
||||||
|
print(f" 均值: ${y_real.mean():,.2f}")
|
||||||
|
|
||||||
|
# ========== 步骤3: 划分数据集 ==========
|
||||||
|
print("\n[3/8] 划分数据集(90%训练 + 10%验证)...")
|
||||||
|
n = len(X)
|
||||||
|
train_end = int(n * 0.9)
|
||||||
|
|
||||||
|
X_train, y_train = X[:train_end], y[:train_end]
|
||||||
|
X_val, y_val = X[train_end:], y[train_end:]
|
||||||
|
|
||||||
|
print(f" 训练集: {len(X_train):,} 样本")
|
||||||
|
print(f" 验证集: {len(X_val):,} 样本")
|
||||||
|
|
||||||
|
# ========== 步骤4: 创建数据加载器 ==========
|
||||||
|
print("\n[4/8] 创建数据加载器...")
|
||||||
|
train_dataset = TensorDataset(
|
||||||
|
torch.FloatTensor(X_train),
|
||||||
|
torch.FloatTensor(y_train)
|
||||||
|
)
|
||||||
|
val_dataset = TensorDataset(
|
||||||
|
torch.FloatTensor(X_val),
|
||||||
|
torch.FloatTensor(y_val)
|
||||||
|
)
|
||||||
|
|
||||||
|
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
|
||||||
|
val_loader = DataLoader(val_dataset, batch_size=128, shuffle=False)
|
||||||
|
|
||||||
|
# ========== 步骤5: 创建N-BEATS模型 ==========
|
||||||
|
print("\n[5/8] 创建N-BEATS模型...")
|
||||||
|
config = NBeatsConfig(
|
||||||
|
input_size=168,
|
||||||
|
forecast_size=1,
|
||||||
|
stacks=['trend', 'seasonality', 'generic'],
|
||||||
|
num_blocks_per_stack=3,
|
||||||
|
block_layers=4,
|
||||||
|
layer_size=256,
|
||||||
|
trend_degree=2,
|
||||||
|
seasonality_harmonics=4
|
||||||
|
)
|
||||||
|
model = NBeats(config)
|
||||||
|
|
||||||
|
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||||
|
model = model.to(device)
|
||||||
|
|
||||||
|
print(f"✓ 模型参数: {model.count_parameters():,}")
|
||||||
|
print(f"✓ 设备: {device}")
|
||||||
|
print(f"✓ 模型架构: Trend + Seasonality + Generic")
|
||||||
|
|
||||||
|
# ========== 步骤6: 训练 ==========
|
||||||
|
print("\n[6/8] 开始训练...")
|
||||||
|
|
||||||
|
criterion = nn.MSELoss()
|
||||||
|
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
|
||||||
|
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
||||||
|
optimizer, mode='min', factor=0.5, patience=5, verbose=True
|
||||||
|
)
|
||||||
|
|
||||||
|
best_val_loss = float('inf')
|
||||||
|
patience = 15
|
||||||
|
patience_counter = 0
|
||||||
|
max_epochs = 50
|
||||||
|
|
||||||
|
for epoch in range(max_epochs):
|
||||||
|
# 训练
|
||||||
|
model.train()
|
||||||
|
train_loss = 0
|
||||||
|
for batch_X, batch_y in train_loader:
|
||||||
|
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
|
||||||
|
|
||||||
|
optimizer.zero_grad()
|
||||||
|
outputs = model(batch_X)
|
||||||
|
loss = criterion(outputs, batch_y)
|
||||||
|
loss.backward()
|
||||||
|
optimizer.step()
|
||||||
|
|
||||||
|
train_loss += loss.item()
|
||||||
|
|
||||||
|
train_loss /= len(train_loader)
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
model.eval()
|
||||||
|
val_loss = 0
|
||||||
|
with torch.no_grad():
|
||||||
|
for batch_X, batch_y in val_loader:
|
||||||
|
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
|
||||||
|
outputs = model(batch_X)
|
||||||
|
loss = criterion(outputs, batch_y)
|
||||||
|
val_loss += loss.item()
|
||||||
|
|
||||||
|
val_loss /= len(val_loader)
|
||||||
|
|
||||||
|
# 学习率调度
|
||||||
|
scheduler.step(val_loss)
|
||||||
|
|
||||||
|
print(f"Epoch {epoch+1}/{max_epochs}")
|
||||||
|
print(f" Train loss: {train_loss:.6f}")
|
||||||
|
print(f" Val loss: {val_loss:.6f}")
|
||||||
|
print(f" LR: {optimizer.param_groups[0]['lr']:.6f}")
|
||||||
|
|
||||||
|
# 保存最佳模型
|
||||||
|
if val_loss < best_val_loss:
|
||||||
|
best_val_loss = val_loss
|
||||||
|
patience_counter = 0
|
||||||
|
os.makedirs('./models/checkpoints/nbeats_btc_1h', exist_ok=True)
|
||||||
|
model.save('./models/checkpoints/nbeats_btc_1h/best_model.pt')
|
||||||
|
print(f" Saved best model (val_loss: {val_loss:.6f})")
|
||||||
|
else:
|
||||||
|
patience_counter += 1
|
||||||
|
|
||||||
|
# Early stopping
|
||||||
|
if patience_counter >= patience:
|
||||||
|
print(f"\nEarly stopping at epoch {epoch+1}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# ========== 步骤7: 评估 ==========
|
||||||
|
print("\n[7/8] 评估模型...")
|
||||||
|
|
||||||
|
# 重新加载最佳模型
|
||||||
|
checkpoint = torch.load('./models/checkpoints/nbeats_btc_1h/best_model.pt', map_location=device)
|
||||||
|
model = NBeats(checkpoint['config'])
|
||||||
|
model.load_state_dict(checkpoint['model_state_dict'])
|
||||||
|
model = model.to(device)
|
||||||
|
model.eval()
|
||||||
|
|
||||||
|
val_preds = []
|
||||||
|
with torch.no_grad():
|
||||||
|
for batch_X, _ in val_loader:
|
||||||
|
batch_X = batch_X.to(device)
|
||||||
|
pred = model(batch_X)
|
||||||
|
val_preds.extend(pred.cpu().numpy())
|
||||||
|
|
||||||
|
val_preds = np.array(val_preds)
|
||||||
|
|
||||||
|
# 还原真实价格
|
||||||
|
pred_prices = preprocessor.inverse_transform_price(val_preds)
|
||||||
|
true_prices = preprocessor.inverse_transform_price(y_val)
|
||||||
|
|
||||||
|
# 当前价格
|
||||||
|
val_start_idx = train_end + 168
|
||||||
|
current_prices = processed_df['close'].iloc[val_start_idx:val_start_idx+len(pred_prices)].values
|
||||||
|
|
||||||
|
# 方向准确率
|
||||||
|
pred_direction = (pred_prices > current_prices).astype(int)
|
||||||
|
true_direction = (true_prices > current_prices).astype(int)
|
||||||
|
direction_acc = (pred_direction == true_direction).mean()
|
||||||
|
|
||||||
|
# 价格预测误差
|
||||||
|
price_mae = np.abs(pred_prices - true_prices).mean()
|
||||||
|
price_rmse = np.sqrt(((pred_prices - true_prices) ** 2).mean())
|
||||||
|
price_mape = np.mean(np.abs((true_prices - pred_prices) / true_prices)) * 100
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("训练完成 - N-BEATS模型性能")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
print(f"\n📊 价格预测误差:")
|
||||||
|
print(f" MAE: ${price_mae:,.2f}")
|
||||||
|
print(f" RMSE: ${price_rmse:,.2f}")
|
||||||
|
print(f" MAPE: {price_mape:.2f}%")
|
||||||
|
|
||||||
|
print(f"\n📈 方向预测:")
|
||||||
|
print(f" 方向准确率: {direction_acc*100:.2f}%")
|
||||||
|
|
||||||
|
# ========== 步骤8: 保存 ==========
|
||||||
|
print("\n[8/8] 保存模型和预处理器...")
|
||||||
|
|
||||||
|
with open('./models/checkpoints/nbeats_btc_1h/preprocessor.pkl', 'wb') as f:
|
||||||
|
pickle.dump(preprocessor, f)
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
'price_mae': float(price_mae),
|
||||||
|
'price_rmse': float(price_rmse),
|
||||||
|
'price_mape': float(price_mape),
|
||||||
|
'direction_accuracy': float(direction_acc),
|
||||||
|
'best_val_loss': float(best_val_loss)
|
||||||
|
}
|
||||||
|
with open('./models/checkpoints/nbeats_btc_1h/metrics.json', 'w') as f:
|
||||||
|
json.dump(metrics, f, indent=2)
|
||||||
|
|
||||||
|
print(f"\n✓ 模型保存至: ./models/checkpoints/nbeats_btc_1h/")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
train_nbeats()
|
||||||
Reference in new issue
Block a user