From c0a02868ac52288b65986d195d1c7eed94fb433e Mon Sep 17 00:00:00 2001 From: crypto_predictor Date: Wed, 11 Feb 2026 22:07:32 +0800 Subject: [PATCH] feat: refactor model inference architecture - Unified inference logic into models/inference.py - Added InferenceManager for handling DLinear, N-BEATS, and TTM-R2 models - Updated prediction_service.py and auto_trade_polymarket.py to use the new manager - Fixed dependency issues (torch, transformers) - Resolved logger duplication issue - Verified with test_models_feb_2026.py --- analyze_low_accuracy_prev_day.py | 459 ----------------- api/prediction_service.py | 317 +++--------- daily_accuracy_analysis.py | 460 ----------------- dlinear_prob_distribution.py | 406 --------------- ensemble_daily_accuracy_multi_periods.py | 395 --------------- ensemble_five_models.py | 454 ----------------- ensemble_four_models.py | 385 -------------- models/inference.py | 417 +++++++++++++++ save_all_results.py | 458 ----------------- scripts/patch_trader.py | 91 ++++ smart_rest_strategy.py | 451 ----------------- start_system.py | 2 +- test_dlinear_jan_2026_create_labels.py | 305 ----------- .../auto_trade_polymarket.py | 475 ++++++++---------- 14 files changed, 791 insertions(+), 4284 deletions(-) delete mode 100644 analyze_low_accuracy_prev_day.py delete mode 100644 daily_accuracy_analysis.py delete mode 100644 dlinear_prob_distribution.py delete mode 100644 ensemble_daily_accuracy_multi_periods.py delete mode 100644 ensemble_five_models.py delete mode 100644 ensemble_four_models.py create mode 100644 models/inference.py delete mode 100644 save_all_results.py create mode 100644 scripts/patch_trader.py delete mode 100644 smart_rest_strategy.py delete mode 100644 test_dlinear_jan_2026_create_labels.py rename auto_trade_polymarket.py => trader/auto_trade_polymarket.py (73%) diff --git a/analyze_low_accuracy_prev_day.py b/analyze_low_accuracy_prev_day.py deleted file mode 100644 index a96d9da..0000000 --- a/analyze_low_accuracy_prev_day.py +++ /dev/null @@ -1,459 +0,0 @@ -"""分析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() diff --git a/api/prediction_service.py b/api/prediction_service.py index 77b63ba..24b2807 100644 --- a/api/prediction_service.py +++ b/api/prediction_service.py @@ -1,153 +1,42 @@ """ -预测服务 - DLinear 和 TTM-R2 模型预测 +预测服务 - 统一模型预测 (DLinear, N-BEATS, TTM-R2) """ import os import sys -import pickle +import threading from datetime import datetime, timedelta from typing import Optional, List, Dict, Tuple from zoneinfo import ZoneInfo -import torch -import numpy as np import pandas as pd -import pandas_ta as ta -from sklearn.preprocessing import StandardScaler +import numpy as np # 添加项目路径 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from data.fetcher import BinanceDataFetcher from api.models import PredictionModel - - -class ClassificationPreprocessor: - """DLinear分类预处理器""" - - def __init__(self, lookback: int = 168, forecast_horizon: int = 1): - self.lookback = lookback - self.forecast_horizon = forecast_horizon - self.scaler = StandardScaler() - self.is_fitted = False - - def add_features(self, df: pd.DataFrame) -> pd.DataFrame: - 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: pd.DataFrame) -> pd.DataFrame: - 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: pd.DataFrame, fit: bool = 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 CustomUnpickler(pickle.Unpickler): - """自定义反序列化器,用于处理 __main__ 模块的类""" - def find_class(self, module, name): - if module == "__main__": - if name == "ClassificationPreprocessor": - return ClassificationPreprocessor - return super().find_class(module, name) +from models.inference import get_inference_manager class PredictionService: - """预测服务 - 管理 DLinear 和 TTM-R2 模型""" + """预测服务 - 管理多模型预测""" # 类级别的锁,确保多个请求不会同时生成预测 - _prediction_lock = None + _prediction_lock = threading.Lock() def __init__(self): - import threading - import platform - # 初始化锁 - if PredictionService._prediction_lock is None: - PredictionService._prediction_lock = threading.Lock() - - # 设备选择:macOS 用 MPS,Windows/Linux 用 CPU - if platform.system() == 'Darwin': # macOS - self.device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu') - else: # Windows / Linux - self.device = torch.device('cpu') - print(f"使用设备: {self.device}") - self.et_tz = ZoneInfo('America/New_York') self.fetcher = BinanceDataFetcher() - - # 模型和预处理器 - self.dlinear_model = None - self.dlinear_preprocessor = None - self.ttm_model = None + self.inference_manager = get_inference_manager() # 记录正在处理的日期和月份,避免重复处理 self._processing_dates = set() self._processing_months = set() - self._load_models() - - def _load_models(self): - """加载模型""" - print("正在加载预测模型...") - - # 加载 DLinear - from models.dlinear.model import DLinear - - dlinear_path = './models/checkpoints/dlinear_classification_2026/best_model.pt' - preprocessor_path = './models/checkpoints/dlinear_classification_2026/preprocessor.pkl' - - checkpoint = torch.load(dlinear_path, map_location=self.device, weights_only=False) - self.dlinear_model = DLinear(checkpoint['config']) - self.dlinear_model.load_state_dict(checkpoint['model_state_dict']) - self.dlinear_model = self.dlinear_model.to(self.device).eval() - - with open(preprocessor_path, 'rb') as f: - try: - self.dlinear_preprocessor = pickle.load(f) - except AttributeError: - f.seek(0) - self.dlinear_preprocessor = CustomUnpickler(f).load() - - print(f" ✓ DLinear 已加载") - - # 加载 TTM-R2 - from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction - - self.ttm_model = TinyTimeMixerForPrediction.from_pretrained( - "ibm-granite/granite-timeseries-ttm-r2", - trust_remote_code=True - ) - self.ttm_model = self.ttm_model.to(self.device).eval() - - print(f" ✓ TTM-R2 已加载") - print(f" 设备: {self.device}") + # 预加载模型 + print("正在预加载预测模型...") + self.inference_manager.load_all_models() def _get_btc_data(self, days: int = 30) -> pd.DataFrame: """获取BTC历史数据""" @@ -164,83 +53,22 @@ class PredictionService: return df def predict_dlinear(self, df: pd.DataFrame) -> Tuple[int, float]: - """ - 使用 DLinear 预测下一小时的涨跌 - - 输入: df 包含历史K线数据 (最后一行是最新的已完成K线) - 输出: 预测下一小时相对于 df 最后一行收盘价的涨跌 - - Returns: - (prediction, probability): 预测结果(0=跌, 1=涨)和概率 - """ - # 预处理: 使用全部数据,不去掉最后一行 - X, _, _ = self.dlinear_preprocessor.preprocess(df, fit=False) - - if len(X) == 0: + """DLinear 预测代理""" + pred, prob = self.inference_manager.predict_dlinear(df) + if pred is None: return 0, 0.5 - - # 取最后一个窗口进行预测 - X_last = torch.FloatTensor(X[-1:]).to(self.device) - - with torch.no_grad(): - logits = self.dlinear_model(X_last).cpu().numpy() - prob = 1 / (1 + np.exp(-logits)) - prediction = int(prob.item() > 0.5) - probability = float(prob.item()) - - return prediction, probability + return pred, prob if prob is not None else 0.5 def predict_ttm(self, df: pd.DataFrame) -> Tuple[int, float]: - """ - 使用 TTM-R2 预测下一小时的涨跌 - - 输入: df 包含历史K线数据 (最后一行是最新的已完成K线) - 输出: 预测下一小时相对于 df 最后一行收盘价的涨跌 - - Returns: - (prediction, probability): 预测结果(0=跌, 1=涨)和概率 - """ - context_length = 512 - all_prices = df['close'].values - - # 使用最后 context_length 个价格 - if len(all_prices) >= context_length: - context_data = all_prices[-context_length:].reshape(1, -1, 1) - else: - # 数据不足,用最后一个价格填充 - padded = np.full(context_length, all_prices[-1]) - padded[-len(all_prices):] = all_prices - context_data = padded.reshape(1, -1, 1) - - # 基准价格: 最后一行的收盘价 (即预测目标小时的前一小时收盘价) - base_price = float(df['close'].iloc[-1]) - - context_tensor = torch.FloatTensor(context_data).to(self.device) - - with torch.no_grad(): - output = self.ttm_model(context_tensor) - pred_price = output.prediction_outputs.cpu().numpy()[0, 0, 0] - - # 判断涨跌: 预测价格 vs 基准价格 - prediction = 1 if pred_price > base_price else 0 - - # 计算伪概率: 基于价格变化百分比 - price_pct = (pred_price - base_price) / base_price - # 将价格变化百分比映射到概率 (sigmoid) - probability = 1 / (1 + np.exp(-price_pct * 100)) - - return prediction, float(probability) - + """TTM-R2 预测代理""" + pred, prob = self.inference_manager.predict_ttm(df) + if pred is None: + return 0, 0.5 + return pred, prob if prob is not None else 0.5 + def predict_current_hour(self) -> Dict: """ - 预测当前小时的涨跌 - - 逻辑: - - 获取最新的K线数据 (最后一行是最新已完成的K线) - - 预测下一小时相对于最后一行收盘价的涨跌 - - Returns: - 预测结果字典 + 预测当前小时的涨跌 (多模型集成) """ df = self._get_btc_data() @@ -254,7 +82,6 @@ class PredictionService: last_close = float(df['close'].iloc[-1]) # 当前正在形成的K线信息 - current_open = float(df['open'].iloc[-1]) current_price = float(df['close'].iloc[-1]) # 当前价格 results = { @@ -266,28 +93,45 @@ class PredictionService: "models": {} } - # DLinear 预测 (预测下一小时相对于 last_close 的涨跌) - dlinear_pred, dlinear_prob = self.predict_dlinear(df) - results["models"]["DLinear"] = { - "prediction": dlinear_pred, - "probability": round(dlinear_prob, 4), - "direction": "UP" if dlinear_pred == 1 else "DOWN" - } + # 使用 InferenceManager 获取所有预测 + predictions = self.inference_manager.predict_all(df) - # TTM-R2 预测 - ttm_pred, ttm_prob = self.predict_ttm(df) - results["models"]["TTM-R2"] = { - "prediction": ttm_pred, - "probability": round(ttm_prob, 4), - "direction": "UP" if ttm_pred == 1 else "DOWN" - } + # 填充结果 + if 'dlinear' in predictions: + results["models"]["DLinear"] = { + "prediction": predictions['dlinear']['prediction'], + "probability": round(predictions['dlinear']['probability'], 4), + "direction": "UP" if predictions['dlinear']['prediction'] == 1 else "DOWN" + } + + if 'nbeats' in predictions: + results["models"]["N-BEATS"] = { + "prediction": predictions['nbeats']['prediction'], + "probability": round(predictions['nbeats']['probability'], 4), + "direction": "UP" if predictions['nbeats']['prediction'] == 1 else "DOWN" + } + + if 'ttm' in predictions: + results["models"]["TTM-R2"] = { + "prediction": predictions['ttm']['prediction'], + "probability": round(predictions['ttm']['probability'], 4), + "direction": "UP" if predictions['ttm']['prediction'] == 1 else "DOWN" + } + + if 'ensemble' in predictions and predictions['ensemble']: + ens_pred = predictions['ensemble']['prediction'] + results["models"]["Ensemble"] = { + "prediction": ens_pred, + "probability": 0.0, # 集成暂无概率 + "direction": "UP" if ens_pred == 1 else "DOWN", + "votes": predictions['ensemble']['votes'] + } return results def ensure_predictions_for_date(self, date: str) -> None: """ 确保指定日期有预测数据,如果没有则生成 - Args: date: 日期 (YYYY-MM-DD) """ @@ -301,9 +145,15 @@ class PredictionService: self._processing_dates.add(date) try: - # 检查是否有缺失的小时 - for model_name in ["DLinear", "TTM-R2"]: - missing_hours = PredictionModel.get_missing_hours(date, model_name) + # 检查是否有缺失的小时 (现在只检查 DLinear 和 TTM-R2 作为代表,或者扩展到 N-BEATS) + # 为了保持兼容性,还是主要检查这两个,或者可以增加 N-BEATS + models_to_check = ["DLinear", "TTM-R2", "N-BEATS"] + + for model_name in models_to_check: + # 数据库中模型名字对应 + db_model_name = model_name + + missing_hours = PredictionModel.get_missing_hours(date, db_model_name) if not missing_hours: continue @@ -348,18 +198,6 @@ class PredictionService: hour: int, model_name: str) -> None: """ 为指定小时生成预测 - - 预测逻辑: - - 预测目标: {date} {hour}:00 的K线收盘价涨跌 - - 输入数据: {hour-1}:00 及以前的K线数据 (不包含目标小时) - - 涨跌判断: 预测价格 vs 前一小时收盘价 (即 {hour-1}:00 的 close) - - 正确率验证: 目标小时的 close vs 前一小时的 close - - Args: - df: 历史K线数据 (UTC时间戳) - date: 目标日期 (美东时间 YYYY-MM-DD) - hour: 目标小时 (美东时间 0-23) - model_name: 模型名称 """ try: # 1. 构建目标时间 (美东时间 -> UTC) @@ -374,7 +212,6 @@ class PredictionService: # 3. 找到目标小时的K线 (用于验证) target_mask = df['timestamp_dt'] == target_time_utc if not target_mask.any(): - # 目标小时数据不存在,跳过 return target_row = df[target_mask].iloc[0] @@ -385,7 +222,6 @@ class PredictionService: prev_time_utc = target_time_utc - pd.Timedelta(hours=1) prev_mask = df['timestamp_dt'] == prev_time_utc if not prev_mask.any(): - # 前一小时数据不存在,跳过 return prev_row = df[prev_mask].iloc[0] @@ -396,16 +232,23 @@ class PredictionService: df_input = df[input_mask].copy() if len(df_input) < 200: - # 数据不足,跳过 return # 6. 调用模型预测 - if model_name == "DLinear": - prediction, probability = self.predict_dlinear(df_input) - else: # TTM-R2 - prediction, probability = self.predict_ttm(df_input) + prediction = None + probability = 0.5 - # 7. 判断实际涨跌 (目标小时收盘价 vs 前一小时收盘价) + if model_name == "DLinear": + prediction, probability = self.inference_manager.predict_dlinear(df_input) + elif model_name == "TTM-R2": + prediction, probability = self.inference_manager.predict_ttm(df_input) + elif model_name == "N-BEATS": + prediction, probability = self.inference_manager.predict_nbeats(df_input) + + if prediction is None: + return + + # 7. 判断实际涨跌 actual_result = 1 if target_close > prev_close else 0 # 8. 判断预测是否正确 @@ -434,39 +277,29 @@ class PredictionService: def ensure_predictions_for_month(self, month: str) -> None: """ 确保指定月份有预测数据 - Args: month: 月份 (YYYY-MM) """ # 使用锁避免并发冲突 with PredictionService._prediction_lock: - # 检查是否正在处理该月份 if month in self._processing_months: return - - # 标记正在处理 self._processing_months.add(month) try: - # 解析月份 year, mon = map(int, month.split('-')) - - # 计算该月的日期范围 from calendar import monthrange _, last_day = monthrange(year, mon) - # 对于过去的日期,确保有预测数据 today = datetime.now(self.et_tz).date() for day in range(1, last_day + 1): date = f"{year}-{mon:02d}-{day:02d}" date_obj = datetime.strptime(date, '%Y-%m-%d').date() - # 只处理过去和今天的日期 if date_obj <= today: self.ensure_predictions_for_date(date) finally: - # 移除处理标记 with PredictionService._prediction_lock: self._processing_months.discard(month) diff --git a/daily_accuracy_analysis.py b/daily_accuracy_analysis.py deleted file mode 100644 index 88ed88e..0000000 --- a/daily_accuracy_analysis.py +++ /dev/null @@ -1,460 +0,0 @@ -"""按天统计双模型和三模型的准确率""" - -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() diff --git a/dlinear_prob_distribution.py b/dlinear_prob_distribution.py deleted file mode 100644 index 60f3e35..0000000 --- a/dlinear_prob_distribution.py +++ /dev/null @@ -1,406 +0,0 @@ -"""分析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() diff --git a/ensemble_daily_accuracy_multi_periods.py b/ensemble_daily_accuracy_multi_periods.py deleted file mode 100644 index 9e80c64..0000000 --- a/ensemble_daily_accuracy_multi_periods.py +++ /dev/null @@ -1,395 +0,0 @@ -"""四模型集成每日准确率分析 - 多时间段测试(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() diff --git a/ensemble_five_models.py b/ensemble_five_models.py deleted file mode 100644 index 3d8e325..0000000 --- a/ensemble_five_models.py +++ /dev/null @@ -1,454 +0,0 @@ -"""五模型集成预测: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() diff --git a/ensemble_four_models.py b/ensemble_four_models.py deleted file mode 100644 index 34c98ba..0000000 --- a/ensemble_four_models.py +++ /dev/null @@ -1,385 +0,0 @@ -"""四模型集成预测: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() diff --git a/models/inference.py b/models/inference.py new file mode 100644 index 0000000..d73f283 --- /dev/null +++ b/models/inference.py @@ -0,0 +1,417 @@ +""" +模型推理服务 - 统一管理 DLinear, N-BEATS, TTM-R2 模型加载与预测 +""" +import os +import sys +import pickle +import torch +import numpy as np +import pandas as pd +import pandas_ta as ta +from typing import Dict, List, Optional, Tuple, Union, Any +from sklearn.preprocessing import StandardScaler + +# 添加项目根目录到 sys.path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from models.dlinear.model import DLinear +from models.nbeats.model import NBeats +try: + from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction +except (ImportError, NameError): + # 尝试作为可选依赖处理 + TinyTimeMixerForPrediction = None + + + +class CustomUnpickler(pickle.Unpickler): + """自定义反序列化器,用于处理 __main__ 模块的类重定向""" + def find_class(self, module, name): + # 将 __main__ 中的类重定向到当前模块 + if module == "__main__": + if name == "ClassificationPreprocessor": + return ClassificationPreprocessor + if name == "NBeatsPreprocessor": + return NBeatsPreprocessor + return super().find_class(module, name) + + +class ClassificationPreprocessor: + """DLinear分类预处理器""" + + def __init__(self, lookback: int = 168, forecast_horizon: int = 1): + self.lookback = lookback + self.forecast_horizon = forecast_horizon + self.scaler = StandardScaler() + self.is_fitted = False + self.feature_columns = ['open', 'high', 'low', 'close', 'volume', + 'rsi', 'macd', 'macd_hist', 'macd_signal'] + + def add_features(self, df: pd.DataFrame) -> pd.DataFrame: + 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: pd.DataFrame) -> pd.DataFrame: + 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: pd.DataFrame, fit: bool = True): + df = self.add_features(df) + df = self.create_labels(df) + df = df.dropna().reset_index(drop=True) + + if fit: + scaled_features = self.scaler.fit_transform(df[self.feature_columns]) + self.is_fitted = True + else: + scaled_features = self.scaler.transform(df[self.feature_columns]) + + 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 preprocess_for_inference(self, df: pd.DataFrame): + """推理专用预处理:整个df一次性处理,不生成标签""" + df = self.add_features(df) + df = df.dropna().reset_index(drop=True) + + # 确保有足够的数据 + if len(df) == 0: + return np.array([]), df, [] + + scaled_features = self.scaler.transform(df[self.feature_columns]) + + X = [] + valid_indices = [] + + if len(scaled_features) < self.lookback: + return np.array([]), df, [] + + # 注意:这里生成的窗口数量应该是 len(scaled) - lookback + 1 + # 例如 len=168, lookback=168 -> range(1) -> i=0 -> [0:168] (1个窗口) + for i in range(len(scaled_features) - self.lookback + 1): + window = scaled_features[i:i + self.lookback] + X.append(window) + valid_indices.append(i + self.lookback - 1) + + return np.array(X), df, valid_indices + + +class NBeatsPreprocessor: + """N-BEATS预处理器""" + def __init__(self, lookback: int = 168, forecast_horizon: int = 1): + self.lookback = lookback + self.forecast_horizon = forecast_horizon + self.price_scaler = StandardScaler() + self.is_fitted = False + + def preprocess(self, df: pd.DataFrame, fit: bool = True): + df = df.copy() + prices = df['close'].values.reshape(-1, 1) + + if fit: + scaled_prices = self.price_scaler.fit_transform(prices) + self.is_fitted = True + else: + scaled_prices = self.price_scaler.transform(prices) + + X = [] + valid_indices = [] + + if len(scaled_prices) < self.lookback: + return np.array([]), df, [] + + for i in range(len(scaled_prices) - self.lookback + 1): + X.append(scaled_prices[i:i + self.lookback, 0]) + valid_indices.append(i + self.lookback - 1) + + return np.array(X), df, valid_indices + + +class InferenceManager: + """统一模型推理管理器""" + + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(InferenceManager, cls).__new__(cls) + cls._instance.initialized = False + return cls._instance + + def __init__(self, device_str: str = None): + if self.initialized: + return + + import platform + if device_str: + self.device = torch.device(device_str) + elif platform.system() == 'Darwin' and torch.backends.mps.is_available(): + self.device = torch.device('mps') + else: + self.device = torch.device('cpu') + + print(f"InferenceManager 使用设备: {self.device}") + + self.models = {} + self.preprocessors = {} + + # 配置路径 + self.configs = { + 'dlinear': { + 'model_path': './models/checkpoints/dlinear_classification_2026/best_model.pt', + 'preprocessor_path': './models/checkpoints/dlinear_classification_2026/preprocessor.pkl' + }, + 'nbeats': { + 'model_path': './models/checkpoints/nbeats_btc_1h/best_model.pt', + 'preprocessor_path': './models/checkpoints/nbeats_btc_1h/preprocessor.pkl' + }, + 'ttm': { + 'model_name': 'ibm-granite/granite-timeseries-ttm-r2' + } + } + self.initialized = True + + def load_model(self, model_name: str): + """加载指定模型""" + if model_name in self.models: + return + + print(f"正在加载 {model_name}...") + try: + if model_name == 'dlinear': + self._load_dlinear() + elif model_name == 'nbeats': + self._load_nbeats() + elif model_name == 'ttm': + self._load_ttm() + else: + print(f"未知模型: {model_name}") + except Exception as e: + print(f"加载 {model_name} 失败: {e}") + import traceback + traceback.print_exc() + + def load_all_models(self): + """加载所有模型""" + self.load_model('dlinear') + self.load_model('nbeats') + self.load_model('ttm') + + def _load_dlinear(self): + config = self.configs['dlinear'] + if not os.path.exists(config['model_path']): + print(f"DLinear 模型文件不存在: {config['model_path']}") + return + + checkpoint = torch.load(config['model_path'], map_location=self.device, weights_only=False) + model = DLinear(checkpoint['config']) + model.load_state_dict(checkpoint['model_state_dict']) + self.models['dlinear'] = model.to(self.device).eval() + + with open(config['preprocessor_path'], 'rb') as f: + try: + self.preprocessors['dlinear'] = pickle.load(f) + except AttributeError: + f.seek(0) + self.preprocessors['dlinear'] = CustomUnpickler(f).load() + print(" ✓ DLinear 已加载") + + def _load_nbeats(self): + config = self.configs['nbeats'] + if not os.path.exists(config['model_path']): + print(f"N-BEATS 模型文件不存在: {config['model_path']}") + return + + checkpoint = torch.load(config['model_path'], map_location=self.device, weights_only=False) + model = NBeats(checkpoint['config']) + self.models['nbeats'] = model.to(self.device).eval() + with open(config['preprocessor_path'], 'rb') as f: + try: + self.preprocessors['nbeats'] = pickle.load(f) + except AttributeError: + f.seek(0) + self.preprocessors['nbeats'] = CustomUnpickler(f).load() + print(" ✓ N-BEATS 已加载") + + def _load_ttm(self): + # 检查 TTM 类是否已导入 + if TinyTimeMixerForPrediction is None: + print("TTM-R2 模型类未定义 (可能是依赖库缺失)") + return + + config = self.configs['ttm'] + try: + model = TinyTimeMixerForPrediction.from_pretrained( + config['model_name'], + trust_remote_code=True + ) + self.models['ttm'] = model.to(self.device).eval() + print(" ✓ TTM-R2 已加载") + except Exception as e: + print(f"TTM 加载失败: {e}") + + def predict_dlinear(self, df: pd.DataFrame) -> Tuple[Optional[int], Optional[float]]: + """ + DLinear预测 (输入df包含历史数据,预测下一时刻) + """ + if 'dlinear' not in self.models: + self.load_model('dlinear') + if 'dlinear' not in self.models: + return None, None + + model = self.models['dlinear'] + preprocessor = self.preprocessors['dlinear'] + + # 预处理 + X, _, _ = preprocessor.preprocess_for_inference(df) + + if len(X) == 0: + return None, None + + # 取最后一个窗口 + X_last = torch.FloatTensor(X[-1:]).to(self.device) + + with torch.no_grad(): + logits = model(X_last).cpu().numpy() + prob = 1 / (1 + np.exp(-logits)) + prediction = int(prob.item() > 0.5) + probability = float(prob.item()) + + return prediction, probability + + def predict_nbeats(self, df: pd.DataFrame) -> Tuple[Optional[int], Optional[float]]: + """ + N-BEATS预测 + """ + if 'nbeats' not in self.models: + self.load_model('nbeats') + if 'nbeats' not in self.models: + return None, None + + model = self.models['nbeats'] + preprocessor = self.preprocessors['nbeats'] + + # 预处理 + X, _, _ = preprocessor.preprocess(df, fit=False) + + if len(X) == 0: + return None, None + + # 取最后一个窗口 + X_last = torch.FloatTensor(X[-1:]).to(self.device) + current_price_scaled = X[-1, -1] # 窗口中最后一个点的缩放价格 + + with torch.no_grad(): + output = model(X_last).cpu().numpy() + # output shape: (batch, forecast_horizon) or (batch, 1) + if output.ndim == 1: + pred_price_scaled = output[0] + else: + pred_price_scaled = output[0, 0] + + prediction = 1 if pred_price_scaled > current_price_scaled else 0 + + # N-BEATS 回归模型没有直接概率 + probability = 0.6 if prediction == 1 else 0.4 # 占位 + + return prediction, probability + + def predict_ttm(self, df: pd.DataFrame) -> Tuple[Optional[int], Optional[float]]: + """ + TTM-R2预测 + """ + if 'ttm' not in self.models: + self.load_model('ttm') + if 'ttm' not in self.models: + return None, None + + model = self.models['ttm'] + context_length = 512 + + all_prices = df['close'].values + if len(all_prices) == 0: + return None, None + + # TTM 上下文准备 + if len(all_prices) >= context_length: + context_data = all_prices[-context_length:].reshape(1, -1, 1) + else: + padded = np.full(context_length, all_prices[-1]) + padded[-len(all_prices):] = all_prices + context_data = padded.reshape(1, -1, 1) + + context_tensor = torch.FloatTensor(context_data).to(self.device) + base_price = float(df['close'].iloc[-1]) + + with torch.no_grad(): + output = model(context_tensor) + pred_price = output.prediction_outputs.cpu().numpy()[0, 0, 0] + + prediction = 1 if pred_price > base_price else 0 + + # 简单的概率模拟 + price_pct = (pred_price - base_price) / base_price + probability = 1 / (1 + np.exp(-price_pct * 100)) + + return prediction, float(probability) + + def predict_all(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + 使用所有加载的模型进行预测 + """ + results = {} + + # DLinear + dl_pred, dl_prob = self.predict_dlinear(df) + if dl_pred is not None: + results['dlinear'] = {'prediction': dl_pred, 'probability': dl_prob} + + # N-BEATS + nb_pred, nb_prob = self.predict_nbeats(df) + if nb_pred is not None: + results['nbeats'] = {'prediction': nb_pred, 'probability': nb_prob} + + # TTM + ttm_pred, ttm_prob = self.predict_ttm(df) + if ttm_pred is not None: + results['ttm'] = {'prediction': ttm_pred, 'probability': ttm_prob} + + # 集成 (投票) + votes = [] + if dl_pred is not None: votes.append(dl_pred) + if nb_pred is not None: votes.append(nb_pred) + if ttm_pred is not None: votes.append(ttm_pred) + + if votes: + # 简单投票: > 50% 胜出 + vote_sum = sum(votes) + if vote_sum > len(votes) / 2: + ensemble_pred = 1 + elif vote_sum < len(votes) / 2: + ensemble_pred = 0 + else: + # 平票时,如果有 DLinear,优先 DLinear,否则 0 (保守) + ensemble_pred = dl_pred if dl_pred is not None else 0 + + results['ensemble'] = {'prediction': ensemble_pred, 'votes': votes} + else: + results['ensemble'] = None + + return results + +def get_inference_manager() -> InferenceManager: + """获取单例""" + return InferenceManager() diff --git a/save_all_results.py b/save_all_results.py deleted file mode 100644 index a59cbd2..0000000 --- a/save_all_results.py +++ /dev/null @@ -1,458 +0,0 @@ -"""保存所有测试结果到项目 - 生成完整报告""" - -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() diff --git a/scripts/patch_trader.py b/scripts/patch_trader.py new file mode 100644 index 0000000..41f0040 --- /dev/null +++ b/scripts/patch_trader.py @@ -0,0 +1,91 @@ +import os + +file_path = 'trader/auto_trade_polymarket.py' + +with open(file_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + +new_lines = [] +inserted = False + +# Code to insert +insert_code = """ + # === 新增: 保存初始预测和状态 === + try: + # 确定最终预测 (用于日志) + final_pred = 0 + if 'ensemble' in all_preds and all_preds['ensemble']: + final_pred = all_preds['ensemble']['prediction'] + elif 'dlinear' in all_preds: + final_pred = all_preds['dlinear']['prediction'] + + et_tz = ZoneInfo('America/New_York') + current_time_utc = pd.Timestamp(current_time) + current_time_et = current_time_utc.tz_localize('UTC').tz_convert(et_tz) + + # 1. 保存预测到 model_predictions + common_data = { + "timestamp": current_time_utc.isoformat(), + "timestamp_et": current_time_et.isoformat(), + "date": current_time_et.strftime('%Y-%m-%d'), + "hour": current_time_et.hour, + "actual_result": None, + "is_correct": None, + "open_price": float(df['open'].iloc[-1]), + "close_price": None, + } + + def save_single_pred(name, pred_res): + data = common_data.copy() + data['model_name'] = name + data['prediction'] = pred_res['prediction'] + data['probability'] = pred_res.get('probability', 0.5) + PredictionModel.save_prediction(data) + + if 'dlinear' in all_preds: save_single_pred("DLinear", all_preds['dlinear']) + if 'nbeats' in all_preds: save_single_pred("N-BEATS", all_preds['nbeats']) + if 'ttm' in all_preds: save_single_pred("TTM-R2", all_preds['ttm']) + if 'ensemble' in all_preds and all_preds['ensemble']: save_single_pred("Ensemble", all_preds['ensemble']) + + logger.info(f"💾 [Init] 初始预测已保存到数据库") + + # 2. 记录当前监控状态到 trades (作为 'in_progress') + status = 'in_progress' + market_title = self.current_market['title'] if self.current_market else "Waiting for Market..." + market_slug = self.current_market['slug'] if self.current_market else None + + log_data = { + 'timestamp': datetime.now().isoformat(), + 'market_title': market_title, + 'market_slug': market_slug, + 'model_name': 'Ensemble', + 'prediction': 'UP' if final_pred == 1 else 'DOWN', + 'probability': 0.0, + 'action': 'MONITOR', + 'price': 0, + 'amount': 0, + 'size': 0, + 'status': status, + 'error_message': 'System Started - Monitoring', + 'raw_data': {} + } + self.db.log_trade(log_data) + logger.info(f"💾 [Init] 初始监控状态已记录") + + except Exception as e: + logger.error(f"保存初始状态失败: {e}") + import traceback + traceback.print_exc() + +""" + +for line in lines: + if "def wait_for_next_hour(self)" in line and not inserted: + new_lines.append(insert_code) + inserted = True + new_lines.append(line) + +with open(file_path, 'w', encoding='utf-8') as f: + f.writelines(new_lines) + +print("Successfully patched trader/auto_trade_polymarket.py") diff --git a/smart_rest_strategy.py b/smart_rest_strategy.py deleted file mode 100644 index 620ff86..0000000 --- a/smart_rest_strategy.py +++ /dev/null @@ -1,451 +0,0 @@ -"""智能休息策略回测:基于历史准确率动态决定是否交易""" - -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() diff --git a/start_system.py b/start_system.py index 88c8bd6..19ec665 100755 --- a/start_system.py +++ b/start_system.py @@ -106,7 +106,7 @@ def main(): # 2. 启动自动交易程序 # 使用 -u 参数禁用输出缓冲 - trade_cmd = f"{python_exec} -u auto_trade_polymarket.py" + trade_cmd = f"{python_exec} -u trader/auto_trade_polymarket.py" trade_process = start_process(trade_cmd, "Polymarket 自动交易程序") log("系统启动完成!", GREEN) diff --git a/test_dlinear_jan_2026_create_labels.py b/test_dlinear_jan_2026_create_labels.py deleted file mode 100644 index 0d5980b..0000000 --- a/test_dlinear_jan_2026_create_labels.py +++ /dev/null @@ -1,305 +0,0 @@ -"""2026年1月BTC数据 - DLinear 准确率测试 (create_labels版) - -与test_dlinear_jan_2026.py对比: -- 本脚本使用create_labels预处理方式(auto_trade旧逻辑) -- 每个目标K线截取到input_idx的df,调用create_labels后取X[-1]预测 -- 用于验证create_labels对预测结果的影响 - -输入数据:预测K线的前一根(即用截止到t的数据预测t+1) -价格对比:预测结果与预测K线(t+1)的收盘价做对比 -时间:币安数据UTC,输出转为北京时间(UTC+8),按北京时间分天 -""" - -import sys -import os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -import torch -import numpy as np -import pandas as pd -import pickle -import pandas_ta as ta -from datetime import timedelta -from sklearn.preprocessing import StandardScaler -from data.fetcher import BinanceDataFetcher - -# ==================== 时区与美股判断 ==================== - -def utc_to_beijing(ts): - """UTC时间转北京时间(UTC+8)""" - return ts + timedelta(hours=8) - -def is_us_stock_open_by_utc(utc_ts): - """根据UTC时间判断美股当天是否开盘 - - 美股交易日基于美东时间(ET),UTC-5(冬令时) - 2026年1月在冬令时范围内(夏令时3月第二个周日开始) - 所以美东 = UTC - 5h - """ - et_ts = utc_ts - timedelta(hours=5) - et_date = pd.Timestamp(et_ts.date()) - - us_holidays_jan_2026 = { - pd.Timestamp('2026-01-01'), # 元旦 New Year's Day - pd.Timestamp('2026-01-19'), # 马丁·路德·金日 MLK Day - } - - if et_date.weekday() >= 5: - return False - if et_date in us_holidays_jan_2026: - return False - return True - -# ==================== pickle反序列化占位类 ==================== - -class ClassificationPreprocessor: - """pickle.load时需要能找到这个类名""" - def __init__(self, lookback=168, forecast_horizon=1): - self.lookback = lookback - self.forecast_horizon = forecast_horizon - self.scaler = StandardScaler() - self.is_fitted = False - -# ==================== create_labels预处理 ==================== - -def preprocess_with_create_labels(df, scaler, lookback=168, forecast_horizon=1): - """使用create_labels方式预处理(auto_trade旧逻辑) - - 步骤:加特征 -> create_labels(shift + 砍尾) -> dropna -> scaler.transform -> 构建窗口 - """ - 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] - - # create_labels: shift + 砍最后一行 - 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) - df = df[:-forecast_horizon] - - df = df.dropna().reset_index(drop=True) - - feature_cols = ['open', 'high', 'low', 'close', 'volume', - 'rsi', 'macd', 'macd_hist', 'macd_signal'] - - scaled_features = scaler.transform(df[feature_cols]) - - X = [] - for i in range(len(scaled_features) - lookback): - X.append(scaled_features[i:i + lookback]) - - return np.array(X), df - -# ==================== 工具函数 ==================== - -def fmt_pred(val): - """格式化预测结果""" - if val is None or (isinstance(val, float) and np.isnan(val)): - return "N/A" - return "涨" if int(val) == 1 else "跌" - -def fmt_result(pred, actual): - """格式化对错""" - if pred is None or (isinstance(pred, float) and np.isnan(pred)): - return " " - return "✓" if int(pred) == int(actual) else "✗" - - -def main(): - print("=" * 90) - print("2026年1月 BTC 1h 涨跌预测准确率测试 (DLinear - create_labels版)") - print("输入:预测K线的前一根 | 对比:预测结果 vs 预测K线收盘价 | 时间:北京时间(UTC+8)") - print("预处理:截取到input的df,调用create_labels后取X[-1]") - print("=" * 90) - - device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu') - print(f"使用设备: {device}") - - # ========== 步骤1: 加载模型 ========== - print("\n[1/4] 加载模型...") - - from models.dlinear.model import DLinear - checkpoint = torch.load('./models/checkpoints/dlinear_classification_2026/best_model.pt', - map_location=device, weights_only=False) - model = DLinear(checkpoint['config']) - model.load_state_dict(checkpoint['model_state_dict']) - model = model.to(device).eval() - with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f: - preprocessor = pickle.load(f) - print(" ✓ DLinear已加载") - - # ========== 步骤2: 获取测试数据 ========== - print("\n[2/4] 获取数据...") - fetcher = BinanceDataFetcher() - df = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-10-01', end_date='2026-02-01', use_cache=False) - - print(f"数据范围(UTC): {df['timestamp'].min()} -> {df['timestamp'].max()}") - print(f"总行数: {len(df)}") - - # ========== 步骤3: 预处理与预测 ========== - print("\n[3/4] 运行预测...") - - lookback = preprocessor.lookback - scaler = preprocessor.scaler - - # 找出1月(北京时间)的所有预测目标K线索引 - jan_targets = [] - for target_idx in range(1, len(df)): - target_time_utc = df.iloc[target_idx]['timestamp'] - target_time_bj = utc_to_beijing(target_time_utc) - if target_time_bj.year == 2026 and target_time_bj.month == 1: - jan_targets.append(target_idx) - - print(f"1月(北京时间)目标K线数: {len(jan_targets)}") - - results = [] - - for target_idx in jan_targets: - target_time_utc = df.iloc[target_idx]['timestamp'] - target_time_bj = utc_to_beijing(target_time_utc) - - # 输入K线索引(前一根) - input_idx = target_idx - 1 - current_close = df.iloc[input_idx]['close'] - target_close = df.iloc[target_idx]['close'] - actual_label = 1 if target_close > current_close else 0 - - # --- DLinear 预测(create_labels方式:截取到input_idx,取X[-1]) --- - dl_pred = None - dl_prob = None - - df_up_to_input = df.iloc[:input_idx + 1] - X, _ = preprocess_with_create_labels(df_up_to_input, scaler, lookback) - - if len(X) > 0: - x_tensor = torch.FloatTensor(X[-1:]).to(device) - with torch.no_grad(): - logits = model(x_tensor).cpu().numpy() - prob = 1 / (1 + np.exp(-logits)) - dl_pred = int(prob.item() > 0.5) - dl_prob = float(prob.item()) - - results.append({ - 'time_utc': target_time_utc, - 'time_bj': target_time_bj, - 'current_close': current_close, - 'target_close': target_close, - 'actual': actual_label, - 'dlinear': dl_pred, - 'dl_prob': dl_prob, - }) - - # ========== 步骤4: 输出结果 ========== - if not results: - print("没有找到符合条件的测试样本") - return - - df_res = pd.DataFrame(results) - df_res['date_bj'] = df_res['time_bj'].dt.date - df_res['us_open'] = df_res['time_utc'].apply(is_us_stock_open_by_utc) - - total_samples = len(df_res) - - print(f"\n[4/4] 准确率统计 (2026年1月,北京时间)") - print(f"总样本数: {total_samples}") - - # ========== 每小时详细数据 ========== - print("\n" + "=" * 90) - print("每小时详细数据") - print("=" * 90) - - hourly_header = ( - f"{'北京时间':<18} {'前收盘':>10} {'现收盘':>10} {'实际':>4} " - f"{'DL':>3} {'':>1} {'概率':>6}" - ) - - for date_bj, day_group in df_res.groupby('date_bj'): - us_open = day_group['us_open'].iloc[0] - us_str = "美股开盘" if us_open else "美股休市" - - dl_day_correct = (day_group['dlinear'] == day_group['actual']).sum() - dl_day_total = day_group['dlinear'].notna().sum() - dl_day_acc = dl_day_correct / dl_day_total if dl_day_total > 0 else 0 - - print(f"\n--- {date_bj} ({us_str}) --- DL准确率:{dl_day_acc:.1%} ({dl_day_correct}/{dl_day_total})") - print(hourly_header) - print("-" * 90) - - for _, row in day_group.iterrows(): - time_str = row['time_bj'].strftime('%m-%d %H:%M') - actual_str = fmt_pred(row['actual']) - dl_str = fmt_pred(row['dlinear']) - dl_r = fmt_result(row['dlinear'], row['actual']) - prob_str = f"{row['dl_prob']:.3f}" if row['dl_prob'] is not None else " N/A" - - print( - f"{time_str:<18} {row['current_close']:>10.2f} {row['target_close']:>10.2f} {actual_str:>4} " - f"{dl_str:>3} {dl_r:>1} {prob_str:>6}" - ) - - # ========== 每日准确率汇总表 ========== - print("\n\n" + "=" * 90) - print("每日准确率汇总 (北京时间)") - print("=" * 90) - - header = f"{'日期':<14} {'样本':>4} {'美股':>6} {'DLinear':>8}" - print(header) - print("-" * 90) - - for date_bj, group in df_res.groupby('date_bj'): - total = len(group) - us_open = group['us_open'].iloc[0] - us_str = "开盘" if us_open else "休市" - - dl_valid = group.dropna(subset=['dlinear']) - dl_acc = (dl_valid['dlinear'] == dl_valid['actual']).mean() if len(dl_valid) > 0 else 0 - dl_str = f"{dl_acc:.1%}" - - print(f"{str(date_bj):<14} {total:>4} {us_str:>6} {dl_str:>8}") - - print("-" * 90) - - # --- 总准确率 --- - dl_valid_all = df_res.dropna(subset=['dlinear']) - dl_total = (dl_valid_all['dlinear'] == dl_valid_all['actual']).mean() if len(dl_valid_all) > 0 else 0 - - print(f"{'总计':<14} {total_samples:>4} {'':>6} {dl_total:.2%}") - print("=" * 90) - - # --- 美股开盘日 vs 休市日对比 --- - print("\n[美股开盘日 vs 休市日 准确率对比]") - print("-" * 60) - for label, flag in [("开盘日", True), ("休市日", False)]: - subset = df_res[df_res['us_open'] == flag] - if len(subset) == 0: - continue - n = len(subset) - dl_s = subset.dropna(subset=['dlinear']) - dl_a = (dl_s['dlinear'] == dl_s['actual']).mean() if len(dl_s) > 0 else 0 - print(f" {label} ({n}样本): DLinear={dl_a:.2%}") - - print("-" * 60) - - # --- 预测分布统计 --- - print("\n[预测分布统计]") - print("-" * 60) - actual_up = (df_res['actual'] == 1).sum() - actual_down = (df_res['actual'] == 0).sum() - print(f" 实际: 上涨={actual_up} ({actual_up/total_samples:.1%}) 下跌={actual_down} ({actual_down/total_samples:.1%})") - - dl_valid = df_res['dlinear'].dropna() - if len(dl_valid) > 0: - up = (dl_valid == 1).sum() - down = (dl_valid == 0).sum() - print(f" DLinear: 预测上涨={up} ({up/len(dl_valid):.1%}) 预测下跌={down} ({down/len(dl_valid):.1%})") - - print("=" * 90) - - -if __name__ == "__main__": - main() diff --git a/auto_trade_polymarket.py b/trader/auto_trade_polymarket.py similarity index 73% rename from auto_trade_polymarket.py rename to trader/auto_trade_polymarket.py index a5ca99b..8d98eab 100644 --- a/auto_trade_polymarket.py +++ b/trader/auto_trade_polymarket.py @@ -1,7 +1,7 @@ """ BTC Hourly 自动交易脚本 -基于DLinear模型预测,在Polymarket自动下单BTC小时涨跌 +基于DLinear、N-BEATS、TTM-R2多模型集成预测,在Polymarket自动下单BTC小时涨跌 支持: - 每小时自动寻找对应市场 - 实时价格刷新 @@ -18,24 +18,22 @@ import logging from logging.handlers import RotatingFileHandler import schedule from datetime import datetime, timedelta -from typing import Optional, Tuple, Dict -import pickle +from typing import Optional, Tuple, Dict, List from zoneinfo import ZoneInfo import torch import numpy as np import pandas as pd -import pandas_ta as ta -from sklearn.preprocessing import StandardScaler -from dotenv import load_dotenv import httpx +from dotenv import load_dotenv # 添加项目路径 -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from data.fetcher import BinanceDataFetcher from data.trading_db import TradingDatabase from api.models import PredictionModel +from models.inference import get_inference_manager load_dotenv() @@ -57,6 +55,7 @@ def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO) -> loggi # 创建logger logger = logging.getLogger("polymarket_trader") logger.setLevel(log_level) + logger.propagate = False # 避免重复添加handler if logger.handlers: @@ -93,78 +92,8 @@ def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO) -> loggi logger = setup_logging() - -class ClassificationPreprocessor: - """DLinear分类预处理器""" - - def __init__(self, lookback: int = 168, forecast_horizon: int = 1): - self.lookback = lookback - self.forecast_horizon = forecast_horizon - self.scaler = StandardScaler() - self.is_fitted = False - - def add_features(self, df: pd.DataFrame) -> pd.DataFrame: - 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: pd.DataFrame) -> pd.DataFrame: - 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: pd.DataFrame, fit: bool = 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 preprocess_for_inference(self, df: pd.DataFrame): - """推理专用预处理:不调用create_labels,整个df一次性处理 - - 与test_dlinear_jan_2026.py逻辑一致: - 加特征 -> dropna -> scaler.transform -> 构建窗口 - """ - df = self.add_features(df) - df = df.dropna().reset_index(drop=True) - - feature_cols = ['open', 'high', 'low', 'close', 'volume', - 'rsi', 'macd', 'macd_hist', 'macd_signal'] - - scaled_features = self.scaler.transform(df[feature_cols]) - - X = [] - for i in range(len(scaled_features) - self.lookback): - X.append(scaled_features[i:i + self.lookback]) - - return np.array(X), df - - class PolymarketBTCTrader: - """Polymarket BTC Hourly交易器""" + """Polymarket BTC Hourly交易器 (多模型集成版)""" GAMMA_API = "https://gamma-api.polymarket.com" CLOB_API = "https://clob.polymarket.com" @@ -196,18 +125,11 @@ class PolymarketBTCTrader: headers={'Accept-Encoding': 'identity'} ) - # 设备选择:macOS 用 MPS,Windows/Linux 用 CPU - import platform - if platform.system() == 'Darwin': # macOS - self.device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu') - else: # Windows / Linux - self.device = torch.device('cpu') - print(f"使用设备: {self.device}") - - # 加载模型 - self.model = None - self.preprocessor = None - self._load_model() + # 初始化推理管理器 + logger.info("初始化推理管理器...") + self.inference_manager = get_inference_manager() + self.inference_manager.load_all_models() + self.device = self.inference_manager.device # 数据获取器 self.fetcher = BinanceDataFetcher() @@ -224,25 +146,6 @@ class PolymarketBTCTrader: self.current_market = None self.last_price_update = None - def _load_model(self): - """加载DLinear模型""" - logger.info("加载DLinear模型...") - - from models.dlinear.model import DLinear - - checkpoint_path = './models/checkpoints/dlinear_classification_2026/best_model.pt' - preprocessor_path = './models/checkpoints/dlinear_classification_2026/preprocessor.pkl' - - checkpoint = torch.load(checkpoint_path, map_location=self.device, weights_only=False) - self.model = DLinear(checkpoint['config']) - self.model.load_state_dict(checkpoint['model_state_dict']) - self.model = self.model.to(self.device).eval() - - with open(preprocessor_path, 'rb') as f: - self.preprocessor = pickle.load(f) - - logger.info(f"✓ 模型已加载 (设备: {self.device})") - def _init_clob_client(self): """初始化CLOB交易客户端""" try: @@ -271,7 +174,7 @@ class PolymarketBTCTrader: elif result.get('error'): status = 'failure' elif result.get('prediction'): # 有预测但未交易(如市场未找到) - status = 'in_progress' # 或者 failure? 未找到市场算 failure + status = 'in_progress' if result.get('error'): status = 'failure' @@ -279,7 +182,7 @@ class PolymarketBTCTrader: 'timestamp': result.get('timestamp'), 'market_title': result.get('market'), 'market_slug': market.get('slug') if market else result.get('market_slug'), - 'model_name': 'DLinear', + 'model_name': 'Ensemble', # 记录为集成策略 'prediction': result.get('prediction'), 'probability': result.get('probability'), 'action': result.get('side'), @@ -301,13 +204,6 @@ class PolymarketBTCTrader: 格式: bitcoin-up-or-down-{month}-{day}-{hour}am/pm-et 例如: bitcoin-up-or-down-february-3-8am-et - - Args: - target_time: 目标时间(ET时区) - for_current_hour: 如果True,使用当前整点;否则使用下一个整点 - - Returns: - 市场slug """ # 获取美东时间 et_tz = ZoneInfo('America/New_York') @@ -338,15 +234,7 @@ class PolymarketBTCTrader: return slug def get_market_by_slug(self, slug: str) -> Optional[Dict]: - """ - 通过slug获取市场信息 - - Args: - slug: 市场slug - - Returns: - 市场信息 - """ + """通过slug获取市场信息""" try: response = self.http.get( f"{self.GAMMA_API}/events", @@ -400,15 +288,7 @@ class PolymarketBTCTrader: return None def get_current_hourly_market(self) -> Optional[Dict]: - """ - 获取BTC市场 - - Args: - for_current_hour: 如果True,获取当前整点市场;否则获取下一个整点市场 - - Returns: - 市场信息 - """ + """获取BTC市场""" slug = self.generate_market_slug() logger.info(f"搜索市场: {slug}") @@ -426,12 +306,7 @@ class PolymarketBTCTrader: return market def refresh_prices(self) -> Optional[Dict]: - """ - 刷新当前市场价格 - - Returns: - 更新后的市场信息 - """ + """刷新当前市场价格""" if not self.current_market: self.get_current_hourly_market() @@ -478,18 +353,7 @@ class PolymarketBTCTrader: return market def get_realtime_prices(self, up_token_id: str, down_token_id: str) -> Optional[list]: - """ - 从CLOB API获取实时买入价格 - - 使用/price?side=BUY端点获取真实买入价格 (与页面显示一致) - - Args: - up_token_id: Up代币ID - down_token_id: Down代币ID - - Returns: - [up_price, down_price] 或 None - """ + """从CLOB API获取实时买入价格""" try: # 使用/price端点获取买入价格 up_response = self.http.get( @@ -516,16 +380,7 @@ class PolymarketBTCTrader: return None def execute_trade(self, prediction: int, market: Dict) -> Dict: - """ - 执行交易 - - Args: - prediction: 预测结果(0=跌, 1=涨) - market: 市场信息 - - Returns: - 交易结果 - """ + """执行交易""" result = { 'timestamp': datetime.now().isoformat(), 'market': market['title'], @@ -598,16 +453,7 @@ class PolymarketBTCTrader: def get_prediction_with_delay(self, delay_seconds: float = 2.0) -> Tuple[int, float]: """ - 延迟后获取K线并预测当前小时的涨跌 - - 注意: 使用上一小时收盘的K线(倒数第二根)来预测当前小时 - 例如: 22:00整点时,用21:00收盘的K线预测22:00这个小时的涨跌 - - Args: - delay_seconds: 延迟秒数(等待K线更新) - - Returns: - (prediction, probability): 预测结果(0=跌, 1=涨)和概率 + 延迟后获取K线并预测当前小时的涨跌 (多模型) """ logger.info(f"⏳ 等待{delay_seconds}秒获取最新K线...") time.sleep(delay_seconds) @@ -626,7 +472,6 @@ class PolymarketBTCTrader: ) # 使用倒数第二根K线(上一小时收盘)进行预测 - # 例如22:00时,最新K线是14:00 UTC(22:00北京),我们用13:00 UTC(21:00北京)的数据 prev_time = df['timestamp'].iloc[-2] prev_close = df['close'].iloc[-2] current_time = df['timestamp'].iloc[-1] @@ -637,14 +482,12 @@ class PolymarketBTCTrader: logger.info(f" 当前K线时间: {current_time} (预测目标)") logger.info(f" 当前收盘价: ${current_close:,.2f} (未收盘)") - # 回填上一小时的实际结果 - # prev_time的K线已收盘,用它的收盘价(prev_close) vs 再前一根的收盘价来判断涨跌 + # 回填上一小时的实际结果 (多模型) self._update_previous_prediction(df) - # 用除最后一根外的数据进行预测(无create_labels,与test逻辑一致) + # 用除最后一根外的数据进行预测 df_for_prediction = df.iloc[:-1] - X, processed_df = self.preprocessor.preprocess_for_inference(df_for_prediction) - + # 打印模型输入的最后一根K线数据 last_input_row = df_for_prediction.iloc[-1] logger.info(f" 📌 模型输入末根K线: {last_input_row['timestamp']} " @@ -652,52 +495,95 @@ class PolymarketBTCTrader: f"L:{last_input_row['low']:.2f} C:{last_input_row['close']:.2f} " f"V:{last_input_row['volume']:.0f}") - X_last = torch.FloatTensor(X[-1:]).to(self.device) - - with torch.no_grad(): - logits = self.model(X_last).cpu().numpy() - prob = 1 / (1 + np.exp(-logits)) - prediction = int(prob.item() > 0.5) - probability = float(prob.item()) + # === 多模型预测 === + all_preds = self.inference_manager.predict_all(df_for_prediction) - direction = "📈 UP (涨)" if prediction == 1 else "📉 DOWN (跌)" - confidence = probability if prediction == 1 else 1 - probability - logger.info(f"🤖 DLinear预测: {direction}") - logger.info(f" 置信度: {confidence:.2%}") + # 解析预测结果 + final_prediction = 0 + final_probability = 0.5 + + # 1. DLinear + if 'dlinear' in all_preds: + dl = all_preds['dlinear'] + dl_dir = "📈 UP" if dl['prediction'] == 1 else "📉 DOWN" + logger.info(f" [DLinear] {dl_dir} (conf: {dl['probability']:.2%})") + + # 2. N-BEATS + if 'nbeats' in all_preds: + nb = all_preds['nbeats'] + nb_dir = "📈 UP" if nb['prediction'] == 1 else "📉 DOWN" + logger.info(f" [N-BEATS] {nb_dir}") + + # 3. TTM-R2 + if 'ttm' in all_preds: + ttm = all_preds['ttm'] + ttm_dir = "📈 UP" if ttm['prediction'] == 1 else "📉 DOWN" + logger.info(f" [TTM-R2] {ttm_dir} (conf: {ttm['probability']:.2%})") + + # 4. 集成结果 + if 'ensemble' in all_preds and all_preds['ensemble']: + ens = all_preds['ensemble'] + ens_pred = ens['prediction'] + votes = ens['votes'] + ens_dir = "📈 UP" if ens_pred == 1 else "📉 DOWN" + vote_str = f"{sum(votes)}/{len(votes)}" + logger.info(f" [Ensemble] {ens_dir} (Votes: {vote_str})") + + # 使用集成结果作为最终决策 + final_prediction = ens_pred + final_probability = 0.6 # 集成策略暂时给一个固定置信度或基于投票比例 + if len(votes) > 0: + final_probability = sum(votes) / len(votes) if ens_pred == 1 else 1 - (sum(votes) / len(votes)) + else: + # 降级到 DLinear + if 'dlinear' in all_preds: + final_prediction = all_preds['dlinear']['prediction'] + final_probability = all_preds['dlinear']['probability'] + logger.warning("⚠️ 集成预测不可用,降级为 DLinear") - # 保存预测结果到 model_predictions 表 - # 当前K线尚未收盘,actual_result 和 is_correct 留空,等收盘后更新 + # 保存所有预测结果到 model_predictions 表 try: et_tz = ZoneInfo('America/New_York') - # current_time 是目标K线的UTC时间戳(当前正在进行的K线) current_time_utc = pd.Timestamp(current_time) current_time_et = current_time_utc.tz_localize('UTC').tz_convert(et_tz) - - prediction_data = { + + common_data = { "timestamp": current_time_utc.isoformat(), "timestamp_et": current_time_et.isoformat(), "date": current_time_et.strftime('%Y-%m-%d'), "hour": current_time_et.hour, - "model_name": "DLinear", - "prediction": prediction, - "probability": probability, "actual_result": None, "is_correct": None, "open_price": float(df['open'].iloc[-1]), "close_price": None, } - PredictionModel.save_prediction(prediction_data) - logger.info(f"💾 预测记录已保存 (ET {current_time_et.strftime('%Y-%m-%d %H:%M')})") + + # 辅助函数 + def save_single_pred(name, pred_res): + data = common_data.copy() + data['model_name'] = name + data['prediction'] = pred_res['prediction'] + data['probability'] = pred_res.get('probability', 0.5) + PredictionModel.save_prediction(data) + + if 'dlinear' in all_preds: + save_single_pred("DLinear", all_preds['dlinear']) + if 'nbeats' in all_preds: + save_single_pred("N-BEATS", all_preds['nbeats']) + if 'ttm' in all_preds: + save_single_pred("TTM-R2", all_preds['ttm']) + if 'ensemble' in all_preds and all_preds['ensemble']: + # 集成也保存 + save_single_pred("Ensemble", all_preds['ensemble']) + + logger.info(f"💾 所有模型预测记录已保存 (ET {current_time_et.strftime('%Y-%m-%d %H:%M')})") except Exception as e: logger.error(f"保存预测记录失败: {e}") - return prediction, probability + return final_prediction, final_probability def _update_previous_prediction(self, df: pd.DataFrame): - """回填上一小时预测的实际结果 - - df.iloc[-2] 是上一小时已收盘K线,收盘价 vs 开盘价即为实际涨跌。 - """ + """回填上一小时预测的实际结果 (所有模型)""" if len(df) < 2: return @@ -713,20 +599,23 @@ class PolymarketBTCTrader: prev_close = float(prev_row['close']) actual_result = 1 if prev_close > prev_open else 0 - updated = PredictionModel.update_actual_result( - date=prev_date, - hour=prev_hour, - model_name="DLinear", - actual_result=actual_result, - close_price=prev_close - ) - if updated: - record = PredictionModel.get_prediction(prev_date, prev_hour, "DLinear") - is_correct = record.get('is_correct') if record else None - correct_str = '✅ 正确' if is_correct == 1 else '❌ 错误' if is_correct == 0 else '未知' - logger.info(f"📝 回填上一小时结果: ET {prev_date} {prev_hour}:00, " - f"开:{prev_open:.2f} 收:{prev_close:.2f} " - f"实际{'涨' if actual_result == 1 else '跌'}, {correct_str}") + models_to_update = ["DLinear", "N-BEATS", "TTM-R2", "Ensemble"] + + for model_name in models_to_update: + updated = PredictionModel.update_actual_result( + date=prev_date, + hour=prev_hour, + model_name=model_name, + actual_result=actual_result, + close_price=prev_close + ) + if updated: + # 仅打印一条日志代表更新成功,避免刷屏 + pass + + logger.info(f"📝 回填上一小时结果: ET {prev_date} {prev_hour}:00, " + f"实际{'涨' if actual_result == 1 else '跌'} (O:{prev_open:.0f} C:{prev_close:.0f})") + except Exception as e: logger.error(f"回填上一小时结果失败: {e}") @@ -738,21 +627,6 @@ class PolymarketBTCTrader: def run_at_hour_start(self, delay_seconds: float = 2.0, timeout_minutes: int = 50) -> Dict: """ 整点时执行的交易流程 - - 1. 延迟delay_seconds秒等待K线更新 - 2. 获取最新K线 - 3. DLinear预测当前整点的收盘涨跌 - 4. 获取对应的Polymarket市场 - 5. 循环检查价格: - - 如果价格 <= max_price: 下单并结束 - - 如果价格 > max_price: 等待并重试(直到超时) - - Args: - delay_seconds: 整点后延迟秒数 - timeout_minutes: 价格等待超时时间(分钟) - - Returns: - 执行结果 """ logger.info("=" * 60) logger.info(f"⏰ 整点交易触发 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") @@ -872,21 +746,30 @@ class PolymarketBTCTrader: logger.info(f" 上一K线: {prev_time} 收盘价: ${prev_close:,.2f}") logger.info(f" 当前K线: {current_time} 收盘价: ${current_close:,.2f}") - # 进行预测(无create_labels,与test逻辑一致) + # 进行多模型预测 df_for_prediction = df.iloc[:-1] - X, _ = self.preprocessor.preprocess_for_inference(df_for_prediction) - X_last = torch.FloatTensor(X[-1:]).to(self.device) + all_preds = self.inference_manager.predict_all(df_for_prediction) - with torch.no_grad(): - logits = self.model(X_last).cpu().numpy() - prob = 1 / (1 + np.exp(-logits)) - prediction = int(prob.item() > 0.5) - probability = float(prob.item()) - - direction = "📈 UP (涨)" if prediction == 1 else "📉 DOWN (跌)" - confidence = probability if prediction == 1 else 1 - probability - - logger.info(f" 🤖 预测: {direction} (置信度: {confidence:.2%})") + # DLinear + if 'dlinear' in all_preds: + dl = all_preds['dlinear'] + logger.info(f" 🤖 DLinear: {'📈 UP' if dl['prediction']==1 else '📉 DOWN'} ({dl['probability']:.1%})") + + # N-BEATS + if 'nbeats' in all_preds: + nb = all_preds['nbeats'] + logger.info(f" 🤖 N-BEATS: {'📈 UP' if nb['prediction']==1 else '📉 DOWN'}") + + # TTM-R2 + if 'ttm' in all_preds: + ttm = all_preds['ttm'] + logger.info(f" 🤖 TTM-R2: {'📈 UP' if ttm['prediction']==1 else '📉 DOWN'} ({ttm['probability']:.1%})") + + prediction = 0 + if 'ensemble' in all_preds and all_preds['ensemble']: + ens = all_preds['ensemble'] + prediction = ens['prediction'] + logger.info(f" 🤖 集成结果: {'📈 UP' if prediction==1 else '📉 DOWN'}") # 显示市场价格 if self.current_market: @@ -904,13 +787,76 @@ class PolymarketBTCTrader: logger.info(f" ⏰ 距离下一整点: {wait_minutes}分钟") logger.info("") + + # === 新增: 保存初始预测和状态 === + try: + # 确定最终预测 (用于日志) + final_pred = 0 + if 'ensemble' in all_preds and all_preds['ensemble']: + final_pred = all_preds['ensemble']['prediction'] + elif 'dlinear' in all_preds: + final_pred = all_preds['dlinear']['prediction'] + + et_tz = ZoneInfo('America/New_York') + current_time_utc = pd.Timestamp(current_time) + current_time_et = current_time_utc.tz_localize('UTC').tz_convert(et_tz) + + # 1. 保存预测到 model_predictions + common_data = { + "timestamp": current_time_utc.isoformat(), + "timestamp_et": current_time_et.isoformat(), + "date": current_time_et.strftime('%Y-%m-%d'), + "hour": current_time_et.hour, + "actual_result": None, + "is_correct": None, + "open_price": float(df['open'].iloc[-1]), + "close_price": None, + } + + def save_single_pred(name, pred_res): + data = common_data.copy() + data['model_name'] = name + data['prediction'] = pred_res['prediction'] + data['probability'] = pred_res.get('probability', 0.5) + PredictionModel.save_prediction(data) + + if 'dlinear' in all_preds: save_single_pred("DLinear", all_preds['dlinear']) + if 'nbeats' in all_preds: save_single_pred("N-BEATS", all_preds['nbeats']) + if 'ttm' in all_preds: save_single_pred("TTM-R2", all_preds['ttm']) + if 'ensemble' in all_preds and all_preds['ensemble']: save_single_pred("Ensemble", all_preds['ensemble']) + + logger.info(f"💾 [Init] 初始预测已保存到数据库") + + # 2. 记录当前监控状态到 trades (作为 'in_progress') + status = 'in_progress' + market_title = self.current_market['title'] if self.current_market else "Waiting for Market..." + market_slug = self.current_market['slug'] if self.current_market else None + + log_data = { + 'timestamp': datetime.now().isoformat(), + 'market_title': market_title, + 'market_slug': market_slug, + 'model_name': 'Ensemble', + 'prediction': 'UP' if final_pred == 1 else 'DOWN', + 'probability': 0.0, + 'action': 'MONITOR', + 'price': 0, + 'amount': 0, + 'size': 0, + 'status': status, + 'error_message': 'System Started - Monitoring', + 'raw_data': {} + } + self.db.log_trade(log_data) + logger.info(f"💾 [Init] 初始监控状态已记录") + + except Exception as e: + logger.error(f"保存初始状态失败: {e}") + import traceback + traceback.print_exc() + def wait_for_next_hour(self) -> datetime: - """ - 等待到下一个整点 - - Returns: - 下一个整点时间 - """ + """等待到下一个整点""" now = datetime.now() next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) wait_seconds = (next_hour - now).total_seconds() @@ -921,19 +867,9 @@ class PolymarketBTCTrader: return next_hour def run_scheduler(self, interval_minutes: int = 5, delay_seconds: float = 2.0): - """ - 定时运行交易器 - - 工作流程: - 1. 每个整点+delay_seconds秒触发交易 - 2. 期间每interval_minutes分钟刷新价格 - - Args: - interval_minutes: 价格刷新间隔(分钟) - delay_seconds: 整点后延迟秒数 - """ + """定时运行交易器""" logger.info("=" * 60) - logger.info("🤖 BTC Hourly自动交易已启动") + logger.info("🤖 BTC Hourly自动交易 (集成版) 已启动") logger.info("=" * 60) logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}") logger.info(f" 下单时机: 每个整点后延迟{delay_seconds}秒") @@ -968,7 +904,7 @@ class PolymarketBTCTrader: def main(): - parser = argparse.ArgumentParser(description='BTC Hourly自动交易 - 基于DLinear模型') + parser = argparse.ArgumentParser(description='BTC Hourly自动交易 - 基于DLinear、N-BEATS、TTM-R2集成模型') parser.add_argument('--dry-run', action='store_true', default=True, help='模拟模式(不实际下单)') parser.add_argument('--real', action='store_true', @@ -1016,7 +952,10 @@ def main(): logs = [] if os.path.exists(log_file): with open(log_file, 'r') as f: - logs = json.load(f) + try: + logs = json.load(f) + except: + pass logs.append(result) with open(log_file, 'w') as f: json.dump(logs, f, indent=2, ensure_ascii=False)