Files
crypto_predictor 1bd9d25f60 refactor: 清理测试文件和2019相关代码
- 删除测试脚本: test_2026_classification.py, test_lagllama.py, test_timesfm.py, test_ttm.py
- 删除2019相关文件: analyze_2019_*, apply_2019_*, dlinear_2019_*, three_models_2019_*
- 添加集成模型相关代码和分析脚本
- 添加N-BEATS模型和CatBoost分类模型
2026-02-03 20:40:32 +08:00

284 lines
9.1 KiB
Python

"""使用N-BEATS模型训练:预测未来1小时BTC价格"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import numpy as np
import pandas as pd
import pandas_ta as ta
from sklearn.preprocessing import StandardScaler
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from data.fetcher import BinanceDataFetcher
from models.nbeats.model import NBeats, NBeatsConfig
import json
import pickle
class NBeatsPreprocessor:
"""N-BEATS预处理器:预测未来价格"""
def __init__(self, lookback=168, forecast_horizon=1):
self.lookback = lookback
self.forecast_horizon = forecast_horizon
self.price_scaler = StandardScaler()
self.is_fitted = False
def preprocess(self, df, fit=True):
"""预处理数据"""
df = df.copy()
# 创建标签:未来价格
df['future_price'] = df['close'].shift(-self.forecast_horizon)
df = df[:-self.forecast_horizon].reset_index(drop=True)
# 标准化价格
prices = df['close'].values.reshape(-1, 1)
future_prices = df['future_price'].values.reshape(-1, 1)
if fit:
scaled_prices = self.price_scaler.fit_transform(prices)
scaled_future = self.price_scaler.transform(future_prices)
self.is_fitted = True
else:
if not self.is_fitted:
raise ValueError("Must fit before transform")
scaled_prices = self.price_scaler.transform(prices)
scaled_future = self.price_scaler.transform(future_prices)
# 创建序列
X, y = [], []
for i in range(len(scaled_prices) - self.lookback):
X.append(scaled_prices[i:i + self.lookback, 0])
y.append(scaled_future[i + self.lookback, 0])
return np.array(X), np.array(y), df
def inverse_transform_price(self, scaled_price):
"""还原真实价格"""
return self.price_scaler.inverse_transform(scaled_price.reshape(-1, 1)).flatten()
def train_nbeats():
print("="*80)
print("N-BEATS模型训练 - 预测未来1小时BTC价格")
print("="*80)
# ========== 步骤1: 获取数据 ==========
print("\n[1/8] 获取BTC 2020-2025年数据...")
fetcher = BinanceDataFetcher()
df = fetcher.fetch_ohlcv(
symbol='BTC',
timeframe='1h',
start_date='2020-01-01',
end_date='2025-12-31',
use_cache=True
)
print(f"✓ 数据量: {len(df):,} 行")
print(f"✓ 时间: {df['timestamp'].min()} 至 {df['timestamp'].max()}")
print(f"✓ 价格: ${df['close'].iloc[0]:,.2f} → ${df['close'].iloc[-1]:,.2f}")
# ========== 步骤2: 预处理 ==========
print("\n[2/8] 数据预处理...")
preprocessor = NBeatsPreprocessor(lookback=168, forecast_horizon=1)
X, y, processed_df = preprocessor.preprocess(df, fit=True)
print(f"✓ 序列数量: {len(X):,}")
print(f"✓ 输入形状: {X.shape}")
print(f"✓ 输出形状: {y.shape}")
# 还原真实价格看统计
y_real = preprocessor.inverse_transform_price(y)
print(f"\n✓ 目标价格统计:")
print(f" 最小值: ${y_real.min():,.2f}")
print(f" 最大值: ${y_real.max():,.2f}")
print(f" 均值: ${y_real.mean():,.2f}")
# ========== 步骤3: 划分数据集 ==========
print("\n[3/8] 划分数据集(90%训练 + 10%验证)...")
n = len(X)
train_end = int(n * 0.9)
X_train, y_train = X[:train_end], y[:train_end]
X_val, y_val = X[train_end:], y[train_end:]
print(f" 训练集: {len(X_train):,} 样本")
print(f" 验证集: {len(X_val):,} 样本")
# ========== 步骤4: 创建数据加载器 ==========
print("\n[4/8] 创建数据加载器...")
train_dataset = TensorDataset(
torch.FloatTensor(X_train),
torch.FloatTensor(y_train)
)
val_dataset = TensorDataset(
torch.FloatTensor(X_val),
torch.FloatTensor(y_val)
)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=128, shuffle=False)
# ========== 步骤5: 创建N-BEATS模型 ==========
print("\n[5/8] 创建N-BEATS模型...")
config = NBeatsConfig(
input_size=168,
forecast_size=1,
stacks=['trend', 'seasonality', 'generic'],
num_blocks_per_stack=3,
block_layers=4,
layer_size=256,
trend_degree=2,
seasonality_harmonics=4
)
model = NBeats(config)
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
model = model.to(device)
print(f"✓ 模型参数: {model.count_parameters():,}")
print(f"✓ 设备: {device}")
print(f"✓ 模型架构: Trend + Seasonality + Generic")
# ========== 步骤6: 训练 ==========
print("\n[6/8] 开始训练...")
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=5, verbose=True
)
best_val_loss = float('inf')
patience = 15
patience_counter = 0
max_epochs = 50
for epoch in range(max_epochs):
# 训练
model.train()
train_loss = 0
for batch_X, batch_y in train_loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
train_loss += loss.item()
train_loss /= len(train_loader)
# 验证
model.eval()
val_loss = 0
with torch.no_grad():
for batch_X, batch_y in val_loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
outputs = model(batch_X)
loss = criterion(outputs, batch_y)
val_loss += loss.item()
val_loss /= len(val_loader)
# 学习率调度
scheduler.step(val_loss)
print(f"Epoch {epoch+1}/{max_epochs}")
print(f" Train loss: {train_loss:.6f}")
print(f" Val loss: {val_loss:.6f}")
print(f" LR: {optimizer.param_groups[0]['lr']:.6f}")
# 保存最佳模型
if val_loss < best_val_loss:
best_val_loss = val_loss
patience_counter = 0
os.makedirs('./models/checkpoints/nbeats_btc_1h', exist_ok=True)
model.save('./models/checkpoints/nbeats_btc_1h/best_model.pt')
print(f" Saved best model (val_loss: {val_loss:.6f})")
else:
patience_counter += 1
# Early stopping
if patience_counter >= patience:
print(f"\nEarly stopping at epoch {epoch+1}")
break
# ========== 步骤7: 评估 ==========
print("\n[7/8] 评估模型...")
# 重新加载最佳模型
checkpoint = torch.load('./models/checkpoints/nbeats_btc_1h/best_model.pt', map_location=device)
model = NBeats(checkpoint['config'])
model.load_state_dict(checkpoint['model_state_dict'])
model = model.to(device)
model.eval()
val_preds = []
with torch.no_grad():
for batch_X, _ in val_loader:
batch_X = batch_X.to(device)
pred = model(batch_X)
val_preds.extend(pred.cpu().numpy())
val_preds = np.array(val_preds)
# 还原真实价格
pred_prices = preprocessor.inverse_transform_price(val_preds)
true_prices = preprocessor.inverse_transform_price(y_val)
# 当前价格
val_start_idx = train_end + 168
current_prices = processed_df['close'].iloc[val_start_idx:val_start_idx+len(pred_prices)].values
# 方向准确率
pred_direction = (pred_prices > current_prices).astype(int)
true_direction = (true_prices > current_prices).astype(int)
direction_acc = (pred_direction == true_direction).mean()
# 价格预测误差
price_mae = np.abs(pred_prices - true_prices).mean()
price_rmse = np.sqrt(((pred_prices - true_prices) ** 2).mean())
price_mape = np.mean(np.abs((true_prices - pred_prices) / true_prices)) * 100
print("\n" + "="*80)
print("训练完成 - N-BEATS模型性能")
print("="*80)
print(f"\n📊 价格预测误差:")
print(f" MAE: ${price_mae:,.2f}")
print(f" RMSE: ${price_rmse:,.2f}")
print(f" MAPE: {price_mape:.2f}%")
print(f"\n📈 方向预测:")
print(f" 方向准确率: {direction_acc*100:.2f}%")
# ========== 步骤8: 保存 ==========
print("\n[8/8] 保存模型和预处理器...")
with open('./models/checkpoints/nbeats_btc_1h/preprocessor.pkl', 'wb') as f:
pickle.dump(preprocessor, f)
metrics = {
'price_mae': float(price_mae),
'price_rmse': float(price_rmse),
'price_mape': float(price_mape),
'direction_accuracy': float(direction_acc),
'best_val_loss': float(best_val_loss)
}
with open('./models/checkpoints/nbeats_btc_1h/metrics.json', 'w') as f:
json.dump(metrics, f, indent=2)
print(f"\n✓ 模型保存至: ./models/checkpoints/nbeats_btc_1h/")
print("="*80)
if __name__ == "__main__":
train_nbeats()