Files
crypto_predictorandClaude Sonnet 4.5 9bbdc8d42b 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>
2026-01-29 21:27:15 +08:00

183 lines
5.1 KiB
Python

"""Base model interface for all prediction models."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Literal, Optional
import torch
import torch.nn as nn
@dataclass
class ModelConfig:
"""Base configuration for all models."""
input_dim: int # Number of input features
lookback: int # Sequence length
task: Literal['classification', 'regression'] = 'classification'
hidden_dim: int = 512
dropout: float = 0.1
class BasePredictor(nn.Module, ABC):
"""Abstract base class for all prediction models."""
def __init__(self, config: ModelConfig):
"""Initialize model with configuration.
Args:
config: Model configuration
"""
super().__init__()
self.config = config
@abstractmethod
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Forward pass.
Args:
x: Input tensor of shape (batch_size, lookback, input_dim)
Returns:
Output tensor. Shape depends on task:
- Classification: (batch_size, 1) with sigmoid output
- Regression: (batch_size, 1) with raw value
"""
pass
def predict(self, x: torch.Tensor) -> torch.Tensor:
"""Make predictions (inference mode).
Args:
x: Input tensor
Returns:
Predictions
"""
self.eval()
with torch.no_grad():
return self.forward(x)
def get_loss_function(self, pos_weight=None, device='cpu') -> nn.Module:
"""Get appropriate loss function for the task.
Args:
pos_weight: Weight for positive class (for imbalanced data)
device: Device to place tensors on
Returns:
Loss function
"""
if self.config.task == 'classification':
if pos_weight is not None:
# 使用float32以兼容MPS (Mac M1)
pos_weight_tensor = torch.tensor([pos_weight], dtype=torch.float32).to(device)
return nn.BCEWithLogitsLoss(pos_weight=pos_weight_tensor)
else:
return nn.BCELoss()
else: # regression
return nn.MSELoss()
def save(self, path: str):
"""Save model checkpoint.
Args:
path: Path to save checkpoint
"""
torch.save({
'model_state_dict': self.state_dict(),
'config': self.config
}, path)
@classmethod
def load(cls, path: str, device: Optional[str] = None):
"""Load model from checkpoint.
Args:
path: Path to checkpoint
device: Device to load model to
Returns:
Loaded model
"""
checkpoint = torch.load(path, map_location=device)
model = cls(checkpoint['config'])
model.load_state_dict(checkpoint['model_state_dict'])
return model
def to_onnx(self, path: str, dummy_input: Optional[torch.Tensor] = None):
"""Export model to ONNX format.
Args:
path: Path to save ONNX model
dummy_input: Example input tensor. If None, will create default.
"""
# Move to CPU for ONNX export (critical for M1)
self.cpu()
self.eval()
# Create dummy input if not provided
if dummy_input is None:
dummy_input = torch.randn(
1,
self.config.lookback,
self.config.input_dim
)
# Ensure dummy input is on CPU
dummy_input = dummy_input.cpu()
# Export to ONNX
torch.onnx.export(
self,
dummy_input,
path,
export_params=True,
opset_version=14,
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
print(f"Model exported to ONNX: {path}")
def count_parameters(self) -> int:
"""Count trainable parameters.
Returns:
Number of trainable parameters
"""
return sum(p.numel() for p in self.parameters() if p.requires_grad)
if __name__ == "__main__":
# Test base class (with a dummy implementation)
class DummyModel(BasePredictor):
def __init__(self, config: ModelConfig):
super().__init__(config)
self.fc = nn.Linear(config.input_dim * config.lookback, 1)
if config.task == 'classification':
self.activation = nn.Sigmoid()
else:
self.activation = nn.Identity()
def forward(self, x):
batch_size = x.size(0)
x = x.view(batch_size, -1)
x = self.fc(x)
return self.activation(x)
# Test
config = ModelConfig(input_dim=10, lookback=168, task='classification')
model = DummyModel(config)
print(f"Parameters: {model.count_parameters()}")
# Test forward pass
x = torch.randn(32, 168, 10)
output = model(x)
print(f"Output shape: {output.shape}")