Initial commit: BTC 1h prediction model (53.10% accuracy)

🎯 项目成果:
- 准确率: 53.10% (2026年测试集)
- 任务类型: 二分类 (预测涨/跌)
- 特征: OHLCV + RSI + MACD (9个特征)
- 训练数据: 2020-2025年BTC小时数据 (52,351序列)
- 测试数据: 2026年1-29日 (484序列)

📊 性能指标:
- 精确率: 51.79%
- 召回率: 49.36%
- F1分数: 0.5054
- 预测分布: 非常平衡 (偏差仅-11个)

🔬 实验历史:
- 尝试了5种方案 (回归/分类, 不同数据划分)
- 分类任务优于回归任务 (53.10% vs 49.5%)
- 全量训练避免了过拟合问题

📦 主要模型文件:
- models/checkpoints/dlinear_classification_2026/
  - best_model.pt (最佳模型)
  - preprocessor.pkl (预处理器)
  - test_2026_results.json (测试结果)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
crypto_predictorandClaude Sonnet 4.5 committed 2026-01-29 21:27:15 +08:00
commit 9bbdc8d42b
82 files changed
+8742

No files matched your search

+1
View File
@@ -0,0 +1 @@
"""Training and evaluation utilities."""
+58
View File
@@ -0,0 +1,58 @@
"""Training configuration."""
from dataclasses import dataclass
from typing import Literal
@dataclass
class TrainingConfig:
"""Configuration for model training."""
# Training parameters
batch_size: int = 64
max_epochs: int = 100
learning_rate: float = 0.001
weight_decay: float = 1e-5
# Early stopping
early_stopping_patience: int = 10
early_stopping_min_delta: float = 1e-4
# Learning rate scheduler
lr_scheduler: Literal['plateau', 'cosine', 'none'] = 'plateau'
lr_patience: int = 5
lr_factor: float = 0.5
# Device
device: str = 'auto' # 'auto', 'mps', 'cuda', or 'cpu'
# Logging
log_interval: int = 10 # Log every N batches
# Checkpointing
save_best: bool = True
checkpoint_dir: str = './models/checkpoints'
def get_device(self) -> str:
"""Get the device to use for training.
Returns:
Device string ('mps', 'cuda', or 'cpu')
"""
import torch
if self.device != 'auto':
return self.device
# Auto-detect best available device
if torch.backends.mps.is_available():
return 'mps'
elif torch.cuda.is_available():
return 'cuda'
else:
return 'cpu'
if __name__ == "__main__":
config = TrainingConfig()
print(f"Using device: {config.get_device()}")
+296
View File
@@ -0,0 +1,296 @@
"""Model evaluation metrics and utilities."""
from typing import Dict, Literal
import numpy as np
import torch
from sklearn.metrics import (
accuracy_score,
precision_score,
recall_score,
f1_score,
confusion_matrix,
mean_squared_error,
mean_absolute_error
)
class ModelEvaluator:
"""Evaluate model performance with task-specific metrics."""
def __init__(self, task: Literal['classification', 'regression']):
"""Initialize evaluator.
Args:
task: Task type ('classification' or 'regression')
"""
self.task = task
def evaluate(
self,
y_true: np.ndarray,
y_pred: np.ndarray,
y_prob: np.ndarray = None
) -> Dict[str, float]:
"""Compute evaluation metrics.
Args:
y_true: True labels
y_pred: Predicted labels/values
y_prob: Prediction probabilities (for classification)
Returns:
Dictionary of metrics
"""
if self.task == 'classification':
return self._evaluate_classification(y_true, y_pred, y_prob)
else:
return self._evaluate_regression(y_true, y_pred)
def _evaluate_classification(
self,
y_true: np.ndarray,
y_pred: np.ndarray,
y_prob: np.ndarray = None
) -> Dict[str, float]:
"""Compute classification metrics.
Args:
y_true: True labels (0 or 1)
y_pred: Predicted labels (0 or 1)
y_prob: Prediction probabilities
Returns:
Dictionary of classification metrics
"""
metrics = {
'accuracy': accuracy_score(y_true, y_pred),
'precision': precision_score(y_true, y_pred, zero_division=0),
'recall': recall_score(y_true, y_pred, zero_division=0),
'f1': f1_score(y_true, y_pred, zero_division=0)
}
# Confusion matrix
cm = confusion_matrix(y_true, y_pred)
if cm.shape == (2, 2):
tn, fp, fn, tp = cm.ravel()
metrics['true_negatives'] = int(tn)
metrics['false_positives'] = int(fp)
metrics['false_negatives'] = int(fn)
metrics['true_positives'] = int(tp)
# Specificity
if (tn + fp) > 0:
metrics['specificity'] = tn / (tn + fp)
# Directional accuracy (if probabilities provided)
if y_prob is not None:
metrics['mean_confidence'] = float(np.mean(y_prob))
return metrics
def _evaluate_regression(
self,
y_true: np.ndarray,
y_pred: np.ndarray
) -> Dict[str, float]:
"""Compute regression metrics.
Args:
y_true: True values
y_pred: Predicted values
Returns:
Dictionary of regression metrics
"""
# Basic regression metrics
mse = mean_squared_error(y_true, y_pred)
mae = mean_absolute_error(y_true, y_pred)
rmse = np.sqrt(mse)
# MAPE (Mean Absolute Percentage Error)
# Avoid division by zero
epsilon = 1e-8
mape = np.mean(np.abs((y_true - y_pred) / (np.abs(y_true) + epsilon))) * 100
# Directional accuracy (sign agreement)
directional_accuracy = np.mean(np.sign(y_true) == np.sign(y_pred))
metrics = {
'mse': float(mse),
'rmse': float(rmse),
'mae': float(mae),
'mape': float(mape),
'directional_accuracy': float(directional_accuracy)
}
return metrics
def backtest_strategy(
self,
y_true: np.ndarray,
y_pred: np.ndarray,
initial_capital: float = 10000.0,
transaction_cost: float = 0.001
) -> Dict[str, float]:
"""Simple backtesting with long/short strategy.
Args:
y_true: True returns
y_pred: Predicted labels (classification) or returns (regression)
initial_capital: Starting capital
transaction_cost: Transaction cost per trade (e.g., 0.001 = 0.1%)
Returns:
Dictionary of backtest metrics
"""
capital = initial_capital
positions = []
returns_list = []
for i in range(len(y_pred)):
if self.task == 'classification':
# Classification: y_pred已经是0/1的标签(不是概率)
# 1 = buy, 0 = sell/short
position = 1 if y_pred[i] == 1 else -1
else:
# Regression: positive prediction = buy, negative = sell
position = 1 if y_pred[i] > 0 else -1
# Calculate return based on position
actual_return = y_true[i]
strategy_return = position * actual_return
# Apply transaction cost if position changes
if i > 0 and positions[-1] != position:
strategy_return -= transaction_cost
# Update capital
capital *= (1 + strategy_return)
positions.append(position)
returns_list.append(strategy_return)
# Calculate metrics
returns_array = np.array(returns_list)
total_return = (capital - initial_capital) / initial_capital
# Sharpe ratio (annualized, assuming hourly data)
# For hourly data: sqrt(24 * 365) ≈ 93
if len(returns_array) > 0 and returns_array.std() > 0:
sharpe_ratio = (returns_array.mean() / returns_array.std()) * np.sqrt(93)
else:
sharpe_ratio = 0.0
# Maximum drawdown
cumulative = np.cumprod(1 + returns_array)
running_max = np.maximum.accumulate(cumulative)
drawdown = (cumulative - running_max) / running_max
max_drawdown = float(drawdown.min())
# Win rate
win_rate = np.mean(returns_array > 0)
return {
'total_return': float(total_return),
'final_capital': float(capital),
'sharpe_ratio': float(sharpe_ratio),
'max_drawdown': float(max_drawdown),
'win_rate': float(win_rate),
'num_trades': int(np.sum(np.diff(positions) != 0))
}
def evaluate_model(
model: torch.nn.Module,
dataloader: torch.utils.data.DataLoader,
task: Literal['classification', 'regression'],
device: str = 'cpu'
) -> Dict[str, float]:
"""Evaluate a model on a dataset.
Args:
model: PyTorch model
dataloader: DataLoader with evaluation data
task: Task type
device: Device to run evaluation on
Returns:
Dictionary of evaluation metrics
"""
model.eval()
model = model.to(device)
all_preds = []
all_probs = []
all_labels = []
with torch.no_grad():
for batch_X, batch_y in dataloader:
batch_X = batch_X.to(device)
batch_y = batch_y.to(device)
# Forward pass
outputs = model(batch_X).squeeze()
if task == 'classification':
# 注意:修复后的模型输出logits,需要应用sigmoid得到概率
probs = torch.sigmoid(outputs).cpu().numpy()
preds = (probs > 0.5).astype(int)
# 确保是1维数组
if probs.ndim == 0:
probs = np.array([probs])
if preds.ndim == 0:
preds = np.array([preds])
all_probs.extend(probs.flatten())
all_preds.extend(preds.flatten())
else:
# Regression predictions
preds = outputs.cpu().numpy()
if preds.ndim == 0:
preds = np.array([preds])
all_preds.extend(preds.flatten())
# Labels
labels = batch_y.cpu().numpy()
if labels.ndim == 0:
labels = np.array([labels])
all_labels.extend(labels.flatten())
# Convert to arrays
y_true = np.array(all_labels)
y_pred = np.array(all_preds)
y_prob = np.array(all_probs) if task == 'classification' else None
# Evaluate
evaluator = ModelEvaluator(task)
metrics = evaluator.evaluate(y_true, y_pred, y_prob)
# Add backtesting metrics
backtest = evaluator.backtest_strategy(y_true, y_pred)
metrics.update({f'backtest_{k}': v for k, v in backtest.items()})
return metrics
if __name__ == "__main__":
# Test classification evaluation
y_true = np.array([0, 1, 1, 0, 1, 0, 1, 1, 0, 0])
y_pred = np.array([0, 1, 1, 0, 0, 1, 1, 1, 0, 1])
y_prob = np.array([0.3, 0.8, 0.9, 0.2, 0.4, 0.6, 0.7, 0.85, 0.1, 0.55])
evaluator = ModelEvaluator('classification')
metrics = evaluator.evaluate(y_true, y_pred, y_prob)
print("Classification metrics:")
for k, v in metrics.items():
print(f" {k}: {v}")
# Test regression evaluation
y_true_reg = np.random.randn(100) * 0.02
y_pred_reg = y_true_reg + np.random.randn(100) * 0.01
evaluator_reg = ModelEvaluator('regression')
metrics_reg = evaluator_reg.evaluate(y_true_reg, y_pred_reg)
print("\nRegression metrics:")
for k, v in metrics_reg.items():
print(f" {k}: {v}")
+361
View File
@@ -0,0 +1,361 @@
"""Training orchestration and utilities."""
import os
from pathlib import Path
from typing import Optional, Dict
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from tqdm import tqdm
import json
from .config import TrainingConfig
from .evaluator import evaluate_model
class EarlyStopping:
"""Early stopping to stop training when validation loss doesn't improve."""
def __init__(self, patience: int = 10, min_delta: float = 1e-4):
"""Initialize early stopping.
Args:
patience: Number of epochs to wait before stopping
min_delta: Minimum change to qualify as improvement
"""
self.patience = patience
self.min_delta = min_delta
self.counter = 0
self.best_loss = None
self.should_stop = False
def __call__(self, val_loss: float) -> bool:
"""Check if training should stop.
Args:
val_loss: Current validation loss
Returns:
True if training should stop
"""
if self.best_loss is None:
self.best_loss = val_loss
elif val_loss > self.best_loss - self.min_delta:
self.counter += 1
if self.counter >= self.patience:
self.should_stop = True
else:
self.best_loss = val_loss
self.counter = 0
return self.should_stop
class Trainer:
"""Model trainer with support for MPS/CUDA/CPU."""
def __init__(
self,
model: nn.Module,
config: TrainingConfig,
task: str = 'classification',
pos_weight: float = None # 添加类别权重参数
):
"""Initialize trainer.
Args:
model: PyTorch model to train
config: Training configuration
task: Task type ('classification' or 'regression')
pos_weight: Positive class weight for imbalanced data
"""
self.model = model
self.config = config
self.task = task
self.pos_weight = pos_weight
# Set device
self.device = config.get_device()
self.model = self.model.to(self.device)
print(f"Training on device: {self.device}")
# Loss function (with class weight if provided)
if hasattr(model, 'get_loss_function'):
self.criterion = model.get_loss_function(pos_weight=pos_weight, device=self.device)
else:
if task == 'classification':
if pos_weight is not None:
# 使用float32以兼容MPS
pos_weight_tensor = torch.tensor([pos_weight], dtype=torch.float32).to(self.device)
self.criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight_tensor)
else:
self.criterion = nn.BCELoss()
else:
self.criterion = nn.MSELoss()
if pos_weight is not None:
print(f"Using weighted loss - positive class weight: {pos_weight:.2f}")
# Optimizer
self.optimizer = torch.optim.AdamW(
model.parameters(),
lr=config.learning_rate,
weight_decay=config.weight_decay
)
# Learning rate scheduler
if config.lr_scheduler == 'plateau':
self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
self.optimizer,
mode='min',
factor=config.lr_factor,
patience=config.lr_patience,
verbose=True
)
elif config.lr_scheduler == 'cosine':
self.scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
self.optimizer,
T_max=config.max_epochs
)
else:
self.scheduler = None
# Early stopping
self.early_stopping = EarlyStopping(
patience=config.early_stopping_patience,
min_delta=config.early_stopping_min_delta
)
# Training history
self.history = {
'train_loss': [],
'val_loss': [],
'learning_rates': []
}
# Best model tracking
self.best_val_loss = float('inf')
self.best_model_path = None
def train_epoch(self, train_loader: DataLoader) -> float:
"""Train for one epoch.
Args:
train_loader: Training data loader
Returns:
Average training loss
"""
self.model.train()
total_loss = 0.0
num_batches = 0
pbar = tqdm(train_loader, desc='Training')
for batch_X, batch_y in pbar:
batch_X = batch_X.to(self.device)
batch_y = batch_y.to(self.device)
# Forward pass
self.optimizer.zero_grad()
outputs = self.model(batch_X).squeeze()
# Handle different output shapes
if self.task == 'classification':
batch_y = batch_y.float()
loss = self.criterion(outputs, batch_y)
# Backward pass
loss.backward()
self.optimizer.step()
# Track loss
total_loss += loss.item()
num_batches += 1
# Update progress bar
pbar.set_postfix({'loss': f'{loss.item():.4f}'})
return total_loss / num_batches
def validate(self, val_loader: DataLoader) -> float:
"""Validate the model.
Args:
val_loader: Validation data loader
Returns:
Average validation loss
"""
self.model.eval()
total_loss = 0.0
num_batches = 0
with torch.no_grad():
for batch_X, batch_y in val_loader:
batch_X = batch_X.to(self.device)
batch_y = batch_y.to(self.device)
outputs = self.model(batch_X).squeeze()
if self.task == 'classification':
batch_y = batch_y.float()
loss = self.criterion(outputs, batch_y)
total_loss += loss.item()
num_batches += 1
return total_loss / num_batches
def fit(
self,
train_loader: DataLoader,
val_loader: DataLoader,
save_dir: Optional[str] = None
) -> Dict:
"""Train the model.
Args:
train_loader: Training data loader
val_loader: Validation data loader
save_dir: Directory to save checkpoints
Returns:
Training history
"""
if save_dir is None:
save_dir = self.config.checkpoint_dir
save_dir = Path(save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
print(f"Starting training for {self.config.max_epochs} epochs...")
print(f"Model parameters: {self.model.count_parameters()}")
for epoch in range(self.config.max_epochs):
# Train
train_loss = self.train_epoch(train_loader)
# Validate
val_loss = self.validate(val_loader)
# Update history
self.history['train_loss'].append(train_loss)
self.history['val_loss'].append(val_loss)
current_lr = self.optimizer.param_groups[0]['lr']
self.history['learning_rates'].append(current_lr)
# Print progress
print(f"Epoch {epoch + 1}/{self.config.max_epochs}")
print(f" Train loss: {train_loss:.4f}")
print(f" Val loss: {val_loss:.4f}")
print(f" LR: {current_lr:.6f}")
# Save best model
if val_loss < self.best_val_loss:
self.best_val_loss = val_loss
if self.config.save_best:
self.best_model_path = save_dir / 'best_model.pt'
self.model.save(str(self.best_model_path))
print(f" Saved best model (val_loss: {val_loss:.4f})")
# Learning rate scheduling
if self.scheduler is not None:
if self.config.lr_scheduler == 'plateau':
self.scheduler.step(val_loss)
else:
self.scheduler.step()
# Early stopping
if self.early_stopping(val_loss):
print(f"Early stopping at epoch {epoch + 1}")
break
# Save final model
final_model_path = save_dir / 'final_model.pt'
self.model.save(str(final_model_path))
# Save training history
history_path = save_dir / 'training_history.json'
with open(history_path, 'w') as f:
json.dump(self.history, f, indent=2)
print(f"\nTraining completed!")
print(f"Best validation loss: {self.best_val_loss:.4f}")
print(f"Best model saved to: {self.best_model_path}")
return self.history
def evaluate(self, test_loader: DataLoader) -> Dict[str, float]:
"""Evaluate the model on test data.
Args:
test_loader: Test data loader
Returns:
Dictionary of evaluation metrics
"""
# Load best model if available
if self.best_model_path is not None and self.best_model_path.exists():
print(f"Loading best model from {self.best_model_path}")
checkpoint = torch.load(self.best_model_path, map_location=self.device)
self.model.load_state_dict(checkpoint['model_state_dict'])
# Evaluate
metrics = evaluate_model(self.model, test_loader, self.task, self.device)
print("\nTest Metrics:")
for key, value in metrics.items():
print(f" {key}: {value:.4f}")
return metrics
if __name__ == "__main__":
# Test the trainer
from ..models.dlinear.model import DLinear, DLinearConfig
from ..data.fetcher import BinanceDataFetcher
from ..data.preprocessor import CryptoPreprocessor, split_data
from ..data.dataset import create_dataloaders
# Fetch data
print("Fetching data...")
fetcher = BinanceDataFetcher()
df = fetcher.fetch_ohlcv('BTC', '1h')
# Preprocess
print("\nPreprocessing...")
preprocessor = CryptoPreprocessor(lookback=168)
X, y, _ = preprocessor.preprocess_pipeline(df, task='classification')
# Split
train_data, val_data, test_data = split_data(X, y)
# Create dataloaders
train_loader, val_loader, test_loader = create_dataloaders(
train_data, val_data, test_data,
batch_size=32
)
# Create model
print("\nCreating model...")
config = DLinearConfig(
input_dim=X.shape[-1],
lookback=168,
task='classification'
)
model = DLinear(config)
# Train
train_config = TrainingConfig(
batch_size=32,
max_epochs=5, # Short test
early_stopping_patience=3
)
trainer = Trainer(model, train_config, task='classification')
history = trainer.fit(train_loader, val_loader, save_dir='/tmp/test_training')
# Evaluate
metrics = trainer.evaluate(test_loader)