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