Author SHA1 Message Date
crypto_predictor 6a882ed913 fix: update initial trade logic
- Enable immediate trading attempt on startup
- Dynamic timeout calculation (remaining minutes - 10) for initial trade
- Added duplicate check for initial prediction saving to prevent DB errors
2026-02-11 23:36:07 +08:00
crypto_predictor 99fa79cde4 fix: timezone handling and prediction logic
- api/models.py: Fix timezone conversion in stats queries by calculating UTC ranges in Python instead of using SQL CONVERT_TZ. Ensure JSON serializable types.
- api/prediction_service.py:
    - Update ensure_predictions_for_month to check previous day (ET) to cover CST month start.
    - Update _generate_prediction_for_hour to skip saving unclosed candles (current hour).
- trader/auto_trade_polymarket.py: Update to use hourly_model_predictions (V2) table for storing live trading predictions.
2026-02-11 23:14:50 +08:00
crypto_predictor 2993014fa8 feat: upgrade to multi-model ensemble system
- Updated DB schemas: added 'strategy' to trades, new 'hourly_model_predictions' wide table
- Refactored backend: api/models.py, api/prediction_service.py to support wide format
- Updated trader: auto_trade_polymarket.py now logs strategy and saves to new table
- Updated frontend: index.html and app.js to display multi-model accuracy and trade strategy
2026-02-11 22:13:35 +08:00
crypto_predictor c0a02868ac 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
2026-02-11 22:07:32 +08:00
crypto_predictor d34b67f522 Add CLAUDE.md guide and preprocessing comparison script 2026-02-10 23:26:44 +08:00
crypto_predictorandClaude Opus 4.6 84288a687a feat: auto_trade 每小时预测结果写入预测表并回填上一小时实际结果
- 导入 PredictionModel,预测完成后保存到 model_predictions 表
- 当前小时预测 actual_result/is_correct/close_price 留空(未收盘)
- 新增 _update_previous_prediction:每次整点回填上一小时实际涨跌(收盘价 vs 开盘价)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:19:57 +08:00
crypto_predictorandClaude Opus 4.6 e78836bf42 fix: 修复 auto_trade 推理预处理逻辑,新增 create_labels 版对比测试脚本
auto_trade_polymarket.py: 新增 preprocess_for_inference 方法,推理时不调用
create_labels(避免 df[:-1] 砍掉输入K线),与 test 脚本逻辑一致。
test_dlinear_jan_2026_create_labels.py: 使用 create_labels 方式的 DLinear 测试
脚本,用于对比两种预处理方式的准确率差异。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:05:17 +08:00
crypto_predictor 7990312878 feat: 添加 ONNX 导出功能及 DLinear 模型导出 2026-02-08 15:05:09 +08:00
crypto_predictor 5ca1779d7d fix: 精简 requirements.txt,移除未使用的依赖
移除的依赖:
- gluonts: 项目中未使用
- pytorch-lightning: 非必需
- torchmetrics: 非必需
- onnx/onnxruntime: 非核心功能
- sqlalchemy: 使用 pymysql 直连
- jupyter/matplotlib/seaborn/pytest: 开发工具,非运行必需

保留的核心依赖:
- pandas, numpy, scikit-learn: 数据处理
- granite-tsfm: TTM-R2 模型
- pandas-ta: 技术指标
- fastapi, uvicorn: API 服务
- httpx, python-binance: 数据获取
- schedule: 任务调度
- pymysql, cryptography: 数据库

注意:PyTorch 需要单独安装(Windows 用 CPU 版本)
2026-02-07 22:28:46 +08:00
crypto_predictor 9320b79730 refactor: 重新整理 requirements.txt
主要更改:
- 修正 TTM 模型包名: tsfm_public -> granite-tsfm
- 添加缺失依赖: httpx, schedule, pytorch-lightning, torchmetrics
- 移除过时依赖: darts, u8darts
- 按功能分类组织依赖
- 添加 Windows/macOS 安装说明注释
- 更新安装脚本和文档中的包名
2026-02-07 22:26:07 +08:00
crypto_predictor 06800ddb43 fix: 修正 tsfm-public 包名,添加一键安装脚本
- 添加 install_windows.bat 一键安装脚本
- 修正文档中 tsfm_public -> tsfm-public (PyPI 包名用连字符)
- 添加 GitHub 备用安装方式
2026-02-07 22:02:56 +08:00
crypto_predictor 340c11d4ab docs: 添加 Windows 环境安装指南
包含完整的安装流程:
- Miniconda 安装
- Conda 环境创建
- PyTorch CPU 版本安装
- 项目依赖安装
- 数据库配置
- 环境变量配置
- 运行和验证步骤
- 常见问题解答
2026-02-07 21:55:56 +08:00
crypto_predictor d0696a42ba feat: 添加 Windows 启动脚本
- start_system.bat: CMD 批处理脚本,双击即可运行
- start_system.ps1: PowerShell 脚本,支持进程监控和优雅退出

两个脚本都会:
1. 激活 crypto_pred_312 conda 环境
2. 启动 API 服务 (端口 8000)
3. 启动自动交易程序
2026-02-07 21:54:46 +08:00
crypto_predictor 233f0c5d10 feat: 添加 Windows 兼容性支持,优化设备选择逻辑
- start_system.py: 跨平台进程管理
  - Windows 使用 CREATE_NEW_PROCESS_GROUP 和 terminate()
  - Unix 使用 setsid 和 killpg
  - 自动检测并启用 Windows ANSI 颜色支持

- 设备选择逻辑优化:
  - macOS: 优先使用 MPS,回退到 CPU
  - Windows/Linux: 强制使用 CPU(无 CUDA 依赖)

- 影响文件:
  - api/prediction_service.py
  - auto_trade_polymarket.py
  - training/config.py
2026-02-07 21:48:06 +08:00
29 changed files with 2437 additions and 4281 deletions

No files matched your search

+6 -1
View File
@@ -12,7 +12,12 @@
"Bash(git push:*)", "Bash(git push:*)",
"Bash(conda activate:*)", "Bash(conda activate:*)",
"WebSearch", "WebSearch",
"Bash(pip install:*)" "Bash(pip install:*)",
"Bash(git status:*)",
"Bash(git -C /Users/xxz/PycharmProjects/crypto_predictor check-ignore -v test_dlinear_jan_2026_create_labels.py)",
"Bash(git -C /Users/xxz/PycharmProjects/crypto_predictor status)",
"Bash(git -C /Users/xxz/PycharmProjects/crypto_predictor diff)",
"Bash(git -C /Users/xxz/PycharmProjects/crypto_predictor log --oneline -5)"
] ]
} }
} }
+83
View File
@@ -0,0 +1,83 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
基于深度学习的 BTC 小时级涨跌预测系统,包含模型训练、FastAPI 后端、Vue 3 看板、Polymarket 自动交易。当前最佳模型 DLinear 准确率 53.10%。
## 环境与构建
```bash
# Python 环境(必须使用 Conda,禁止 venv/poetry)
conda activate crypto_pred_312
# 安装依赖(PyTorch 需根据平台单独安装,requirements.txt 不含 torch)
pip install -r requirements.txt
# 数据库:仅支持 MySQL,禁止 SQLite
# 配置:复制 .env.example 为 .env 并填写 MySQL 连接信息
```
## 常用命令
```bash
# 启动完整系统(API + 自动交易)
python start_system.py
# 单独启动 API 服务
python -m uvicorn api.main:app --host 0.0.0.0 --port 8000
# 单独启动自动交易
python auto_trade_polymarket.py
# 训练 DLinear 分类模型
python train_classification_full.py
# 测试模型(2026年数据)
python test_models_feb_2026.py
# 集成预测
python ensemble_predict.py
# 测试与检查
pytest
flake8 .
```
## 架构
```
数据流: Binance API → data/fetcher.py → data/preprocessor.py (技术指标) → data/dataset.py → 模型推理
交易流: 模型预测 → auto_trade_polymarket.py → Polymarket 下单 → MySQL 记录
服务流: FastAPI (api/main.py) → 前端看板 (api/static/) → http://localhost:8000
```
### 核心模块
- **`data/`** — 数据层:`fetcher.py`(Binance数据获取)、`preprocessor.py`(特征工程: RSI+MACD)、`dataset.py`(PyTorch数据集)、`database.py`(市场数据MySQL)、`trading_db.py`(交易历史MySQL)
- **`models/`** — 模型层:`dlinear/model.py`(主力模型,时序分解+线性层)、`nbeats/`、`base.py`(抽象基类)
- **`models/checkpoints/dlinear_classification_2026/`** — 训练产物:`best_model.pt`、`preprocessor.pkl`、`test_2026_results.json`
- **`training/`** — 训练层:`trainer.py`(训练循环,支持 MPS/CUDA/CPU)、`config.py`(超参数)、`evaluator.py`(评估)
- **`api/`** — 服务层:`main.py`(FastAPI入口)、`models.py`(DB操作+时区转换)、`prediction_service.py`(预测服务)、`polymarket_client.py`(交易客户端)
- **`start_system.py`** — 统一启动入口(同时启动 API 和自动交易)
- **`auto_trade_polymarket.py`** — 自动交易主程序(每小时执行预测+下单)
### DLinear 模型结构
输入 (168h × 9特征) → MovingAvg 时序分解 → 季节性/趋势分量各经线性层 → MLP 聚合 → logits 输出 (BCEWithLogitsLoss)
## 开发约束
- **数据库**:仅 MySQL(`pymysql`),禁止 SQLite。连接使用 `data/database.py` 的 `DatabaseManager` 或 `api/models.py` 的 `get_db_connection`
- **时区**:数据库存 UTC/ET,前端展示 CST(北京时间),转换用 `api.models.convert_to_cst()`
- **代码注释**:中文,解释"为什么"
- **命名**:变量/函数 `snake_case`,类 `PascalCase`
- **代理访问 Binance**:需在 `.env` 配置 `SOCKS5_PROXY` 和 `USE_PROXY=true`
- **日志**:交易日志在 `logs/polymarket_trading.log`(10MB 轮转)
## 禁止事项
- 禁止引入 SQLite 或恢复已删除的 SQLite 代码
- 禁止在未确认的情况下升级核心库版本(torch, pandas 等)
- 禁止删除看似无用但可能被外部脚本调用的代码
-459
View File
@@ -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()
+6 -6
View File
@@ -106,8 +106,8 @@ async def get_monthly_accuracy(
service = get_prediction_service() service = get_prediction_service()
service.ensure_predictions_for_month(month) service.ensure_predictions_for_month(month)
# 获取统计 # 获取统计 (使用 V2 方法)
stats = PredictionModel.get_monthly_stats(month) stats = PredictionModel.get_monthly_stats_v2(month)
return { return {
"success": True, "success": True,
@@ -135,8 +135,8 @@ async def get_daily_accuracy(
service = get_prediction_service() service = get_prediction_service()
service.ensure_predictions_for_month(month) service.ensure_predictions_for_month(month)
# 获取统计 # 获取统计 (使用 V2 方法)
result = PredictionModel.get_daily_stats(month=month, page=page, limit=limit) result = PredictionModel.get_daily_stats_v2(month=month, page=page, limit=limit)
return { return {
"success": True, "success": True,
@@ -155,8 +155,8 @@ async def get_hourly_accuracy(date: str):
service = get_prediction_service() service = get_prediction_service()
service.ensure_predictions_for_date(date) service.ensure_predictions_for_date(date)
# 获取详情 # 获取详情 (使用 V2 方法)
details = PredictionModel.get_hourly_details(date) details = PredictionModel.get_hourly_details_v2(date)
return { return {
"success": True, "success": True,
+345 -175
View File
@@ -4,7 +4,7 @@
import pymysql import pymysql
import os import os
from typing import Optional, List, Dict from typing import Optional, List, Dict
from datetime import datetime from datetime import datetime, timedelta
from contextlib import contextmanager from contextlib import contextmanager
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -88,6 +88,7 @@ def _dict_from_row(row):
def init_model_predictions_table(): def init_model_predictions_table():
"""初始化 model_predictions 表""" """初始化 model_predictions 表"""
# 兼容旧表,但主要使用 hourly_model_predictions
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
@@ -110,6 +111,42 @@ def init_model_predictions_table():
INDEX idx_model (model_name) INDEX idx_model (model_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
''') ''')
# 确保 hourly_model_predictions 表存在
cursor.execute('''
CREATE TABLE IF NOT EXISTS hourly_model_predictions (
id INT AUTO_INCREMENT PRIMARY KEY,
timestamp DATETIME NOT NULL,
timestamp_et VARCHAR(50),
date VARCHAR(10),
hour INT,
open_price DOUBLE,
close_price DOUBLE,
actual_result INT COMMENT '0=Down, 1=Up',
dlinear_prediction INT,
dlinear_prob DOUBLE,
dlinear_correct INT,
nbeats_prediction INT,
nbeats_prob DOUBLE,
nbeats_correct INT,
ttm_prediction INT,
ttm_prob DOUBLE,
ttm_correct INT,
ensemble_prediction INT,
ensemble_votes VARCHAR(50),
ensemble_correct INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_date_hour (date, hour),
INDEX idx_timestamp (timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
''')
conn.commit() conn.commit()
@@ -143,12 +180,6 @@ class TradeModel:
day: Optional[str] = None) -> Dict: day: Optional[str] = None) -> Dict:
""" """
获取历史交易记录 获取历史交易记录
Args:
page: 页码
limit: 每页条数
month: 月份筛选 (YYYY-MM)
day: 日期筛选 (YYYY-MM-DD)
""" """
offset = (page - 1) * limit offset = (page - 1) * limit
@@ -160,7 +191,7 @@ class TradeModel:
params = [] params = []
if month: if month:
where_clauses.append("DATE_FORMAT(timestamp, '%%Y-%%m') = %s") where_clauses.append("DATE_FORMAT(timestamp, '%Y-%m') = %s")
params.append(month) params.append(month)
if day: if day:
@@ -199,130 +230,178 @@ class TradeModel:
class PredictionModel: class PredictionModel:
"""预测记录模型""" """预测记录模型 (V2 - 宽表结构)"""
@staticmethod @staticmethod
def get_prediction(date: str, hour: int, model_name: str) -> Optional[Dict]: def save_hourly_prediction(data: Dict) -> int:
"""获取指定时间和模型的预测记录"""
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM model_predictions
WHERE date = %s AND hour = %s AND model_name = %s
""", (date, hour, model_name))
row = cursor.fetchone()
return _dict_from_row(row)
@staticmethod
def save_prediction(data: Dict) -> int:
""" """
保存预测记录 保存每小时综合预测记录 (宽表)
Args:
data: 预测数据字典
Returns:
插入的记录ID
""" """
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
sql = """ sql = """
INSERT INTO model_predictions INSERT INTO hourly_model_predictions
(timestamp, timestamp_et, date, hour, model_name, (timestamp, timestamp_et, date, hour, open_price, close_price, actual_result,
prediction, probability, actual_result, is_correct, dlinear_prediction, dlinear_prob, dlinear_correct,
open_price, close_price) nbeats_prediction, nbeats_prob, nbeats_correct,
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ttm_prediction, ttm_prob, ttm_correct,
ensemble_prediction, ensemble_votes, ensemble_correct)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
prediction = VALUES(prediction),
probability = VALUES(probability),
actual_result = VALUES(actual_result),
is_correct = VALUES(is_correct),
open_price = VALUES(open_price), open_price = VALUES(open_price),
close_price = VALUES(close_price) close_price = VALUES(close_price),
actual_result = VALUES(actual_result),
dlinear_prediction = VALUES(dlinear_prediction),
dlinear_prob = VALUES(dlinear_prob),
dlinear_correct = VALUES(dlinear_correct),
nbeats_prediction = VALUES(nbeats_prediction),
nbeats_prob = VALUES(nbeats_prob),
nbeats_correct = VALUES(nbeats_correct),
ttm_prediction = VALUES(ttm_prediction),
ttm_prob = VALUES(ttm_prob),
ttm_correct = VALUES(ttm_correct),
ensemble_prediction = VALUES(ensemble_prediction),
ensemble_votes = VALUES(ensemble_votes),
ensemble_correct = VALUES(ensemble_correct)
""" """
cursor.execute(sql, ( cursor.execute(sql, (
data.get('timestamp'), data.get('timestamp'),
data.get('timestamp_et'), data.get('timestamp_et'),
data.get('date'), data.get('date'),
data.get('hour'), data.get('hour'),
data.get('model_name'),
data.get('prediction'),
data.get('probability'),
data.get('actual_result'),
data.get('is_correct'),
data.get('open_price'), data.get('open_price'),
data.get('close_price') data.get('close_price'),
data.get('actual_result'),
data.get('dlinear_prediction'),
data.get('dlinear_prob'),
data.get('dlinear_correct'),
data.get('nbeats_prediction'),
data.get('nbeats_prob'),
data.get('nbeats_correct'),
data.get('ttm_prediction'),
data.get('ttm_prob'),
data.get('ttm_correct'),
data.get('ensemble_prediction'),
str(data.get('ensemble_votes')) if data.get('ensemble_votes') else None,
data.get('ensemble_correct')
)) ))
conn.commit() conn.commit()
return cursor.lastrowid or 0 return cursor.lastrowid or 0
@staticmethod @staticmethod
def update_actual_result(date: str, hour: int, model_name: str, def update_actual_result_v2(date: str, hour: int, actual_result: int, close_price: float) -> bool:
actual_result: int, close_price: float) -> bool: """
"""更新实际结果""" 更新实际结果,并重新计算各模型正确性 (V2)
"""
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# 先获取预测值 # 获取该小时记录
cursor.execute(""" cursor.execute("""
SELECT prediction FROM model_predictions SELECT * FROM hourly_model_predictions
WHERE date = %s AND hour = %s AND model_name = %s WHERE date = %s AND hour = %s
""", (date, hour, model_name)) """, (date, hour))
row = cursor.fetchone() row = cursor.fetchone()
if not row: if not row:
return False return False
prediction = row['prediction'] # 计算正确性
is_correct = 1 if prediction == actual_result else 0 updates = {
'actual_result': actual_result,
'close_price': close_price
}
cursor.execute(""" # DLinear
UPDATE model_predictions if row['dlinear_prediction'] is not None:
SET actual_result = %s, is_correct = %s, close_price = %s updates['dlinear_correct'] = 1 if row['dlinear_prediction'] == actual_result else 0
WHERE date = %s AND hour = %s AND model_name = %s
""", (actual_result, is_correct, close_price, date, hour, model_name)) # N-BEATS
if row['nbeats_prediction'] is not None:
updates['nbeats_correct'] = 1 if row['nbeats_prediction'] == actual_result else 0
# TTM
if row['ttm_prediction'] is not None:
updates['ttm_correct'] = 1 if row['ttm_prediction'] == actual_result else 0
# Ensemble
if row['ensemble_prediction'] is not None:
updates['ensemble_correct'] = 1 if row['ensemble_prediction'] == actual_result else 0
# 构建更新SQL
set_clause = ", ".join([f"{k} = %s" for k in updates.keys()])
sql = f"UPDATE hourly_model_predictions SET {set_clause} WHERE date = %s AND hour = %s"
cursor.execute(sql, list(updates.values()) + [date, hour])
conn.commit() conn.commit()
return True return True
@staticmethod @staticmethod
def get_monthly_stats(month: str) -> Dict: def get_monthly_stats_v2(month: str) -> Dict:
""" """
获取月度统计 获取月度统计 (V2 - 从宽表)
修改:不使用 SQL 进行时区转换和格式化,而是查询 UTC 时间范围,然后在 Python 中处理
Args:
month: 月份 (YYYY-MM)
Returns:
各模型的统计数据
""" """
# 计算 UTC 时间范围
try:
year, mon = map(int, month.split('-'))
start_date_cst = datetime(year, mon, 1, tzinfo=TZ_CST)
# 下个月第一天
if mon == 12:
end_date_cst = datetime(year + 1, 1, 1, tzinfo=TZ_CST)
else:
end_date_cst = datetime(year, mon + 1, 1, tzinfo=TZ_CST)
# 转换为 UTC
start_dt_utc = start_date_cst.astimezone(TZ_UTC)
end_dt_utc = end_date_cst.astimezone(TZ_UTC)
start_str = start_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
end_str = end_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
except Exception as e:
print(f"日期解析错误: {e}")
return {}
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
sql = """ # 统计各个模型的准确率
SELECT models = ['dlinear', 'nbeats', 'ttm', 'ensemble']
model_name,
COUNT(*) as total,
SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct,
SUM(CASE WHEN is_correct = 0 THEN 1 ELSE 0 END) as incorrect
FROM model_predictions
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s
AND actual_result IS NOT NULL
GROUP BY model_name
"""
cursor.execute(sql, (month,))
rows = cursor.fetchall()
result = {} result = {}
for row in rows: for model in models:
row_dict = _dict_from_row(row) col_pred = f"{model}_prediction"
model = row_dict['model_name'] col_correct = f"{model}_correct"
total = row_dict['total']
correct = row_dict['correct'] or 0 # 查询 UTC 时间范围内的所有数据
incorrect = row_dict['incorrect'] or 0 sql = f"""
SELECT
COUNT({col_pred}) as total,
SUM(CASE WHEN {col_correct} = 1 THEN 1 ELSE 0 END) as correct,
SUM(CASE WHEN {col_correct} = 0 THEN 1 ELSE 0 END) as incorrect
FROM hourly_model_predictions
WHERE timestamp >= %s AND timestamp < %s
AND actual_result IS NOT NULL
"""
cursor.execute(sql, (start_str, end_str))
row = cursor.fetchone()
if row:
total = int(row['total'])
correct = int(row['correct'] or 0)
incorrect = int(row['incorrect'] or 0)
accuracy = (correct / total * 100) if total > 0 else 0 accuracy = (correct / total * 100) if total > 0 else 0
result[model] = { # 格式化键名以匹配前端
model_key = {
'dlinear': 'DLinear',
'nbeats': 'N-BEATS',
'ttm': 'TTM-R2',
'ensemble': 'Ensemble'
}.get(model, model)
result[model_key] = {
"total": total, "total": total,
"correct": correct, "correct": correct,
"incorrect": incorrect, "incorrect": incorrect,
@@ -332,66 +411,118 @@ class PredictionModel:
return result return result
@staticmethod @staticmethod
def get_daily_stats(month: str, page: int = 1, limit: int = 10) -> Dict: def get_daily_stats_v2(month: str, page: int = 1, limit: int = 10) -> Dict:
""" """
获取每日统计 获取每日统计 (V2 - 从宽表)
修改:不使用 SQL 进行时区转换,查询 UTC 范围,然后在 Python 中转换
"""
# 计算 UTC 时间范围
try:
year, mon = map(int, month.split('-'))
start_date_cst = datetime(year, mon, 1, tzinfo=TZ_CST)
if mon == 12:
end_date_cst = datetime(year + 1, 1, 1, tzinfo=TZ_CST)
else:
end_date_cst = datetime(year, mon + 1, 1, tzinfo=TZ_CST)
start_dt_utc = start_date_cst.astimezone(TZ_UTC)
end_dt_utc = end_date_cst.astimezone(TZ_UTC)
start_str = start_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
end_str = end_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
except Exception:
return {"data": [], "page": page, "limit": limit, "total": 0, "total_pages": 0}
Args:
month: 月份 (YYYY-MM)
page: 页码
limit: 每页条数
"""
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# 获取范围内的所有原始数据
# 我们需要 timestamp, actual_result, 和各模型的 prediction/correct
# 为了按 CST 日期分组,我们需要取出每一行,然后在 Python 中转换时间
sql = """ sql = """
SELECT SELECT
DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as date_cst, timestamp,
model_name, actual_result,
COUNT(*) as total, dlinear_prediction, dlinear_correct,
SUM(CASE WHEN is_correct = 1 THEN 1 ELSE 0 END) as correct, nbeats_prediction, nbeats_correct,
SUM(CASE WHEN is_correct = 0 THEN 1 ELSE 0 END) as incorrect ttm_prediction, ttm_correct,
FROM model_predictions ensemble_prediction, ensemble_correct
WHERE DATE_FORMAT(CONVERT_TZ(timestamp, '+00:00', '+08:00'), '%%Y-%%m') = %s FROM hourly_model_predictions
WHERE timestamp >= %s AND timestamp < %s
AND actual_result IS NOT NULL AND actual_result IS NOT NULL
GROUP BY date_cst, model_name ORDER BY timestamp DESC
ORDER BY date_cst DESC
""" """
cursor.execute(sql, (month,)) cursor.execute(sql, (start_str, end_str))
rows = cursor.fetchall() rows = cursor.fetchall()
# 重组数据: 按日期合并不同模型的统计 # 在 Python 中处理分组
daily_data = {} daily_stats = {} # { "YYYY-MM-DD": { "date": "...", "dlinear": {...}, ... } }
for row in rows: for row in rows:
row_dict = _dict_from_row(row) # 转换 timestamp 到 CST
date = str(row_dict['date_cst']) # CST 日期 ts_utc = row['timestamp']
if date not in daily_data: # 确保是带时区的 datetime
daily_data[date] = {"date": date} if isinstance(ts_utc, str):
ts_utc = datetime.fromisoformat(str(ts_utc).replace('Z', '+00:00'))
if ts_utc.tzinfo is None:
ts_utc = ts_utc.replace(tzinfo=TZ_UTC)
model = row_dict['model_name'] ts_cst = ts_utc.astimezone(TZ_CST)
total = row_dict['total'] date_str = ts_cst.strftime('%Y-%m-%d')
correct = row_dict['correct'] or 0
incorrect = row_dict['incorrect'] or 0
accuracy = (correct / total * 100) if total > 0 else 0
daily_data[date][model] = { if date_str not in daily_stats:
"total": total, daily_stats[date_str] = {
"correct": correct, "date": date_str,
"incorrect": incorrect, "raw_data": {m: {"total": 0, "correct": 0} for m in ['dlinear', 'nbeats', 'ttm', 'ensemble']}
"accuracy": round(accuracy, 2)
} }
# 排序并分页 # 累加统计
sorted_dates = sorted(daily_data.keys(), reverse=True) models = ['dlinear', 'nbeats', 'ttm', 'ensemble']
for model in models:
pred = row[f"{model}_prediction"]
correct_flag = row[f"{model}_correct"]
if pred is not None:
daily_stats[date_str]['raw_data'][model]['total'] += 1
if correct_flag == 1:
daily_stats[date_str]['raw_data'][model]['correct'] += 1
# 整理为最终格式
sorted_dates = sorted(daily_stats.keys(), reverse=True)
total_count = len(sorted_dates) total_count = len(sorted_dates)
# 分页
offset = (page - 1) * limit offset = (page - 1) * limit
paginated_dates = sorted_dates[offset:offset + limit] paginated_dates = sorted_dates[offset:offset + limit]
data = [daily_data[d] for d in paginated_dates] result_data = []
for date_key in paginated_dates:
day_item = daily_stats[date_key]
formatted_item = {"date": day_item["date"]}
for model in ['dlinear', 'nbeats', 'ttm', 'ensemble']:
raw = day_item['raw_data'][model]
total = int(raw['total'])
correct = int(raw['correct'])
accuracy = (correct / total * 100) if total > 0 else 0
model_key = {
'dlinear': 'DLinear',
'nbeats': 'N-BEATS',
'ttm': 'TTM-R2',
'ensemble': 'Ensemble'
}.get(model, model)
formatted_item[model_key] = {
"total": total,
"correct": correct,
"accuracy": round(accuracy, 2)
}
result_data.append(formatted_item)
return { return {
"data": data, "data": result_data,
"page": page, "page": page,
"limit": limit, "limit": limit,
"total": total_count, "total": total_count,
@@ -399,74 +530,113 @@ class PredictionModel:
} }
@staticmethod @staticmethod
def get_hourly_details(date: str) -> List[Dict]: def get_hourly_details_v2(date: str) -> List[Dict]:
""" """
获取指定日期的每小时详情 获取指定日期的每小时详情 (V2)
修改:根据传入的 CST 日期计算 UTC 范围,查询该范围内的数据
"""
try:
# date 是 'YYYY-MM-DD' (CST)
date_cst_start = datetime.strptime(date, '%Y-%m-%d').replace(tzinfo=TZ_CST)
date_cst_end = date_cst_start + timedelta(days=1)
start_dt_utc = date_cst_start.astimezone(TZ_UTC)
end_dt_utc = date_cst_end.astimezone(TZ_UTC)
start_str = start_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
end_str = end_dt_utc.strftime('%Y-%m-%d %H:%M:%S')
except Exception as e:
print(f"日期处理错误: {e}")
return []
Args:
date: 日期 (YYYY-MM-DD, CST 北京时间)
"""
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# 按 CST 日期筛选,并获取 CST 小时
cursor.execute(""" cursor.execute("""
SELECT SELECT *
*, FROM hourly_model_predictions
HOUR(CONVERT_TZ(timestamp, '+00:00', '+08:00')) as hour_cst, WHERE timestamp >= %s AND timestamp < %s
CONVERT_TZ(timestamp, '+00:00', '+08:00') as timestamp_cst_dt ORDER BY timestamp ASC
FROM model_predictions """, (start_str, end_str))
WHERE DATE(CONVERT_TZ(timestamp, '+00:00', '+08:00')) = %s
ORDER BY hour_cst, model_name
""", (date,))
rows = cursor.fetchall() rows = cursor.fetchall()
result = []
# 按 CST 小时重组数据
hourly_data = {}
for row in rows: for row in rows:
row_dict = _dict_from_row(row) # 计算 CST 小时用于显示排序 (虽然 SQL 已经按 timestamp 排序了,但为了确保前端显示 hour 字段正确)
hour_cst = row_dict['hour_cst'] # 使用 CST 小时 ts_utc = row['timestamp']
if hour_cst not in hourly_data: if isinstance(ts_utc, str):
# 将 timestamp 转换为 CST 时间字符串 ts_utc = datetime.fromisoformat(str(ts_utc).replace('Z', '+00:00'))
timestamp_cst = convert_to_cst(row_dict.get('timestamp', '')) if ts_utc.tzinfo is None:
hourly_data[hour_cst] = { ts_utc = ts_utc.replace(tzinfo=TZ_UTC)
"hour": hour_cst, # 返回 CST 小时 ts_cst = ts_utc.astimezone(TZ_CST)
"timestamp_et": row_dict['timestamp_et'], hour_cst = ts_cst.hour
"timestamp_cst": timestamp_cst,
"open_price": row_dict['open_price'],
"close_price": row_dict['close_price'],
"actual_result": row_dict['actual_result'],
"models": {}
}
model = row_dict['model_name'] hour_data = {
hourly_data[hour_cst]["models"][model] = { "hour": hour_cst,
"prediction": row_dict['prediction'], "timestamp_et": row['timestamp_et'],
"probability": row_dict['probability'], "timestamp_cst": convert_to_cst(row.get('timestamp', '')),
"is_correct": row_dict['is_correct'] "open_price": row['open_price'],
"close_price": row['close_price'],
"actual_result": row['actual_result'],
"models": {
"DLinear": {
"prediction": row['dlinear_prediction'],
"probability": row['dlinear_prob'],
"is_correct": row['dlinear_correct']
},
"N-BEATS": {
"prediction": row['nbeats_prediction'],
"probability": row['nbeats_prob'],
"is_correct": row['nbeats_correct']
},
"TTM-R2": {
"prediction": row['ttm_prediction'],
"probability": row['ttm_prob'],
"is_correct": row['ttm_correct']
},
"Ensemble": {
"prediction": row['ensemble_prediction'],
"is_correct": row['ensemble_correct'],
"votes": row['ensemble_votes']
} }
}
}
result.append(hour_data)
# 按 CST 小时排序返回 return result
return [hourly_data[h] for h in sorted(hourly_data.keys())]
@staticmethod @staticmethod
def get_missing_hours(date: str, model_name: str) -> List[int]: def save_prediction(data: Dict) -> int:
"""兼容旧接口 - 如果需要的话,暂时保留,或者可以将旧表写入也放在这里"""
# 旧表写入逻辑... 略,或保持不变
# 为了完整性,我们可以保留对旧表的操作,或者逐步弃用。
# 这里为了安全,保留原方法的签名,实际调用旧表逻辑。
# 但我们主要逻辑将切换到 save_hourly_prediction
# 见 prediction_service.py 的修改
# ... (keep old logic or redirect) ...
# 这里我们假设 prediction_service.py 会改为调用 save_hourly_prediction
# 如果还有地方调用 save_prediction (比如测试代码), 我们可以保留它
return 0
@staticmethod
def update_actual_result(date: str, hour: int, model_name: str,
actual_result: int, close_price: float) -> bool:
"""兼容旧接口"""
return False
@staticmethod
def get_missing_hours(date: str, model_name: str = 'Ensemble') -> List[int]:
""" """
获取指定日期缺少预测的小时列表 获取指定日期缺少预测的小时列表 (V2)
model_name 参数现在主要用于兼容,实际检查整行是否存在
Args:
date: 日期 (YYYY-MM-DD)
model_name: 模型名称
Returns:
缺少预测的小时列表
""" """
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
SELECT hour FROM model_predictions SELECT hour FROM hourly_model_predictions
WHERE date = %s AND model_name = %s WHERE date = %s
""", (date, model_name)) """, (date,))
rows = cursor.fetchall() rows = cursor.fetchall()
existing_hours = {row['hour'] for row in rows} existing_hours = {row['hour'] for row in rows}
+107 -256
View File
@@ -1,147 +1,42 @@
""" """
预测服务 - DLinear 和 TTM-R2 模型预测 预测服务 - 统一模型预测 (DLinear, N-BEATS, TTM-R2)
""" """
import os import os
import sys import sys
import pickle import threading
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, List, Dict, Tuple from typing import Optional, List, Dict, Tuple
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import torch
import numpy as np
import pandas as pd import pandas as pd
import pandas_ta as ta import numpy as np
from sklearn.preprocessing import StandardScaler
# 添加项目路径 # 添加项目路径
sys.path.insert(0, os.path.dirname(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.fetcher import BinanceDataFetcher
from api.models import PredictionModel from api.models import PredictionModel
from models.inference import get_inference_manager
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)
class PredictionService: class PredictionService:
"""预测服务 - 管理 DLinear 和 TTM-R2 模型""" """预测服务 - 管理多模型预测"""
# 类级别的锁,确保多个请求不会同时生成预测 # 类级别的锁,确保多个请求不会同时生成预测
_prediction_lock = None _prediction_lock = threading.Lock()
def __init__(self): def __init__(self):
import threading
# 初始化锁
if PredictionService._prediction_lock is None:
PredictionService._prediction_lock = threading.Lock()
self.device = torch.device('mps' if torch.backends.mps.is_available() else
'cuda' if torch.cuda.is_available() else 'cpu')
self.et_tz = ZoneInfo('America/New_York') self.et_tz = ZoneInfo('America/New_York')
self.fetcher = BinanceDataFetcher() self.fetcher = BinanceDataFetcher()
self.inference_manager = get_inference_manager()
# 模型和预处理器
self.dlinear_model = None
self.dlinear_preprocessor = None
self.ttm_model = None
# 记录正在处理的日期和月份,避免重复处理 # 记录正在处理的日期和月份,避免重复处理
self._processing_dates = set() self._processing_dates = set()
self._processing_months = set() self._processing_months = set()
self._load_models() # 预加载模型
print("正在预加载预测模型...")
def _load_models(self): self.inference_manager.load_all_models()
"""加载模型"""
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}")
def _get_btc_data(self, days: int = 30) -> pd.DataFrame: def _get_btc_data(self, days: int = 30) -> pd.DataFrame:
"""获取BTC历史数据""" """获取BTC历史数据"""
@@ -158,83 +53,22 @@ class PredictionService:
return df return df
def predict_dlinear(self, df: pd.DataFrame) -> Tuple[int, float]: def predict_dlinear(self, df: pd.DataFrame) -> Tuple[int, float]:
""" """DLinear 预测代理"""
使用 DLinear 预测下一小时的涨跌 pred, prob = self.inference_manager.predict_dlinear(df)
if pred is None:
输入: df 包含历史K线数据 (最后一行是最新的已完成K线)
输出: 预测下一小时相对于 df 最后一行收盘价的涨跌
Returns:
(prediction, probability): 预测结果(0=跌, 1=涨)和概率
"""
# 预处理: 使用全部数据,不去掉最后一行
X, _, _ = self.dlinear_preprocessor.preprocess(df, fit=False)
if len(X) == 0:
return 0, 0.5 return 0, 0.5
return pred, prob if prob is not None else 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
def predict_ttm(self, df: pd.DataFrame) -> Tuple[int, float]: def predict_ttm(self, df: pd.DataFrame) -> Tuple[int, float]:
""" """TTM-R2 预测代理"""
使用 TTM-R2 预测下一小时的涨跌 pred, prob = self.inference_manager.predict_ttm(df)
if pred is None:
输入: df 包含历史K线数据 (最后一行是最新的已完成K线) return 0, 0.5
输出: 预测下一小时相对于 df 最后一行收盘价的涨跌 return pred, prob if prob is not None else 0.5
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)
def predict_current_hour(self) -> Dict: def predict_current_hour(self) -> Dict:
""" """
预测当前小时的涨跌 预测当前小时的涨跌 (多模型集成)
逻辑:
- 获取最新的K线数据 (最后一行是最新已完成的K线)
- 预测下一小时相对于最后一行收盘价的涨跌
Returns:
预测结果字典
""" """
df = self._get_btc_data() df = self._get_btc_data()
@@ -248,7 +82,6 @@ class PredictionService:
last_close = float(df['close'].iloc[-1]) last_close = float(df['close'].iloc[-1])
# 当前正在形成的K线信息 # 当前正在形成的K线信息
current_open = float(df['open'].iloc[-1])
current_price = float(df['close'].iloc[-1]) # 当前价格 current_price = float(df['close'].iloc[-1]) # 当前价格
results = { results = {
@@ -260,49 +93,62 @@ class PredictionService:
"models": {} "models": {}
} }
# DLinear 预测 (预测下一小时相对于 last_close 的涨跌) # 使用 InferenceManager 获取所有预测
dlinear_pred, dlinear_prob = self.predict_dlinear(df) predictions = self.inference_manager.predict_all(df)
# 填充结果
if 'dlinear' in predictions:
results["models"]["DLinear"] = { results["models"]["DLinear"] = {
"prediction": dlinear_pred, "prediction": predictions['dlinear']['prediction'],
"probability": round(dlinear_prob, 4), "probability": round(predictions['dlinear']['probability'], 4),
"direction": "UP" if dlinear_pred == 1 else "DOWN" "direction": "UP" if predictions['dlinear']['prediction'] == 1 else "DOWN"
} }
# TTM-R2 预测 if 'nbeats' in predictions:
ttm_pred, ttm_prob = self.predict_ttm(df) 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"] = { results["models"]["TTM-R2"] = {
"prediction": ttm_pred, "prediction": predictions['ttm']['prediction'],
"probability": round(ttm_prob, 4), "probability": round(predictions['ttm']['probability'], 4),
"direction": "UP" if ttm_pred == 1 else "DOWN" "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 return results
def ensure_predictions_for_date(self, date: str) -> None: def ensure_predictions_for_date(self, date: str) -> None:
""" """
确保指定日期有预测数据,如果没有则生成 确保指定日期有预测数据,如果没有则生成 (V2)
Args: Args:
date: 日期 (YYYY-MM-DD) date: 日期 (YYYY-MM-DD)
""" """
# 使用锁避免并发冲突 # 使用锁避免并发冲突
with PredictionService._prediction_lock: with PredictionService._prediction_lock:
# 检查是否正在处理该日期
if date in self._processing_dates: if date in self._processing_dates:
return return
# 标记正在处理
self._processing_dates.add(date) self._processing_dates.add(date)
try: try:
# 检查是否有缺失的小时 # 检查是否有缺失的小时 (只检查宽表是否有记录)
for model_name in ["DLinear", "TTM-R2"]: missing_hours = PredictionModel.get_missing_hours(date)
missing_hours = PredictionModel.get_missing_hours(date, model_name)
if not missing_hours: if not missing_hours:
continue return
print(f"为 {date} 生成 {model_name} 预测 (缺失 {len(missing_hours)} 小时)...") print(f"为 {date} 生成预测 (缺失 {len(missing_hours)} 小时)...")
# 获取足够的历史数据 # 获取足够的历史数据
target_date = datetime.strptime(date, '%Y-%m-%d') target_date = datetime.strptime(date, '%Y-%m-%d')
@@ -310,13 +156,12 @@ class PredictionService:
if df is None or len(df) < 200: if df is None or len(df) < 200:
print(f" 警告: 数据不足,无法为 {date} 生成预测") print(f" 警告: 数据不足,无法为 {date} 生成预测")
continue return
# 为每个缺失的小时生成预测 # 为每个缺失的小时生成预测
for hour in missing_hours: for hour in missing_hours:
self._generate_prediction_for_hour(df, date, hour, model_name) self._generate_prediction_for_hour(df, date, hour)
finally: finally:
# 移除处理标记
with PredictionService._prediction_lock: with PredictionService._prediction_lock:
self._processing_dates.discard(date) self._processing_dates.discard(date)
@@ -338,22 +183,9 @@ class PredictionService:
print(f"获取历史数据失败: {e}") print(f"获取历史数据失败: {e}")
return None return None
def _generate_prediction_for_hour(self, df: pd.DataFrame, date: str, def _generate_prediction_for_hour(self, df: pd.DataFrame, date: str, hour: int) -> None:
hour: int, model_name: str) -> None:
""" """
为指定小时生成预测 为指定小时生成预测 (V2)
预测逻辑:
- 预测目标: {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: try:
# 1. 构建目标时间 (美东时间 -> UTC) # 1. 构建目标时间 (美东时间 -> UTC)
@@ -368,7 +200,6 @@ class PredictionService:
# 3. 找到目标小时的K线 (用于验证) # 3. 找到目标小时的K线 (用于验证)
target_mask = df['timestamp_dt'] == target_time_utc target_mask = df['timestamp_dt'] == target_time_utc
if not target_mask.any(): if not target_mask.any():
# 目标小时数据不存在,跳过
return return
target_row = df[target_mask].iloc[0] target_row = df[target_mask].iloc[0]
@@ -379,7 +210,6 @@ class PredictionService:
prev_time_utc = target_time_utc - pd.Timedelta(hours=1) prev_time_utc = target_time_utc - pd.Timedelta(hours=1)
prev_mask = df['timestamp_dt'] == prev_time_utc prev_mask = df['timestamp_dt'] == prev_time_utc
if not prev_mask.any(): if not prev_mask.any():
# 前一小时数据不存在,跳过
return return
prev_row = df[prev_mask].iloc[0] prev_row = df[prev_mask].iloc[0]
@@ -390,77 +220,98 @@ class PredictionService:
df_input = df[input_mask].copy() df_input = df[input_mask].copy()
if len(df_input) < 200: if len(df_input) < 200:
# 数据不足,跳过
return return
# 6. 调用模型预测 # 6. 调用多模型预测
if model_name == "DLinear": preds = self.inference_manager.predict_all(df_input)
prediction, probability = self.predict_dlinear(df_input)
else: # TTM-R2 # 7. 判断实际涨跌
prediction, probability = self.predict_ttm(df_input) # Check if the candle is closed (current time > candle open time + 1 hour)
current_now_utc = datetime.now(ZoneInfo('UTC')).replace(tzinfo=None)
candle_close_time = target_time_utc + timedelta(hours=1)
is_closed = current_now_utc >= candle_close_time
if not is_closed:
# 仅保存已完成的K线预测,未完成的跳过 (等待完成后再生成)
# 实时交易逻辑在 auto_trade_polymarket.py 中单独处理
return
# 7. 判断实际涨跌 (目标小时收盘价 vs 前一小时收盘价)
actual_result = 1 if target_close > prev_close else 0 actual_result = 1 if target_close > prev_close else 0
# 8. 判断预测是否正确 # 8. 组装数据 (宽表格式)
is_correct = 1 if prediction == actual_result else 0
# 9. 保存预测结果
prediction_data = { prediction_data = {
"timestamp": target_time_utc.isoformat(), "timestamp": target_time_utc.isoformat(),
"timestamp_et": target_time_et.isoformat(), "timestamp_et": target_time_et.isoformat(),
"date": date, "date": date,
"hour": hour, "hour": hour,
"model_name": model_name,
"prediction": prediction,
"probability": probability,
"actual_result": actual_result,
"is_correct": is_correct,
"open_price": target_open, "open_price": target_open,
"close_price": target_close "close_price": target_close,
"actual_result": actual_result,
# DLinear
"dlinear_prediction": preds.get('dlinear', {}).get('prediction'),
"dlinear_prob": preds.get('dlinear', {}).get('probability'),
"dlinear_correct": 1 if preds.get('dlinear', {}).get('prediction') == actual_result else 0,
# N-BEATS
"nbeats_prediction": preds.get('nbeats', {}).get('prediction'),
"nbeats_prob": preds.get('nbeats', {}).get('probability'),
"nbeats_correct": 1 if preds.get('nbeats', {}).get('prediction') == actual_result else 0,
# TTM-R2
"ttm_prediction": preds.get('ttm', {}).get('prediction'),
"ttm_prob": preds.get('ttm', {}).get('probability'),
"ttm_correct": 1 if preds.get('ttm', {}).get('prediction') == actual_result else 0,
# Ensemble
"ensemble_prediction": preds.get('ensemble', {}).get('prediction'),
"ensemble_votes": preds.get('ensemble', {}).get('votes'),
"ensemble_correct": 1 if preds.get('ensemble', {}).get('prediction') == actual_result else 0,
} }
PredictionModel.save_prediction(prediction_data) PredictionModel.save_hourly_prediction(prediction_data)
except Exception as e: except Exception as e:
print(f"生成预测失败 ({date} {hour}:00 {model_name}): {e}") print(f"生成预测失败 ({date} {hour}:00): {e}")
def ensure_predictions_for_month(self, month: str) -> None: def ensure_predictions_for_month(self, month: str) -> None:
""" """
确保指定月份有预测数据 确保指定月份有预测数据 (CST)
需要检查:
1. 该月的所有日期 (ET)
2. 上个月的最后一天 (ET) -> 因为 ET 上个月最后一天 11:00-23:00 对应 CST 该月第一天 00:00-12:00
Args: Args:
month: 月份 (YYYY-MM) month: 月份 (YYYY-MM)
""" """
# 使用锁避免并发冲突 # 使用锁避免并发冲突
with PredictionService._prediction_lock: with PredictionService._prediction_lock:
# 检查是否正在处理该月份
if month in self._processing_months: if month in self._processing_months:
return return
# 标记正在处理
self._processing_months.add(month) self._processing_months.add(month)
try: try:
# 解析月份
year, mon = map(int, month.split('-')) year, mon = map(int, month.split('-'))
# 计算该月的日期范围
from calendar import monthrange from calendar import monthrange
_, last_day = monthrange(year, mon) _, last_day = monthrange(year, mon)
# 对于过去的日期,确保有预测数据 # 1. 确保上个月最后一天有数据 (为了 CST 的开头)
first_day_of_month = datetime(year, mon, 1).date()
prev_day = first_day_of_month - timedelta(days=1)
prev_day_str = prev_day.strftime('%Y-%m-%d')
self.ensure_predictions_for_date(prev_day_str)
today = datetime.now(self.et_tz).date() today = datetime.now(self.et_tz).date()
# 2. 确保本月所有日期
for day in range(1, last_day + 1): for day in range(1, last_day + 1):
date = f"{year}-{mon:02d}-{day:02d}" date = f"{year}-{mon:02d}-{day:02d}"
date_obj = datetime.strptime(date, '%Y-%m-%d').date() date_obj = datetime.strptime(date, '%Y-%m-%d').date()
# 只处理过去和今天的日期
if date_obj <= today: if date_obj <= today:
self.ensure_predictions_for_date(date) self.ensure_predictions_for_date(date)
finally: finally:
# 移除处理标记
with PredictionService._prediction_lock: with PredictionService._prediction_lock:
self._processing_months.discard(month) self._processing_months.discard(month)
+131 -77
View File
@@ -74,8 +74,8 @@
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<div class="stat-card"> <div class="stat-card">
<div class="stat-label">价格</div> <div class="stat-label">策略</div>
<div class="stat-value">${{ currentTrade.price?.toFixed(3) || '-' }}</div> <div class="stat-value small">{{ currentTrade.strategy || 'Ensemble' }}</div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
@@ -116,6 +116,7 @@
<tr> <tr>
<th>时间 (北京)</th> <th>时间 (北京)</th>
<th>市场</th> <th>市场</th>
<th>策略</th>
<th>预测</th> <th>预测</th>
<th>概率</th> <th>概率</th>
<th>价格</th> <th>价格</th>
@@ -128,6 +129,9 @@
<td class="text-truncate" style="max-width: 200px;"> <td class="text-truncate" style="max-width: 200px;">
{{ trade.market_title }} {{ trade.market_title }}
</td> </td>
<td>
<span class="badge bg-secondary">{{ trade.strategy || 'Ensemble' }}</span>
</td>
<td :class="getPredictionClass(trade.prediction)"> <td :class="getPredictionClass(trade.prediction)">
<i :class="trade.prediction === 'UP' ? 'bi-arrow-up' : 'bi-arrow-down'"></i> <i :class="trade.prediction === 'UP' ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
{{ trade.prediction }} {{ trade.prediction }}
@@ -139,7 +143,7 @@
</td> </td>
</tr> </tr>
<tr v-if="!tradeHistory.data?.length"> <tr v-if="!tradeHistory.data?.length">
<td colspan="6" class="text-center text-muted py-4"> <td colspan="7" class="text-center text-muted py-4">
暂无数据 暂无数据
</td> </td>
</tr> </tr>
@@ -184,66 +188,80 @@
<!-- 当月汇总卡片 --> <!-- 当月汇总卡片 -->
<div class="row mb-4"> <div class="row mb-4">
<!-- DLinear 卡片 --> <!-- Ensemble 卡片 -->
<div class="col-md-6"> <div class="col-md-3">
<div class="card h-100 border-primary"> <div class="card h-100 border-primary shadow-sm">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white text-center">
<i class="bi bi-graph-up-arrow"></i> DLinear 模型 <i class="bi bi-diagram-3"></i> 集成策略 (投票)
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row text-center" v-if="monthlyStats.DLinear"> <div class="text-center" v-if="monthlyStats.Ensemble">
<div class="col-3"> <h2 class="display-6 text-primary fw-bold">{{ monthlyStats.Ensemble.accuracy }}%</h2>
<div class="stat-number">{{ monthlyStats.DLinear.total }}</div> <div class="text-muted mb-2">准确率</div>
<div class="stat-label">预测次数</div> <div class="d-flex justify-content-between px-3 border-top pt-2">
</div> <span class="text-success"><i class="bi bi-check-lg"></i> {{ monthlyStats.Ensemble.correct }}</span>
<div class="col-3"> <span class="text-secondary">{{ monthlyStats.Ensemble.total }} 次</span>
<div class="stat-number text-success">{{ monthlyStats.DLinear.correct }}</div> <span class="text-danger"><i class="bi bi-x-lg"></i> {{ monthlyStats.Ensemble.incorrect }}</span>
<div class="stat-label">正确</div>
</div>
<div class="col-3">
<div class="stat-number text-danger">{{ monthlyStats.DLinear.incorrect }}</div>
<div class="stat-label">错误</div>
</div>
<div class="col-3">
<div class="stat-number text-primary">{{ monthlyStats.DLinear.accuracy }}%</div>
<div class="stat-label">准确率</div>
</div> </div>
</div> </div>
<div v-else class="text-center text-muted py-3"> <div v-else class="text-center text-muted py-3">暂无数据</div>
暂无数据
</div> </div>
</div> </div>
</div> </div>
<!-- DLinear 卡片 -->
<div class="col-md-3">
<div class="card h-100 border-secondary">
<div class="card-header bg-secondary text-white text-center">
<i class="bi bi-graph-up-arrow"></i> DLinear
</div>
<div class="card-body">
<div class="text-center" v-if="monthlyStats.DLinear">
<h3 class="text-dark">{{ monthlyStats.DLinear.accuracy }}%</h3>
<div class="text-muted small">准确率</div>
<div class="mt-2 small text-muted">
{{ monthlyStats.DLinear.correct }}/{{ monthlyStats.DLinear.total }}
</div>
</div>
<div v-else class="text-center text-muted py-3">暂无数据</div>
</div>
</div>
</div>
<!-- N-BEATS 卡片 -->
<div class="col-md-3">
<div class="card h-100 border-secondary">
<div class="card-header bg-secondary text-white text-center">
<i class="bi bi-soundwave"></i> N-BEATS
</div>
<div class="card-body">
<div class="text-center" v-if="monthlyStats['N-BEATS']">
<h3 class="text-dark">{{ monthlyStats['N-BEATS'].accuracy }}%</h3>
<div class="text-muted small">准确率</div>
<div class="mt-2 small text-muted">
{{ monthlyStats['N-BEATS'].correct }}/{{ monthlyStats['N-BEATS'].total }}
</div>
</div>
<div v-else class="text-center text-muted py-3">暂无数据</div>
</div>
</div>
</div> </div>
<!-- TTM-R2 卡片 --> <!-- TTM-R2 卡片 -->
<div class="col-md-6"> <div class="col-md-3">
<div class="card h-100 border-success"> <div class="card h-100 border-secondary">
<div class="card-header bg-success text-white"> <div class="card-header bg-secondary text-white text-center">
<i class="bi bi-cpu"></i> TTM-R2 模型 <i class="bi bi-cpu"></i> TTM-R2
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row text-center" v-if="monthlyStats['TTM-R2']"> <div class="text-center" v-if="monthlyStats['TTM-R2']">
<div class="col-3"> <h3 class="text-dark">{{ monthlyStats['TTM-R2'].accuracy }}%</h3>
<div class="stat-number">{{ monthlyStats['TTM-R2'].total }}</div> <div class="text-muted small">准确率</div>
<div class="stat-label">预测次数</div> <div class="mt-2 small text-muted">
</div> {{ monthlyStats['TTM-R2'].correct }}/{{ monthlyStats['TTM-R2'].total }}
<div class="col-3">
<div class="stat-number text-success">{{ monthlyStats['TTM-R2'].correct }}</div>
<div class="stat-label">正确</div>
</div>
<div class="col-3">
<div class="stat-number text-danger">{{ monthlyStats['TTM-R2'].incorrect }}</div>
<div class="stat-label">错误</div>
</div>
<div class="col-3">
<div class="stat-number text-primary">{{ monthlyStats['TTM-R2'].accuracy }}%</div>
<div class="stat-label">准确率</div>
</div> </div>
</div> </div>
<div v-else class="text-center text-muted py-3"> <div v-else class="text-center text-muted py-3">暂无数据</div>
暂无数据
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -252,52 +270,61 @@
<!-- 每日准确率表格 --> <!-- 每日准确率表格 -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<i class="bi bi-calendar3"></i> 每日准确率 <i class="bi bi-calendar3"></i> 每日准确率概览
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-hover mb-0 text-center">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th>日期 (北京)</th> <th class="text-start ps-4">日期</th>
<th>DLinear 预测</th> <th>集成策略</th>
<th>DLinear 准确率</th> <th>DLinear</th>
<th>TTM-R2 预测</th> <th>N-BEATS</th>
<th>TTM-R2 准确率</th> <th>TTM-R2</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="day in dailyStats.data" :key="day.date"> <tr v-for="day in dailyStats.data" :key="day.date">
<td>{{ day.date }}</td> <td class="text-start ps-4">{{ day.date }}</td>
<!-- Ensemble -->
<td> <td>
<span v-if="day.DLinear"> <span v-if="day.Ensemble" :class="getAccuracyClass(day.Ensemble.accuracy)">
{{ day.DLinear.correct }}/{{ day.DLinear.total }} {{ day.Ensemble.accuracy }}%
</span> </span>
<span v-else class="text-muted">-</span> <span v-else class="text-muted">-</span>
</td> </td>
<!-- DLinear -->
<td> <td>
<span v-if="day.DLinear" :class="getAccuracyClass(day.DLinear.accuracy)"> <span v-if="day.DLinear" :class="getAccuracyClass(day.DLinear.accuracy)">
{{ day.DLinear.accuracy }}% {{ day.DLinear.accuracy }}%
</span> </span>
<span v-else class="text-muted">-</span> <span v-else class="text-muted">-</span>
</td> </td>
<!-- N-BEATS -->
<td> <td>
<span v-if="day['TTM-R2']"> <span v-if="day['N-BEATS']" :class="getAccuracyClass(day['N-BEATS'].accuracy)">
{{ day['TTM-R2'].correct }}/{{ day['TTM-R2'].total }} {{ day['N-BEATS'].accuracy }}%
</span> </span>
<span v-else class="text-muted">-</span> <span v-else class="text-muted">-</span>
</td> </td>
<!-- TTM-R2 -->
<td> <td>
<span v-if="day['TTM-R2']" :class="getAccuracyClass(day['TTM-R2'].accuracy)"> <span v-if="day['TTM-R2']" :class="getAccuracyClass(day['TTM-R2'].accuracy)">
{{ day['TTM-R2'].accuracy }}% {{ day['TTM-R2'].accuracy }}%
</span> </span>
<span v-else class="text-muted">-</span> <span v-else class="text-muted">-</span>
</td> </td>
<td> <td>
<button class="btn btn-sm btn-outline-primary" <button class="btn btn-sm btn-outline-primary"
@click="showHourlyDetails(day.date)"> @click="showHourlyDetails(day.date)">
<i class="bi bi-eye"></i> 查看 <i class="bi bi-eye"></i> 详情
</button> </button>
</td> </td>
</tr> </tr>
@@ -337,7 +364,7 @@
<!-- 每小时详情弹窗 --> <!-- 每小时详情弹窗 -->
<div class="modal fade" id="hourlyModal" tabindex="-1"> <div class="modal fade" id="hourlyModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"> <h5 class="modal-title">
@@ -346,43 +373,70 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body p-0"> <div class="modal-body p-0">
<table class="table table-sm table-hover mb-0"> <table class="table table-sm table-hover mb-0 text-center align-middle">
<thead class="table-light sticky-top"> <thead class="table-light sticky-top">
<tr> <tr>
<th>时间 (北京)</th> <th>时间</th>
<th>实际涨跌</th> <th>开盘</th>
<th>收盘</th>
<th>实际</th>
<th class="table-primary">集成策略</th>
<th>DLinear</th> <th>DLinear</th>
<th>N-BEATS</th>
<th>TTM-R2</th> <th>TTM-R2</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="hour in hourlyModal.data" :key="hour.hour"> <tr v-for="hour in hourlyModal.data" :key="hour.hour">
<td>{{ hour.timestamp_cst || (hour.hour + ':00') }}</td> <td>{{ hour.timestamp_cst ? hour.timestamp_cst.split(' ')[1] : (hour.hour + ':00') }}</td>
<td :class="hour.actual_result === 1 ? 'text-success' : 'text-danger'"> <td>${{ hour.open_price?.toFixed(0) || '-' }}</td>
<i :class="hour.actual_result === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i> <td>${{ hour.close_price?.toFixed(0) || '-' }}</td>
{{ hour.actual_result === 1 ? 'UP' : 'DOWN' }}
<td :class="hour.actual_result === 1 ? 'text-success fw-bold' : (hour.actual_result === 0 ? 'text-danger fw-bold' : 'text-muted')">
<i v-if="hour.actual_result !== null" :class="hour.actual_result === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
{{ hour.actual_result === 1 ? 'UP' : (hour.actual_result === 0 ? 'DOWN' : '-') }}
</td> </td>
<!-- Ensemble -->
<td class="table-primary bg-opacity-10">
<span v-if="hour.models?.Ensemble"
:class="hour.models.Ensemble.is_correct ? 'text-success' : 'text-danger'">
<i :class="hour.models.Ensemble.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
{{ hour.models.Ensemble.prediction === 1 ? 'UP' : 'DOWN' }}
<i v-if="hour.actual_result !== null" :class="hour.models.Ensemble.is_correct ? 'bi-check-circle-fill' : 'bi-x-circle-fill'"></i>
</span>
</td>
<!-- DLinear -->
<td> <td>
<span v-if="hour.models?.DLinear" <span v-if="hour.models?.DLinear"
:class="hour.models.DLinear.is_correct ? 'text-success' : 'text-danger'"> :class="hour.models.DLinear.is_correct ? 'text-success' : 'text-danger'">
<i :class="hour.models.DLinear.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i> <i :class="hour.models.DLinear.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
{{ hour.models.DLinear.prediction === 1 ? 'UP' : 'DOWN' }} <span class="small text-muted">({{ (hour.models.DLinear.probability * 100).toFixed(0) }}%)</span>
<i :class="hour.models.DLinear.is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i> <i v-if="hour.actual_result !== null" :class="hour.models.DLinear.is_correct ? 'bi-check' : 'bi-x'"></i>
</span> </span>
<span v-else class="text-muted">-</span>
</td> </td>
<!-- N-BEATS -->
<td>
<span v-if="hour.models?.['N-BEATS']"
:class="hour.models['N-BEATS'].is_correct ? 'text-success' : 'text-danger'">
<i :class="hour.models['N-BEATS'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
<i v-if="hour.actual_result !== null" :class="hour.models['N-BEATS'].is_correct ? 'bi-check' : 'bi-x'"></i>
</span>
</td>
<!-- TTM-R2 -->
<td> <td>
<span v-if="hour.models?.['TTM-R2']" <span v-if="hour.models?.['TTM-R2']"
:class="hour.models['TTM-R2'].is_correct ? 'text-success' : 'text-danger'"> :class="hour.models['TTM-R2'].is_correct ? 'text-success' : 'text-danger'">
<i :class="hour.models['TTM-R2'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i> <i :class="hour.models['TTM-R2'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
{{ hour.models['TTM-R2'].prediction === 1 ? 'UP' : 'DOWN' }} <i v-if="hour.actual_result !== null" :class="hour.models['TTM-R2'].is_correct ? 'bi-check' : 'bi-x'"></i>
<i :class="hour.models['TTM-R2'].is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i>
</span> </span>
<span v-else class="text-muted">-</span>
</td> </td>
</tr> </tr>
<tr v-if="!hourlyModal.data?.length"> <tr v-if="!hourlyModal.data?.length">
<td colspan="4" class="text-center text-muted py-4"> <td colspan="8" class="text-center text-muted py-4">
暂无数据 暂无数据
</td> </td>
</tr> </tr>
+380
View File
@@ -0,0 +1,380 @@
"""对比 test 旧逻辑 vs auto_trade 逻辑的数据处理差异
两种预处理方式:
- test旧逻辑:整个df一次性预处理,无create_labels,按索引遍历窗口
- auto_trade逻辑:截取到当前K线的df,含create_labels,取X[-1]预测
对比内容:
1. 预处理后的df行数差异
2. 同一时间点的特征值差异(RSI、MACD等)
3. scaler transform后的值差异
4. 最终输入窗口的数值差异
5. 模型输出logits/概率/预测结果差异
"""
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
# ==================== pickle反序列化需要的占位类 ====================
class ClassificationPreprocessor:
"""pkl文件中保存的类,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
# ==================== 两种预处理器 ====================
class TestPreprocessor:
"""test旧逻辑:无create_labels"""
def __init__(self, lookback=168):
self.lookback = lookback
self.scaler = StandardScaler()
def preprocess(self, df, fit=True):
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]
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])
else:
scaled_features = self.scaler.transform(df[feature_cols])
X = []
valid_indices = []
for i in range(len(scaled_features) - self.lookback):
X.append(scaled_features[i:i + self.lookback])
valid_indices.append(i + self.lookback - 1)
return np.array(X), df, valid_indices, scaled_features
class AutoTradePreprocessor:
"""auto_trade逻辑:含create_labels"""
def __init__(self, lookback=168, forecast_horizon=1):
self.lookback = lookback
self.forecast_horizon = forecast_horizon
self.scaler = StandardScaler()
def preprocess(self, df, fit=True):
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(-self.forecast_horizon)
df['future_return'] = (df['future_price'] - df['close']) / df['close']
df['label'] = (df['future_return'] > 0).astype(int)
df = df[:-self.forecast_horizon]
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])
else:
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, scaled_features
def utc_to_beijing(ts):
return ts + timedelta(hours=8)
def main():
print("=" * 100)
print("数据处理对比:test旧逻辑 vs auto_trade逻辑")
print("=" * 100)
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
# 加载模型和训练时的scaler
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']).to(device).eval()
with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f:
trained_preprocessor = pickle.load(f)
# 提取训练时的scaler参数
trained_scaler = trained_preprocessor.scaler
print(f"训练时scaler均值: {trained_scaler.mean_}")
print(f"训练时scaler方差: {trained_scaler.scale_}")
# 获取数据
print("\n获取数据...")
fetcher = BinanceDataFetcher()
df_full = fetcher.fetch_ohlcv('BTC', '1h', start_date='2025-12-01', end_date='2026-03-01', use_cache=False)
print(f"总行数: {len(df_full)}")
# 选几个2月10日(北京时间)的时间点做对比
# 北京时间02-10 00:00 = UTC 02-09 16:00
test_targets_utc = [
'2026-02-09 16:00:00', # 北京 02-10 00:00
'2026-02-09 20:00:00', # 北京 02-10 04:00
'2026-02-10 00:00:00', # 北京 02-10 08:00
'2026-02-10 04:00:00', # 北京 02-10 12:00
'2026-02-10 08:00:00', # 北京 02-10 16:00
'2026-02-10 12:00:00', # 北京 02-10 20:00
]
feature_cols = ['open', 'high', 'low', 'close', 'volume',
'rsi', 'macd', 'macd_hist', 'macd_signal']
for target_utc_str in test_targets_utc:
target_utc = pd.Timestamp(target_utc_str)
target_bj = utc_to_beijing(target_utc)
# 找到目标K线在df中的索引
target_mask = df_full['timestamp'] == target_utc
if not target_mask.any():
print(f"\n⚠ 找不到 {target_utc_str},跳过")
continue
target_idx = df_full[target_mask].index[0]
input_idx = target_idx - 1 # 输入的最后一根K线
input_row = df_full.iloc[input_idx]
target_row = df_full.iloc[target_idx]
print(f"\n{'=' * 100}")
print(f"目标K线: 北京{target_bj.strftime('%m-%d %H:%M')} (UTC {target_utc_str})")
print(f"输入末根: UTC {input_row['timestamp']} O:{input_row['open']:.2f} H:{input_row['high']:.2f} L:{input_row['low']:.2f} C:{input_row['close']:.2f}")
print(f"目标K线: UTC {target_row['timestamp']} 收盘:{target_row['close']:.2f}")
print("-" * 100)
# ===== 方式A: test旧逻辑 =====
# 对整个df一次性预处理,按索引找到对应窗口
test_prep = TestPreprocessor(lookback=168)
test_prep.scaler = trained_scaler # 使用训练时的scaler
X_test, df_test, idx_test, scaled_test = test_prep.preprocess(df_full, fit=False)
# 找到input_idx对应的窗口
# idx_test[i] = i + 168 - 1,所以 input_idx 在 df_test 中的位置需要查找
# df_test 是 dropna 后 reset_index 的,timestamp 可能对应不同的行号
test_input_mask = df_test['timestamp'] == input_row['timestamp']
if test_input_mask.any():
test_df_idx = df_test[test_input_mask].index[0]
# 对应的窗口序号
test_window_i = test_df_idx - 168 + 1
if 0 <= test_window_i < len(X_test):
X_test_window = X_test[test_window_i]
else:
X_test_window = None
print(" [test] 窗口索引越界")
else:
test_df_idx = None
X_test_window = None
print(" [test] 在df_test中找不到对应时间")
# ===== 方式B: auto_trade逻辑 =====
# 截取到input_idx为止,含create_labels
df_up_to_t = df_full.iloc[:input_idx + 1]
auto_prep = AutoTradePreprocessor(lookback=168)
auto_prep.scaler = trained_scaler # 使用训练时的scaler
X_auto, df_auto, scaled_auto = auto_prep.preprocess(df_up_to_t, fit=False)
if len(X_auto) > 0:
X_auto_window = X_auto[-1] # 取最后一个窗口
else:
X_auto_window = None
print(" [auto] X为空")
# ===== 对比 =====
# 1. df行数对比
print(f"\n[1] 预处理后df行数")
print(f" test: {len(df_test)} 行 (整个df, 无create_labels)")
print(f" auto: {len(df_auto)} 行 (截取到input, 含create_labels砍尾)")
# 2. 最后一行时间对比
print(f"\n[2] 预处理后df最后一行")
print(f" test最后一行: {df_test.iloc[-1]['timestamp']} close={df_test.iloc[-1]['close']:.2f}")
print(f" auto最后一行: {df_auto.iloc[-1]['timestamp']} close={df_auto.iloc[-1]['close']:.2f}")
# 3. 输入末根K线的原始特征对比
if test_df_idx is not None:
test_features = df_test.loc[test_df_idx, feature_cols]
auto_last_row = df_auto.iloc[-1]
auto_features = auto_last_row[feature_cols]
print(f"\n[3] 输入末根K线原始特征值")
print(f" {'特征':<15} {'test值':>15} {'auto值':>15} {'差异':>15}")
print(f" {'-'*60}")
for col in feature_cols:
tv = test_features[col]
av = auto_features[col]
diff = tv - av
marker = " ⚠" if abs(diff) > 1e-6 else ""
print(f" {col:<15} {tv:>15.6f} {av:>15.6f} {diff:>15.8f}{marker}")
# 4. scaler transform 后的值对比(输入末根K线)
if test_df_idx is not None and X_test_window is not None and X_auto_window is not None:
test_scaled_last = scaled_test[test_df_idx]
auto_scaled_last = scaled_auto[-1]
print(f"\n[4] scaler transform 后的值(输入末根K线)")
print(f" {'特征':<15} {'test值':>15} {'auto值':>15} {'差异':>15}")
print(f" {'-'*60}")
for j, col in enumerate(feature_cols):
tv = test_scaled_last[j]
av = auto_scaled_last[j]
diff = tv - av
marker = " ⚠" if abs(diff) > 1e-6 else ""
print(f" {col:<15} {tv:>15.6f} {av:>15.6f} {diff:>15.8f}{marker}")
# 5. 输入窗口对比(整个168步窗口)
if X_test_window is not None and X_auto_window is not None:
window_diff = X_test_window - X_auto_window
max_diff = np.max(np.abs(window_diff))
mean_diff = np.mean(np.abs(window_diff))
nonzero_count = np.count_nonzero(np.abs(window_diff) > 1e-6)
print(f"\n[5] 输入窗口对比 (168步 × 9特征 = 1512个值)")
print(f" 最大绝对差异: {max_diff:.10f}")
print(f" 平均绝对差异: {mean_diff:.10f}")
print(f" 差异>1e-6的值: {nonzero_count} / 1512")
if nonzero_count > 0:
# 找到差异最大的位置
max_pos = np.unravel_index(np.argmax(np.abs(window_diff)), window_diff.shape)
print(f" 最大差异位置: 步={max_pos[0]} 特征={feature_cols[max_pos[1]]}")
print(f" test值: {X_test_window[max_pos]:.10f}")
print(f" auto值: {X_auto_window[max_pos]:.10f}")
# 6. 模型推理对比
if X_test_window is not None and X_auto_window is not None:
with torch.no_grad():
test_tensor = torch.FloatTensor(X_test_window[np.newaxis]).to(device)
auto_tensor = torch.FloatTensor(X_auto_window[np.newaxis]).to(device)
test_logits = model(test_tensor).cpu().numpy().item()
auto_logits = model(auto_tensor).cpu().numpy().item()
test_prob = 1 / (1 + np.exp(-test_logits))
auto_prob = 1 / (1 + np.exp(-auto_logits))
test_pred = "涨" if test_prob > 0.5 else "跌"
auto_pred = "涨" if auto_prob > 0.5 else "跌"
match = "✓ 一致" if test_pred == auto_pred else "✗ 不一致"
print(f"\n[6] 模型推理结果")
print(f" {'':15} {'test':>15} {'auto':>15} {'差异':>15}")
print(f" {'-'*60}")
print(f" {'logits':<15} {test_logits:>15.8f} {auto_logits:>15.8f} {test_logits-auto_logits:>15.8f}")
print(f" {'概率':<15} {test_prob:>15.8f} {auto_prob:>15.8f} {test_prob-auto_prob:>15.8f}")
print(f" {'预测':<15} {test_pred:>15} {auto_pred:>15} {match:>15}")
# ===== 全量对比:2月所有小时 =====
print(f"\n\n{'=' * 100}")
print("全量对比:2月份所有小时预测结果差异统计")
print("=" * 100)
# test方式:一次性预处理
test_prep_full = TestPreprocessor(lookback=168)
test_prep_full.scaler = trained_scaler
X_test_full, df_test_full, idx_test_full, _ = test_prep_full.preprocess(df_full, fit=False)
# 构建 timestamp -> test窗口索引 的映射
test_ts_map = {}
for i, idx in enumerate(idx_test_full):
ts = df_test_full.iloc[idx]['timestamp']
test_ts_map[ts] = i
total = 0
same = 0
diff_list = []
for target_idx in range(1, len(df_full)):
target_time_utc = df_full.iloc[target_idx]['timestamp']
target_time_bj = utc_to_beijing(target_time_utc)
if not (target_time_bj.year == 2026 and target_time_bj.month == 2):
continue
input_idx = target_idx - 1
input_ts = df_full.iloc[input_idx]['timestamp']
# test方式
test_i = test_ts_map.get(input_ts)
if test_i is None or test_i >= len(X_test_full):
continue
# auto方式
df_up_to_t = df_full.iloc[:input_idx + 1]
auto_prep_tmp = AutoTradePreprocessor(lookback=168)
auto_prep_tmp.scaler = trained_scaler
X_auto_tmp, _, _ = auto_prep_tmp.preprocess(df_up_to_t, fit=False)
if len(X_auto_tmp) == 0:
continue
with torch.no_grad():
test_logits = model(torch.FloatTensor(X_test_full[test_i:test_i+1]).to(device)).cpu().numpy().item()
auto_logits = model(torch.FloatTensor(X_auto_tmp[-1:]).to(device)).cpu().numpy().item()
test_prob = 1 / (1 + np.exp(-test_logits))
auto_prob = 1 / (1 + np.exp(-auto_logits))
test_pred = 1 if test_prob > 0.5 else 0
auto_pred = 1 if auto_prob > 0.5 else 0
total += 1
if test_pred == auto_pred:
same += 1
else:
diff_list.append({
'time_bj': target_time_bj.strftime('%m-%d %H:%M'),
'test_prob': test_prob,
'auto_prob': auto_prob,
'test_pred': '涨' if test_pred == 1 else '跌',
'auto_pred': '涨' if auto_pred == 1 else '跌',
})
print(f"总预测数: {total}")
print(f"结果一致: {same} ({same/total:.1%})")
print(f"结果不同: {len(diff_list)} ({len(diff_list)/total:.1%})")
if diff_list:
print(f"\n不一致的时间点:")
print(f" {'北京时间':<15} {'test概率':>10} {'test预测':>8} {'auto概率':>10} {'auto预测':>8}")
print(f" {'-'*55}")
for d in diff_list:
print(f" {d['time_bj']:<15} {d['test_prob']:>10.6f} {d['test_pred']:>8} {d['auto_prob']:>10.6f} {d['auto_pred']:>8}")
print("=" * 100)
if __name__ == "__main__":
main()
-460
View File
@@ -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()
+3 -2
View File
@@ -72,8 +72,8 @@ class TradingDatabase:
INSERT INTO trades ( INSERT INTO trades (
timestamp, market_title, market_slug, model_name, timestamp, market_title, market_slug, model_name,
prediction, probability, action, price, amount, size, prediction, probability, action, price, amount, size,
status, error_message, raw_data status, strategy, error_message, raw_data
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
''' '''
cursor.execute(sql, ( cursor.execute(sql, (
@@ -88,6 +88,7 @@ class TradingDatabase:
trade_data.get('amount'), trade_data.get('amount'),
trade_data.get('size'), trade_data.get('size'),
trade_data.get('status'), trade_data.get('status'),
trade_data.get('strategy'),
trade_data.get('error_message'), trade_data.get('error_message'),
raw_data_json raw_data_json
)) ))
-406
View File
@@ -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()
+295
View File
@@ -0,0 +1,295 @@
# Windows 环境安装指南
本指南帮助您在 Windows 系统上部署加密货币预测系统。
---
## 一、前置要求
- Windows 10/11 (64位)
- 至少 8GB 内存
- 至少 10GB 可用磁盘空间
- 网络连接(需访问 GitHub、PyPI、Binance API)
---
## 二、安装 Miniconda
### 1. 下载 Miniconda
访问 [Miniconda 官网](https://docs.conda.io/en/latest/miniconda.html) 下载 Windows 64-bit 安装包,或直接下载:
```
https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe
```
### 2. 安装
1. 双击运行安装程序
2. 选择 "Just Me" (仅为当前用户安装)
3. 安装路径建议使用默认值或 `C:\Miniconda3`
4. **重要**:勾选 "Add Miniconda3 to my PATH environment variable"
5. 完成安装
### 3. 验证安装
打开 **命令提示符 (CMD)** 或 **PowerShell**:
```powershell
conda --version
# 应显示类似: conda 24.x.x
```
---
## 三、创建 Conda 环境
### 1. 创建 Python 3.12 环境
```powershell
conda create -n crypto_pred_312 python=3.12 -y
```
### 2. 激活环境
```powershell
conda activate crypto_pred_312
```
激活后,命令行前缀应显示 `(crypto_pred_312)`
---
## 四、克隆项目
### 1. 安装 Git (如未安装)
下载并安装 [Git for Windows](https://git-scm.com/download/win)
### 2. 克隆仓库
```powershell
cd C:\Projects # 或您喜欢的目录
git clone http://192.168.50.24:3000/subuhui/crpto_predictor.git
cd crpto_predictor
```
---
## 五、安装依赖
### 1. 安装 PyTorch (CPU 版本)
```powershell
# CPU 版本 PyTorch(Windows 不使用 GPU)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
```
### 2. 安装项目依赖
```powershell
pip install -r requirements.txt
```
### 3. 安装 IBM 时序模型 (TTM-R2)
```powershell
# PyPI 包名是 granite-tsfm,导入时用 tsfm_public
pip install granite-tsfm
```
验证安装:
```powershell
python -c "from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction; print('TTM-R2 安装成功')"
```
---
## 六、配置环境变量
### 1. 复制环境变量模板
```powershell
copy .env.example .env
```
### 2. 编辑 .env 文件
使用记事本或 VSCode 编辑 `.env` 文件:
```ini
# MySQL 数据库配置
MYSQL_HOST=your_mysql_host
MYSQL_PORT=3306
MYSQL_USER=your_username
MYSQL_PASSWORD=your_password
MYSQL_DB=crypto_predictor
# Binance API (可选,用于实时数据)
BINANCE_API_KEY=your_api_key
BINANCE_API_SECRET=your_api_secret
# Polymarket 配置 (可选,用于自动交易)
POLYMARKET_PRIVATE_KEY=your_private_key
DRY_RUN=true # 设为 true 进行模拟交易
# 代理配置 (可选)
USE_PROXY=false
SOCKS5_PROXY=socks5://127.0.0.1:1080
```
---
## 七、数据库配置
### 1. 安装 MySQL
下载并安装 [MySQL Community Server](https://dev.mysql.com/downloads/mysql/)
或使用 Docker:
```powershell
docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=yourpassword mysql:8.0
```
### 2. 创建数据库
```sql
CREATE DATABASE crypto_predictor CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
### 3. 初始化表结构
首次运行时会自动创建表,或手动执行:
```powershell
python -c "from api.models import init_model_predictions_table; from data.trading_db import TradingDatabase; TradingDatabase()"
```
---
## 八、验证安装
### 1. 测试 PyTorch
```powershell
python -c "import torch; print(f'PyTorch: {torch.__version__}'); print(f'Device: cpu')"
```
### 2. 测试模型加载
```powershell
python -c "from training.config import TrainingConfig; c = TrainingConfig(); print(f'Device: {c.get_device()}')"
# 应输出: Device: cpu
```
### 3. 测试数据库连接
```powershell
python -c "from api.models import get_db_connection; conn = get_db_connection(); print('数据库连接成功!')"
```
---
## 九、运行系统
### 方式一:双击批处理脚本 (推荐)
双击项目根目录的 `start_system.bat`
### 方式二:PowerShell 脚本
```powershell
.\start_system.ps1
```
### 方式三:Python 脚本
```powershell
python start_system.py
```
### 方式四:分别启动
```powershell
# 终端 1: API 服务
python -m uvicorn api.main:app --host 0.0.0.0 --port 8000
# 终端 2: 自动交易
python auto_trade_polymarket.py
```
---
## 十、访问系统
- **前端看板**: http://localhost:8000
- **API 文档**: http://localhost:8000/docs
---
## 常见问题
### Q1: `conda activate` 不生效
在 PowerShell 中需要先初始化 conda:
```powershell
conda init powershell
# 重启 PowerShell
```
### Q2: pip 安装超时
使用国内镜像:
```powershell
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
```
### Q3: PyTorch 安装失败
确保使用 CPU 版本的安装命令,不要安装 CUDA 版本。
### Q4: 数据库连接失败
1. 检查 MySQL 服务是否运行
2. 检查 `.env` 中的数据库配置
3. 检查防火墙是否允许 3306 端口
### Q5: 模型文件缺失
确保 `models/checkpoints/` 目录包含预训练模型文件。如缺失,联系管理员获取。
---
## 目录结构
```
crpto_predictor/
├── api/ # API 服务
│ ├── main.py # FastAPI 入口
│ ├── models.py # 数据库模型
│ ├── prediction_service.py # 预测服务
│ └── static/ # 前端静态文件
├── models/ # 模型定义和权重
│ ├── checkpoints/ # 模型权重文件
│ └── dlinear/ # DLinear 模型
├── data/ # 数据层
├── training/ # 训练配置
├── start_system.py # 跨平台启动脚本
├── start_system.bat # Windows CMD 脚本
├── start_system.ps1 # Windows PowerShell 脚本
├── auto_trade_polymarket.py # 自动交易程序
├── requirements.txt # 依赖列表
└── .env # 环境变量 (需自行创建)
```
---
## 更新项目
```powershell
git pull origin main
pip install -r requirements.txt
```
---
*如有问题,请联系管理员。*
-395
View File
@@ -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()
-454
View File
@@ -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()
-385
View File
@@ -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()
+58
View File
@@ -0,0 +1,58 @@
@echo off
chcp 65001 >nul
REM ============================================
REM Windows 环境安装脚本
REM 安装所有依赖和模型
REM ============================================
echo ==========================================
echo 加密货币预测系统 - Windows 安装脚本
echo ==========================================
echo.
REM 检查 conda
where conda >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到 conda
echo 请先安装 Miniconda: https://docs.conda.io/en/latest/miniconda.html
pause
exit /b 1
)
echo [1/6] 创建 conda 环境...
call conda create -n crypto_pred_312 python=3.12 -y
if %errorlevel% neq 0 (
echo [警告] 环境可能已存在,继续...
)
echo.
echo [2/6] 激活环境...
call conda activate crypto_pred_312
echo.
echo [3/6] 安装 PyTorch (CPU 版本)...
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
echo.
echo [4/5] 安装项目依赖...
pip install -r requirements.txt
echo.
echo [5/5] 验证安装...
python -c "import torch; print(f'PyTorch: {torch.__version__}')"
python -c "from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction; print('TTM-R2: 已安装')"
python -c "import pandas_ta; print('pandas-ta: 已安装')"
python -c "from training.config import TrainingConfig; c=TrainingConfig(); print(f'设备: {c.get_device()}')"
echo.
echo ==========================================
echo 安装完成!
echo ==========================================
echo.
echo 下一步:
echo 1. 复制 .env.example 为 .env 并配置数据库
echo 2. 双击 start_system.bat 启动系统
echo 3. 访问 http://localhost:8000
echo.
pause
+417
View File
@@ -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()
+52 -28
View File
@@ -1,45 +1,69 @@
# Core dependencies # ==============================================
torch>=2.0.0 # 加密货币预测系统依赖
torchvision # Python 3.12 | 支持 macOS (MPS) 和 Windows (CPU)
torchaudio # ==============================================
python-binance>=1.0.19
# ----------------------------------------------
# PyTorch (根据平台单独安装)
# ----------------------------------------------
# macOS: pip install torch torchvision torchaudio
# Windows CPU: pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
# 注意:requirements.txt 中不包含 torch,需要手动安装
# ----------------------------------------------
# 数据处理
# ----------------------------------------------
pandas>=2.0.0 pandas>=2.0.0
numpy>=1.24.0 numpy>=1.24.0
scikit-learn>=1.3.0 scikit-learn>=1.3.0
pytz pytz
python-dotenv>=1.0.0
# ONNX # ----------------------------------------------
onnx>=1.14.0 # 时序模型
onnxruntime>=1.15.0 # ----------------------------------------------
# IBM Granite TTM-R2 (导入名: tsfm_public)
granite-tsfm>=0.3.0
# Model-specific dependencies # ----------------------------------------------
darts>=0.27.0 # 技术指标
u8darts[torch]>=0.27.0 # ----------------------------------------------
tsfm_public>=0.2.0 pandas-ta>=0.3.14b
gluonts[torch]>=0.14.0
# API dependencies # ----------------------------------------------
# API 服务
# ----------------------------------------------
fastapi>=0.104.0 fastapi>=0.104.0
uvicorn[standard]>=0.24.0 uvicorn[standard]>=0.24.0
pydantic>=2.0.0 pydantic>=2.0.0
python-multipart>=0.0.6 python-multipart>=0.0.6
# Technical indicators # ----------------------------------------------
pandas-ta>=0.3.14b # HTTP 客户端
# ----------------------------------------------
httpx>=0.25.0
# Development tools # ----------------------------------------------
jupyter>=1.0.0 # 数据源
matplotlib>=3.7.0 # ----------------------------------------------
seaborn>=0.12.0 python-binance>=1.0.19
pytest>=7.4.0
black>=23.0.0
flake8>=6.0.0
# Utilities # ----------------------------------------------
tqdm>=4.66.0 # 任务调度
# ----------------------------------------------
schedule>=1.2.0
# Database # ----------------------------------------------
# 数据库
# ----------------------------------------------
pymysql>=1.1.0 pymysql>=1.1.0
sqlalchemy>=2.0.0
cryptography>=41.0.0 cryptography>=41.0.0
# ----------------------------------------------
# 环境变量
# ----------------------------------------------
python-dotenv>=1.0.0
# ----------------------------------------------
# 进度条
# ----------------------------------------------
tqdm>=4.66.0
-458
View File
@@ -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()
+37
View File
@@ -0,0 +1,37 @@
"""导出 DLinear 模型为 ONNX 格式。
用法:
python scripts/export_onnx.py
输出:
models/checkpoints/dlinear_classification_2026/model.onnx
"""
import sys
from pathlib import Path
# 添加项目根目录到路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from models.dlinear.model import DLinear
def main():
# 模型路径
checkpoint_path = project_root / "models/checkpoints/dlinear_classification_2026/best_model.pt"
output_path = checkpoint_path.parent / "model.onnx"
print(f"加载模型: {checkpoint_path}")
model = DLinear.load(str(checkpoint_path), device='cpu')
print(f"模型配置: lookback={model.config.lookback}, input_dim={model.config.input_dim}")
print(f"参数量: {model.count_parameters():,}")
# 导出为 ONNX
model.to_onnx(str(output_path))
print(f"✓ ONNX 模型已保存到: {output_path}")
if __name__ == "__main__":
main()
+91
View File
@@ -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")
-451
View File
@@ -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()
+47
View File
@@ -0,0 +1,47 @@
@echo off
chcp 65001 >nul
REM ============================================
REM 系统启动脚本 (Windows)
REM 启动 API 服务和自动交易程序
REM ============================================
echo [%date% %time%] 正在启动系统...
REM 检查 conda 环境
where conda >nul 2>&1
if %errorlevel% neq 0 (
echo [错误] 未找到 conda,请先安装 Anaconda 或 Miniconda
pause
exit /b 1
)
REM 激活 conda 环境
echo [%date% %time%] 激活 conda 环境: crypto_pred_312
call conda activate crypto_pred_312
if %errorlevel% neq 0 (
echo [错误] 无法激活 conda 环境 crypto_pred_312
echo 请先创建环境: conda create -n crypto_pred_312 python=3.12
pause
exit /b 1
)
REM 启动 API 服务 (后台运行)
echo [%date% %time%] 启动 API 服务...
start "API服务" cmd /c "python -m uvicorn api.main:app --host 0.0.0.0 --port 8000"
REM 等待 API 启动
timeout /t 3 /nobreak >nul
REM 启动自动交易程序 (后台运行)
echo [%date% %time%] 启动自动交易程序...
start "自动交易" cmd /c "python -u auto_trade_polymarket.py"
echo.
echo ============================================
echo 系统启动完成!
echo 前端访问: http://localhost:8000
echo 关闭窗口可停止对应服务
echo ============================================
echo.
pause
+90
View File
@@ -0,0 +1,90 @@
# ============================================
# 系统启动脚本 (Windows PowerShell)
# 启动 API 服务和自动交易程序
# ============================================
$ErrorActionPreference = "Stop"
function Write-Log {
param([string]$Message, [string]$Color = "White")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$timestamp] $Message" -ForegroundColor $Color
}
Write-Log "正在启动系统..." "Yellow"
# 检查 conda
$condaPath = Get-Command conda -ErrorAction SilentlyContinue
if (-not $condaPath) {
Write-Log "错误: 未找到 conda,请先安装 Anaconda 或 Miniconda" "Red"
Read-Host "按回车键退出"
exit 1
}
# 激活 conda 环境
Write-Log "激活 conda 环境: crypto_pred_312" "Yellow"
try {
conda activate crypto_pred_312
} catch {
Write-Log "错误: 无法激活 conda 环境 crypto_pred_312" "Red"
Write-Log "请先创建环境: conda create -n crypto_pred_312 python=3.12" "Yellow"
Read-Host "按回车键退出"
exit 1
}
# 启动 API 服务
Write-Log "启动 API 服务..." "Yellow"
$apiProcess = Start-Process -FilePath "python" `
-ArgumentList "-m uvicorn api.main:app --host 0.0.0.0 --port 8000" `
-PassThru -NoNewWindow
Write-Log "API 服务已启动 (PID: $($apiProcess.Id))" "Green"
# 等待 API 启动
Start-Sleep -Seconds 3
# 启动自动交易程序
Write-Log "启动自动交易程序..." "Yellow"
$tradeProcess = Start-Process -FilePath "python" `
-ArgumentList "-u auto_trade_polymarket.py" `
-PassThru -NoNewWindow
Write-Log "自动交易程序已启动 (PID: $($tradeProcess.Id))" "Green"
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " 系统启动完成!" -ForegroundColor Green
Write-Host " 前端访问: http://localhost:8000" -ForegroundColor Green
Write-Host " 按 Ctrl+C 停止所有服务" -ForegroundColor Yellow
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""
# 等待用户中断
try {
while ($true) {
# 检查进程状态
if ($apiProcess.HasExited) {
Write-Log "API 服务意外退出" "Red"
break
}
if ($tradeProcess.HasExited) {
Write-Log "自动交易程序意外退出" "Red"
break
}
Start-Sleep -Seconds 1
}
} finally {
Write-Log "正在停止服务..." "Yellow"
if (-not $apiProcess.HasExited) {
Stop-Process -Id $apiProcess.Id -Force -ErrorAction SilentlyContinue
Write-Log "API 服务已停止" "Green"
}
if (-not $tradeProcess.HasExited) {
Stop-Process -Id $tradeProcess.Id -Force -ErrorAction SilentlyContinue
Write-Log "自动交易程序已停止" "Green"
}
Write-Log "所有服务已停止" "Green"
}
+60 -10
View File
@@ -4,34 +4,66 @@
同时启动: 同时启动:
1. FastAPI 后端服务 (提供前端看板 API) 1. FastAPI 后端服务 (提供前端看板 API)
2. Polymarket 自动交易程序 2. Polymarket 自动交易程序
支持平台:macOS, Linux, Windows
""" """
import subprocess import subprocess
import sys import sys
import time import time
import signal import signal
import os import os
import platform
from datetime import datetime from datetime import datetime
# 颜色代码 # 检测操作系统
IS_WINDOWS = platform.system() == 'Windows'
# 颜色代码 (Windows CMD 默认不支持 ANSI,但现代 Windows Terminal 支持)
if IS_WINDOWS:
# 尝试启用 Windows ANSI 支持
try:
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
ANSI_SUPPORTED = True
except Exception:
ANSI_SUPPORTED = False
else:
ANSI_SUPPORTED = True
if ANSI_SUPPORTED:
GREEN = '\033[92m' GREEN = '\033[92m'
YELLOW = '\033[93m' YELLOW = '\033[93m'
RED = '\033[91m' RED = '\033[91m'
RESET = '\033[0m' RESET = '\033[0m'
else:
GREEN = YELLOW = RED = RESET = ''
def log(message, color=RESET): def log(message, color=RESET):
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"{color}[{timestamp}] {message}{RESET}") print(f"{color}[{timestamp}] {message}{RESET}")
def start_process(command, name): def start_process(command, name):
"""启动子进程""" """启动子进程(跨平台兼容)"""
log(f"正在启动 {name}...", YELLOW) log(f"正在启动 {name}...", YELLOW)
try: try:
if IS_WINDOWS:
# Windows: 不使用 preexec_fn,使用 CREATE_NEW_PROCESS_GROUP
process = subprocess.Popen( process = subprocess.Popen(
command, command,
shell=True, shell=True,
stdout=sys.stdout, stdout=sys.stdout,
stderr=sys.stderr, stderr=sys.stderr,
preexec_fn=os.setsid # 创建新的进程组,方便统一杀掉 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
)
else:
# macOS / Linux: 使用 setsid 创建新进程组
process = subprocess.Popen(
command,
shell=True,
stdout=sys.stdout,
stderr=sys.stderr,
preexec_fn=os.setsid
) )
log(f"{name} 已启动 (PID: {process.pid})", GREEN) log(f"{name} 已启动 (PID: {process.pid})", GREEN)
return process return process
@@ -39,6 +71,26 @@ def start_process(command, name):
log(f"启动 {name} 失败: {e}", RED) log(f"启动 {name} 失败: {e}", RED)
return None return None
def terminate_process(process, name):
"""终止进程(跨平台兼容)"""
if process is None:
return
try:
if IS_WINDOWS:
# Windows: 使用 terminate() 或发送 CTRL_BREAK_EVENT
process.terminate()
else:
# macOS / Linux: 终止整个进程组
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
log(f"{name} 已停止", GREEN)
except Exception as e:
log(f"停止 {name} 时出错: {e}", RED)
# 强制杀死
try:
process.kill()
except Exception:
pass
def main(): def main():
# 激活 Conda 环境 # 激活 Conda 环境
# 注意:在子进程中直接使用当前已激活的 python 解释器路径,避免 conda activate 问题 # 注意:在子进程中直接使用当前已激活的 python 解释器路径,避免 conda activate 问题
@@ -54,7 +106,7 @@ def main():
# 2. 启动自动交易程序 # 2. 启动自动交易程序
# 使用 -u 参数禁用输出缓冲 # 使用 -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 自动交易程序") trade_process = start_process(trade_cmd, "Polymarket 自动交易程序")
log("系统启动完成!", GREEN) log("系统启动完成!", GREEN)
@@ -64,10 +116,10 @@ def main():
try: try:
# 监控子进程状态 # 监控子进程状态
while True: while True:
if api_process.poll() is not None: if api_process and api_process.poll() is not None:
log("API 服务意外退出", RED) log("API 服务意外退出", RED)
break break
if trade_process.poll() is not None: if trade_process and trade_process.poll() is not None:
log("自动交易程序意外退出", RED) log("自动交易程序意外退出", RED)
break break
time.sleep(1) time.sleep(1)
@@ -76,10 +128,8 @@ def main():
log("\n正在停止服务...", YELLOW) log("\n正在停止服务...", YELLOW)
finally: finally:
# 优雅退出 # 优雅退出
if api_process: terminate_process(api_process, "API 服务")
os.killpg(os.getpgid(api_process.pid), signal.SIGTERM) terminate_process(trade_process, "自动交易程序")
if trade_process:
os.killpg(os.getpgid(trade_process.pid), signal.SIGTERM)
log("所有服务已停止", GREEN) log("所有服务已停止", GREEN)
@@ -1,7 +1,7 @@
""" """
BTC Hourly 自动交易脚本 BTC Hourly 自动交易脚本
基于DLinear模型预测,在Polymarket自动下单BTC小时涨跌 基于DLinear、N-BEATS、TTM-R2多模型集成预测,在Polymarket自动下单BTC小时涨跌
支持: 支持:
- 每小时自动寻找对应市场 - 每小时自动寻找对应市场
- 实时价格刷新 - 实时价格刷新
@@ -18,23 +18,22 @@ import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import schedule import schedule
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Tuple, Dict from typing import Optional, Tuple, Dict, List
import pickle
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import torch import torch
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pandas_ta as ta
from sklearn.preprocessing import StandardScaler
from dotenv import load_dotenv
import httpx 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.fetcher import BinanceDataFetcher
from data.trading_db import TradingDatabase from data.trading_db import TradingDatabase
from api.models import PredictionModel
from models.inference import get_inference_manager
load_dotenv() load_dotenv()
@@ -56,6 +55,7 @@ def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO) -> loggi
# 创建logger # 创建logger
logger = logging.getLogger("polymarket_trader") logger = logging.getLogger("polymarket_trader")
logger.setLevel(log_level) logger.setLevel(log_level)
logger.propagate = False
# 避免重复添加handler # 避免重复添加handler
if logger.handlers: if logger.handlers:
@@ -92,58 +92,8 @@ def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO) -> loggi
logger = setup_logging() 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
class PolymarketBTCTrader: class PolymarketBTCTrader:
"""Polymarket BTC Hourly交易器""" """Polymarket BTC Hourly交易器 (多模型集成版)"""
GAMMA_API = "https://gamma-api.polymarket.com" GAMMA_API = "https://gamma-api.polymarket.com"
CLOB_API = "https://clob.polymarket.com" CLOB_API = "https://clob.polymarket.com"
@@ -175,14 +125,11 @@ class PolymarketBTCTrader:
headers={'Accept-Encoding': 'identity'} headers={'Accept-Encoding': 'identity'}
) )
# 设备 # 初始化推理管理器
self.device = torch.device('mps' if torch.backends.mps.is_available() else logger.info("初始化推理管理器...")
'cuda' if torch.cuda.is_available() else 'cpu') self.inference_manager = get_inference_manager()
self.inference_manager.load_all_models()
# 加载模型 self.device = self.inference_manager.device
self.model = None
self.preprocessor = None
self._load_model()
# 数据获取器 # 数据获取器
self.fetcher = BinanceDataFetcher() self.fetcher = BinanceDataFetcher()
@@ -199,25 +146,6 @@ class PolymarketBTCTrader:
self.current_market = None self.current_market = None
self.last_price_update = 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): def _init_clob_client(self):
"""初始化CLOB交易客户端""" """初始化CLOB交易客户端"""
try: try:
@@ -246,7 +174,7 @@ class PolymarketBTCTrader:
elif result.get('error'): elif result.get('error'):
status = 'failure' status = 'failure'
elif result.get('prediction'): # 有预测但未交易(如市场未找到) elif result.get('prediction'): # 有预测但未交易(如市场未找到)
status = 'in_progress' # 或者 failure? 未找到市场算 failure status = 'in_progress'
if result.get('error'): if result.get('error'):
status = 'failure' status = 'failure'
@@ -254,7 +182,7 @@ class PolymarketBTCTrader:
'timestamp': result.get('timestamp'), 'timestamp': result.get('timestamp'),
'market_title': result.get('market'), 'market_title': result.get('market'),
'market_slug': market.get('slug') if market else result.get('market_slug'), 'market_slug': market.get('slug') if market else result.get('market_slug'),
'model_name': 'DLinear', 'model_name': 'Ensemble', # 记录为集成策略
'prediction': result.get('prediction'), 'prediction': result.get('prediction'),
'probability': result.get('probability'), 'probability': result.get('probability'),
'action': result.get('side'), 'action': result.get('side'),
@@ -276,13 +204,6 @@ class PolymarketBTCTrader:
格式: bitcoin-up-or-down-{month}-{day}-{hour}am/pm-et 格式: bitcoin-up-or-down-{month}-{day}-{hour}am/pm-et
例如: bitcoin-up-or-down-february-3-8am-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') et_tz = ZoneInfo('America/New_York')
@@ -313,15 +234,7 @@ class PolymarketBTCTrader:
return slug return slug
def get_market_by_slug(self, slug: str) -> Optional[Dict]: def get_market_by_slug(self, slug: str) -> Optional[Dict]:
""" """通过slug获取市场信息"""
通过slug获取市场信息
Args:
slug: 市场slug
Returns:
市场信息
"""
try: try:
response = self.http.get( response = self.http.get(
f"{self.GAMMA_API}/events", f"{self.GAMMA_API}/events",
@@ -375,15 +288,7 @@ class PolymarketBTCTrader:
return None return None
def get_current_hourly_market(self) -> Optional[Dict]: def get_current_hourly_market(self) -> Optional[Dict]:
""" """获取BTC市场"""
获取BTC市场
Args:
for_current_hour: 如果True,获取当前整点市场;否则获取下一个整点市场
Returns:
市场信息
"""
slug = self.generate_market_slug() slug = self.generate_market_slug()
logger.info(f"搜索市场: {slug}") logger.info(f"搜索市场: {slug}")
@@ -401,12 +306,7 @@ class PolymarketBTCTrader:
return market return market
def refresh_prices(self) -> Optional[Dict]: def refresh_prices(self) -> Optional[Dict]:
""" """刷新当前市场价格"""
刷新当前市场价格
Returns:
更新后的市场信息
"""
if not self.current_market: if not self.current_market:
self.get_current_hourly_market() self.get_current_hourly_market()
@@ -453,18 +353,7 @@ class PolymarketBTCTrader:
return market return market
def get_realtime_prices(self, up_token_id: str, down_token_id: str) -> Optional[list]: def get_realtime_prices(self, up_token_id: str, down_token_id: str) -> Optional[list]:
""" """从CLOB API获取实时买入价格"""
从CLOB API获取实时买入价格
使用/price?side=BUY端点获取真实买入价格 (与页面显示一致)
Args:
up_token_id: Up代币ID
down_token_id: Down代币ID
Returns:
[up_price, down_price] 或 None
"""
try: try:
# 使用/price端点获取买入价格 # 使用/price端点获取买入价格
up_response = self.http.get( up_response = self.http.get(
@@ -491,16 +380,7 @@ class PolymarketBTCTrader:
return None return None
def execute_trade(self, prediction: int, market: Dict) -> Dict: def execute_trade(self, prediction: int, market: Dict) -> Dict:
""" """执行交易"""
执行交易
Args:
prediction: 预测结果(0=跌, 1=涨)
market: 市场信息
Returns:
交易结果
"""
result = { result = {
'timestamp': datetime.now().isoformat(), 'timestamp': datetime.now().isoformat(),
'market': market['title'], 'market': market['title'],
@@ -573,16 +453,7 @@ class PolymarketBTCTrader:
def get_prediction_with_delay(self, delay_seconds: float = 2.0) -> Tuple[int, float]: def get_prediction_with_delay(self, delay_seconds: float = 2.0) -> Tuple[int, float]:
""" """
延迟后获取K线并预测当前小时的涨跌 延迟后获取K线并预测当前小时的涨跌 (多模型)
注意: 使用上一小时收盘的K线(倒数第二根)来预测当前小时
例如: 22:00整点时,用21:00收盘的K线预测22:00这个小时的涨跌
Args:
delay_seconds: 延迟秒数(等待K线更新)
Returns:
(prediction, probability): 预测结果(0=跌, 1=涨)和概率
""" """
logger.info(f"⏳ 等待{delay_seconds}秒获取最新K线...") logger.info(f"⏳ 等待{delay_seconds}秒获取最新K线...")
time.sleep(delay_seconds) time.sleep(delay_seconds)
@@ -601,7 +472,6 @@ class PolymarketBTCTrader:
) )
# 使用倒数第二根K线(上一小时收盘)进行预测 # 使用倒数第二根K线(上一小时收盘)进行预测
# 例如22:00时,最新K线是14:00 UTC(22:00北京),我们用13:00 UTC(21:00北京)的数据
prev_time = df['timestamp'].iloc[-2] prev_time = df['timestamp'].iloc[-2]
prev_close = df['close'].iloc[-2] prev_close = df['close'].iloc[-2]
current_time = df['timestamp'].iloc[-1] current_time = df['timestamp'].iloc[-1]
@@ -610,33 +480,141 @@ class PolymarketBTCTrader:
logger.info(f" 上一K线时间: {prev_time} (用于预测)") logger.info(f" 上一K线时间: {prev_time} (用于预测)")
logger.info(f" 上一收盘价: ${prev_close:,.2f}") logger.info(f" 上一收盘价: ${prev_close:,.2f}")
logger.info(f" 当前K线时间: {current_time} (预测目标)") logger.info(f" 当前K线时间: {current_time} (预测目标)")
logger.info(f" 当前收盘价: ${current_close:,.2f} (实际结果)") logger.info(f" 当前收盘价: ${current_close:,.2f} (未收盘)")
# 回填上一小时的实际结果 (多模型)
self._update_previous_prediction(df)
# 用除最后一根外的数据进行预测 # 用除最后一根外的数据进行预测
df_for_prediction = df.iloc[:-1] df_for_prediction = df.iloc[:-1]
X, _, _ = self.preprocessor.preprocess(df_for_prediction, fit=False)
X_last = torch.FloatTensor(X[-1:]).to(self.device)
with torch.no_grad(): # 打印模型输入的最后一根K线数据
logits = self.model(X_last).cpu().numpy() last_input_row = df_for_prediction.iloc[-1]
prob = 1 / (1 + np.exp(-logits)) logger.info(f" 📌 模型输入末根K线: {last_input_row['timestamp']} "
prediction = int(prob.item() > 0.5) f"O:{last_input_row['open']:.2f} H:{last_input_row['high']:.2f} "
probability = float(prob.item()) f"L:{last_input_row['low']:.2f} C:{last_input_row['close']:.2f} "
f"V:{last_input_row['volume']:.0f}")
direction = "📈 UP (涨)" if prediction == 1 else "📉 DOWN (跌)" # === 多模型预测 ===
confidence = probability if prediction == 1 else 1 - probability all_preds = self.inference_manager.predict_all(df_for_prediction)
logger.info(f"🤖 DLinear预测: {direction}")
logger.info(f" 置信度: {confidence:.2%}")
# 显示预测是否正确 (用于验证) # 解析预测结果
actual_up = current_close > prev_close final_prediction = 0
if prediction == 1: final_probability = 0.5
correct = actual_up
# 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: else:
correct = not actual_up # 降级到 DLinear
logger.info(f" 实际涨跌: {'📈 涨' if actual_up else '📉 跌'} ({'✅ 正确' if correct else '❌ 错误'})") if 'dlinear' in all_preds:
final_prediction = all_preds['dlinear']['prediction']
final_probability = all_preds['dlinear']['probability']
logger.warning("⚠️ 集成预测不可用,降级为 DLinear")
return prediction, probability # 保存所有预测结果到 hourly_model_predictions 表 (V2)
try:
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)
prediction_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,
"open_price": float(df['open'].iloc[-1]),
"close_price": None,
"actual_result": None,
# DLinear
"dlinear_prediction": all_preds.get('dlinear', {}).get('prediction'),
"dlinear_prob": all_preds.get('dlinear', {}).get('probability'),
"dlinear_correct": None,
# N-BEATS
"nbeats_prediction": all_preds.get('nbeats', {}).get('prediction'),
"nbeats_prob": all_preds.get('nbeats', {}).get('probability'),
"nbeats_correct": None,
# TTM-R2
"ttm_prediction": all_preds.get('ttm', {}).get('prediction'),
"ttm_prob": all_preds.get('ttm', {}).get('probability'),
"ttm_correct": None,
# Ensemble
"ensemble_prediction": all_preds.get('ensemble', {}).get('prediction'),
"ensemble_votes": all_preds.get('ensemble', {}).get('votes'),
"ensemble_correct": None,
}
PredictionModel.save_hourly_prediction(prediction_data)
logger.info(f"💾 所有模型预测记录已保存 (ET {current_time_et.strftime('%Y-%m-%d %H:%M')})")
except Exception as e:
logger.error(f"保存预测记录失败: {e}")
return final_prediction, final_probability
def _update_previous_prediction(self, df: pd.DataFrame):
"""回填上一小时预测的实际结果 (所有模型)"""
if len(df) < 2:
return
try:
et_tz = ZoneInfo('America/New_York')
prev_row = df.iloc[-2]
prev_utc = pd.Timestamp(prev_row['timestamp'])
prev_et = prev_utc.tz_localize('UTC').tz_convert(et_tz)
prev_date = prev_et.strftime('%Y-%m-%d')
prev_hour = prev_et.hour
prev_open = float(prev_row['open'])
prev_close = float(prev_row['close'])
actual_result = 1 if prev_close > prev_open else 0
# 使用 V2 更新方法 (宽表)
updated = PredictionModel.update_actual_result_v2(
date=prev_date,
hour=prev_hour,
actual_result=actual_result,
close_price=prev_close
)
if updated:
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}")
def run_once(self) -> Dict: def run_once(self) -> Dict:
"""运行一次交易流程(不等待整点)""" """运行一次交易流程(不等待整点)"""
@@ -646,21 +624,6 @@ class PolymarketBTCTrader:
def run_at_hour_start(self, delay_seconds: float = 2.0, timeout_minutes: int = 50) -> Dict: 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("=" * 60)
logger.info(f"⏰ 整点交易触发 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") logger.info(f"⏰ 整点交易触发 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
@@ -756,69 +719,25 @@ class PolymarketBTCTrader:
def _show_initial_prediction(self): def _show_initial_prediction(self):
""" """
启动时显示当前预测(不下单) 启动时显示当前预测并尝试执行交易 (不记录Monitor状态)
""" """
logger.info("\n📊 当前预测状态:") logger.info("\n📊 启动时初始预测检查:")
end_date = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d') # 计算剩余分钟数
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
df = self.fetcher.fetch_ohlcv(
symbol='BTC',
timeframe='1h',
start_date=start_date,
end_date=end_date,
use_cache=False
)
# 显示K线信息
prev_time = df['timestamp'].iloc[-2]
prev_close = df['close'].iloc[-2]
current_time = df['timestamp'].iloc[-1]
current_close = df['close'].iloc[-1]
logger.info(f" 上一K线: {prev_time} 收盘价: ${prev_close:,.2f}")
logger.info(f" 当前K线: {current_time} 收盘价: ${current_close:,.2f}")
# 进行预测
df_for_prediction = df.iloc[:-1]
X, _, _ = self.preprocessor.preprocess(df_for_prediction, fit=False)
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())
direction = "📈 UP (涨)" if prediction == 1 else "📉 DOWN (跌)"
confidence = probability if prediction == 1 else 1 - probability
logger.info(f" 🤖 预测: {direction} (置信度: {confidence:.2%})")
# 显示市场价格
if self.current_market:
up_price = self.current_market['outcome_prices'][0]
down_price = self.current_market['outcome_prices'][1]
if prediction == 1:
logger.info(f" 💰 买入Up价格: ${up_price:.3f} {'✅ 可下单' if up_price <= self.max_price else '❌ 超过限价'}")
else:
logger.info(f" 💰 买入Down价格: ${down_price:.3f} {'✅ 可下单' if down_price <= self.max_price else '❌ 超过限价'}")
# 计算距下一整点时间
now = datetime.now() now = datetime.now()
next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
wait_minutes = int((next_hour - now).total_seconds() / 60) remaining_minutes = int((next_hour - now).total_seconds() / 60)
logger.info(f" ⏰ 距离下一整点: {wait_minutes}分钟")
logger.info("") # 超时时间设为剩余分钟数 - 10,最少为 1 分钟
timeout_minutes = max(1, remaining_minutes - 10)
logger.info(f" 距离下一整点还有 {remaining_minutes} 分钟,设置超时为 {timeout_minutes} 分钟")
# 直接调用 run_at_hour_start,延迟为0,立即执行交易逻辑
self.run_at_hour_start(delay_seconds=0, timeout_minutes=timeout_minutes)
def wait_for_next_hour(self) -> datetime: def wait_for_next_hour(self) -> datetime:
""" """等待到下一个整点"""
等待到下一个整点
Returns:
下一个整点时间
"""
now = datetime.now() now = datetime.now()
next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
wait_seconds = (next_hour - now).total_seconds() wait_seconds = (next_hour - now).total_seconds()
@@ -829,19 +748,9 @@ class PolymarketBTCTrader:
return next_hour return next_hour
def run_scheduler(self, interval_minutes: int = 5, delay_seconds: float = 2.0): 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("=" * 60)
logger.info("🤖 BTC Hourly自动交易已启动") logger.info("🤖 BTC Hourly自动交易 (集成版) 已启动")
logger.info("=" * 60) logger.info("=" * 60)
logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}") logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}")
logger.info(f" 下单时机: 每个整点后延迟{delay_seconds}秒") logger.info(f" 下单时机: 每个整点后延迟{delay_seconds}秒")
@@ -876,7 +785,7 @@ class PolymarketBTCTrader:
def main(): 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, parser.add_argument('--dry-run', action='store_true', default=True,
help='模拟模式(不实际下单)') help='模拟模式(不实际下单)')
parser.add_argument('--real', action='store_true', parser.add_argument('--real', action='store_true',
@@ -924,7 +833,10 @@ def main():
logs = [] logs = []
if os.path.exists(log_file): if os.path.exists(log_file):
with open(log_file, 'r') as f: with open(log_file, 'r') as f:
try:
logs = json.load(f) logs = json.load(f)
except:
pass
logs.append(result) logs.append(result)
with open(log_file, 'w') as f: with open(log_file, 'w') as f:
json.dump(logs, f, indent=2, ensure_ascii=False) json.dump(logs, f, indent=2, ensure_ascii=False)
+54
View File
@@ -9,5 +9,59 @@
"side": "DOWN", "side": "DOWN",
"price": 0.505, "price": 0.505,
"probability": 0.5563886165618896 "probability": 0.5563886165618896
},
{
"timestamp": "2026-02-11T22:32:46.922211",
"success": false,
"error": "该市场已存在成功交易记录",
"prediction": "DOWN",
"probability": 0.0,
"market": "Bitcoin Up or Down - February 11, 9AM ET",
"market_slug": "bitcoin-up-or-down-february-11-9am-et"
},
{
"timestamp": "2026-02-11T23:18:40.579373",
"success": false,
"error": "该市场已存在成功交易记录",
"prediction": "UP",
"probability": 0.6666666666666666,
"market": "Bitcoin Up or Down - February 11, 10AM ET",
"market_slug": "bitcoin-up-or-down-february-11-10am-et"
},
{
"timestamp": "2026-02-11T23:18:59.059130",
"success": false,
"error": "该市场已存在成功交易记录",
"prediction": "UP",
"probability": 0.6666666666666666,
"market": "Bitcoin Up or Down - February 11, 10AM ET",
"market_slug": "bitcoin-up-or-down-february-11-10am-et"
},
{
"timestamp": "2026-02-11T23:19:50.797784",
"success": false,
"error": "该市场已存在成功交易记录",
"prediction": "UP",
"probability": 0.6666666666666666,
"market": "Bitcoin Up or Down - February 11, 10AM ET",
"market_slug": "bitcoin-up-or-down-february-11-10am-et"
},
{
"timestamp": "2026-02-11T23:31:03.776709",
"success": false,
"error": "该市场已存在成功交易记录",
"prediction": "UP",
"probability": 0.6666666666666666,
"market": "Bitcoin Up or Down - February 11, 10AM ET",
"market_slug": "bitcoin-up-or-down-february-11-10am-et"
},
{
"timestamp": "2026-02-11T23:35:39.565123",
"success": false,
"error": "该市场已存在成功交易记录",
"prediction": "UP",
"probability": 0.6666666666666666,
"market": "Bitcoin Up or Down - February 11, 10AM ET",
"market_slug": "bitcoin-up-or-down-february-11-10am-et"
} }
] ]
+8 -3
View File
@@ -38,19 +38,24 @@ class TrainingConfig:
Returns: Returns:
Device string ('mps', 'cuda', or 'cpu') Device string ('mps', 'cuda', or 'cpu')
Note:
macOS 使用 MPS,Windows/Linux 使用 CPU
""" """
import torch import torch
import platform
if self.device != 'auto': if self.device != 'auto':
return self.device return self.device
# Auto-detect best available device # 根据操作系统选择设备:macOS 用 MPS,Windows/Linux 用 CPU
if platform.system() == 'Darwin': # macOS
if torch.backends.mps.is_available(): if torch.backends.mps.is_available():
return 'mps' return 'mps'
elif torch.cuda.is_available():
return 'cuda'
else: else:
return 'cpu' return 'cpu'
else: # Windows / Linux
return 'cpu'
if __name__ == "__main__": if __name__ == "__main__":