ohlcv-transformer / model.py
Ultron28's picture
⚡ ULTRON: Sync model.py
64d4dfc verified
Raw History Blame Contribute Delete
11.4 kB
"""
⚡ ULTRON Systems // Advanced Convolutional Transformer Architecture for OHLCV
Implements:
1. Reversible Instance Normalization (RevIN)
2. Multi-Scale Overlapping 1D Convolutional Tokenizer (kernels 3, 7, 15, 31)
3. Rotary Positional Embeddings (RoPE)
4. Deep Conformer Blocks (Self-Attention + Depthwise Separable 1D Conv)
5. Multi-Horizon Dual Forecasting Head (Return Magnitude + Directional Probability)
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import List, Tuple
from config import ModelConfig
class RevIN(nn.Module):
"""
Reversible Instance Normalization to eliminate non-stationary distribution drift.
Normalizes input sequences per-instance and per-feature.
"""
def __init__(self, num_features: int, eps: float = 1e-5, affine: bool = True):
super().__init__()
self.num_features = num_features
self.eps = eps
self.affine = affine
if self.affine:
self.affine_weight = nn.Parameter(torch.ones(num_features))
self.affine_bias = nn.Parameter(torch.zeros(num_features))
def forward(self, x: torch.Tensor, mode: str = "norm") -> torch.Tensor:
# x: [Batch, SeqLen, Features]
if mode == "norm":
self.mean = torch.mean(x, dim=1, keepdim=True).detach()
self.stdev = torch.sqrt(torch.var(x, dim=1, keepdim=True, unbiased=False) + self.eps).detach()
x_norm = (x - self.mean) / self.stdev
if self.affine:
x_norm = x_norm * self.affine_weight + self.affine_bias
return x_norm
elif mode == "denorm":
x_denorm = x
if self.affine:
x_denorm = (x_denorm - self.affine_bias) / (self.affine_weight + self.eps)
return x_denorm * self.stdev + self.mean
else:
raise ValueError(f"Unknown RevIN mode: {mode}")
class MultiScaleOverlappingConvTokenizer(nn.Module):
"""
Extracts multi-frequency candlestick representations via overlapping 1D convolutions.
Operates over overlapping temporal windows with kernel sizes [3, 7, 15, 31].
Stride S < K ensures heavy overlap between adjacent token receptive fields.
"""
def __init__(self, in_features: int, d_model: int, kernels: List[int] = [3, 7, 15, 31], stride: int = 4):
super().__init__()
self.stride = stride
branch_dim = d_model // len(kernels)
self.branches = nn.ModuleList()
for k in kernels:
# Symmetric causal/same padding
pad = k // 2
self.branches.append(
nn.Sequential(
nn.Conv1d(in_features, branch_dim, kernel_size=k, stride=stride, padding=pad),
nn.GELU()
)
)
# Projection layer to fuse multi-scale features into exact d_model
concat_dim = branch_dim * len(kernels)
self.proj = nn.Linear(concat_dim, d_model)
self.norm = nn.LayerNorm(d_model)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: [Batch, SeqLen, InFeatures] -> [Batch, InFeatures, SeqLen]
x_trans = x.transpose(1, 2)
branch_outs = []
min_len = None
for branch in self.branches:
out = branch(x_trans)
if min_len is None or out.shape[2] < min_len:
min_len = out.shape[2]
branch_outs.append(out)
# Align temporal length across all branches
aligned = [b[:, :, :min_len] for b in branch_outs]
fused = torch.cat(aligned, dim=1) # [Batch, concat_dim, Patches]
# Transpose back: [Batch, Patches, concat_dim]
fused = fused.transpose(1, 2)
return self.norm(self.proj(fused))
class RotaryEmbedding(nn.Module):
"""Rotary Position Embeddings (RoPE) for sequence tokens."""
def __init__(self, dim: int, max_seq_len: int = 2048):
super().__init__()
inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
self.register_buffer("inv_freq", inv_freq)
t = torch.arange(max_seq_len, dtype=torch.float32)
freqs = torch.einsum("i,j->ij", t, self.inv_freq)
emb = torch.cat((freqs, freqs), dim=-1)
self.register_buffer("cos_cached", emb.cos(), persistent=False)
self.register_buffer("sin_cached", emb.sin(), persistent=False)
def forward(self, q: torch.Tensor, k: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
seq_len = q.shape[2]
cos = self.cos_cached[:seq_len, :].unsqueeze(0).unsqueeze(0) # [1, 1, seq_len, dim]
sin = self.sin_cached[:seq_len, :].unsqueeze(0).unsqueeze(0)
def rotate_half(x):
x1 = x[..., :x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2:]
return torch.cat((-x2, x1), dim=-1)
q_rot = (q * cos) + (rotate_half(q) * sin)
k_rot = (k * cos) + (rotate_half(k) * sin)
return q_rot, k_rot
class ConformerConvModule(nn.Module):
"""
Depthwise Separable Convolution block to model fine-grained micro-structure
and local volatility gradients.
"""
def __init__(self, d_model: int, kernel_size: int = 7, dropout: float = 0.1):
super().__init__()
self.norm = nn.LayerNorm(d_model)
# Pointwise expansion
self.pointwise1 = nn.Conv1d(d_model, 2 * d_model, kernel_size=1)
# Depthwise conv
self.depthwise = nn.Conv1d(
d_model, d_model,
kernel_size=kernel_size,
stride=1,
padding=kernel_size // 2,
groups=d_model
)
self.bn = nn.BatchNorm1d(d_model)
self.act = nn.SiLU()
# Pointwise projection
self.pointwise2 = nn.Conv1d(d_model, d_model, kernel_size=1)
self.dropout = nn.Dropout(dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: [Batch, Patches, d_model]
res = x
x = self.norm(x).transpose(1, 2)
# Pointwise expansion with Gated Linear Unit
x = self.pointwise1(x)
x = F.glu(x, dim=1) # [Batch, d_model, Patches]
# Depthwise
x = self.depthwise(x)
x = self.bn(x)
x = self.act(x)
x = self.pointwise2(x)
x = self.dropout(x)
return res + x.transpose(1, 2)
class ConformerBlock(nn.Module):
"""
Hybrid Convolution + FlashAttention Conformer Block.
"""
def __init__(self, d_model: int, n_heads: int = 4, d_ff: int = 256, conv_kernel: int = 7, dropout: float = 0.1):
super().__init__()
self.d_model = d_model
self.n_heads = n_heads
self.head_dim = d_model // n_heads
# 1. Multi-Head Self-Attention with RoPE
self.ln_attn = nn.LayerNorm(d_model)
self.q_proj = nn.Linear(d_model, d_model, bias=False)
self.k_proj = nn.Linear(d_model, d_model, bias=False)
self.v_proj = nn.Linear(d_model, d_model, bias=False)
self.out_proj = nn.Linear(d_model, d_model)
self.rope = RotaryEmbedding(self.head_dim)
self.attn_dropout = dropout
# 2. Conformer Convolution Module
self.conv_module = ConformerConvModule(d_model, kernel_size=conv_kernel, dropout=dropout)
# 3. Feed-Forward SwiGLU Network
self.ln_ffn = nn.LayerNorm(d_model)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Step 1: Pre-LN Self Attention with FlashAttention-2 / scaled_dot_product_attention
res = x
x_norm = self.ln_attn(x)
B, T, C = x_norm.shape
q = self.q_proj(x_norm).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
k = self.k_proj(x_norm).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
v = self.v_proj(x_norm).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
# Apply RoPE
q, k = self.rope(q, k)
# Native PyTorch FlashAttention / SDPA
attn_out = F.scaled_dot_product_attention(
q, k, v,
dropout_p=self.attn_dropout if self.training else 0.0,
is_causal=True # Causal temporal mask ensures no future leakage
)
attn_out = attn_out.transpose(1, 2).contiguous().view(B, T, C)
x = res + self.out_proj(attn_out)
# Step 2: Conformer Depthwise Convolution
x = self.conv_module(x)
# Step 3: Feed-Forward
x = x + self.ffn(self.ln_ffn(x))
return x
class OHLCVTransformer(nn.Module):
"""
End-to-End Advanced Convolutional Transformer for Multi-Horizon OHLCV Forecasting.
"""
def __init__(self, cfg: ModelConfig):
super().__init__()
self.cfg = cfg
self.revin = RevIN(num_features=cfg.in_features) if cfg.use_revin else None
# Multi-scale overlapping tokenizer
self.tokenizer = MultiScaleOverlappingConvTokenizer(
in_features=cfg.in_features,
d_model=cfg.d_model,
kernels=cfg.conv_kernels,
stride=cfg.patch_stride
)
# Conformer Layers
self.blocks = nn.ModuleList([
ConformerBlock(
d_model=cfg.d_model,
n_heads=cfg.n_heads,
d_ff=cfg.d_ff,
conv_kernel=cfg.conv_depthwise_kernel,
dropout=cfg.dropout
) for _ in range(cfg.n_layers)
])
self.final_norm = nn.LayerNorm(cfg.d_model)
# Multi-Horizon Heads:
# 1. Return Regression Head: predicts expected log return for [1m, 3m, 5m, 10m, 15m]
self.reg_head = nn.Sequential(
nn.Linear(cfg.d_model, cfg.d_ff),
nn.GELU(),
nn.Dropout(cfg.dropout),
nn.Linear(cfg.d_ff, cfg.num_horizons)
)
# 2. Directional Classification Head: predicts binary direction probability
self.dir_head = nn.Sequential(
nn.Linear(cfg.d_model, cfg.d_ff),
nn.GELU(),
nn.Dropout(cfg.dropout),
nn.Linear(cfg.d_ff, cfg.num_horizons)
)
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
x: [Batch, SeqLen, InFeatures]
Returns:
pred_returns: [Batch, NumHorizons] (expected future log returns)
pred_logits: [Batch, NumHorizons] (logits for P(return > 0))
"""
# RevIN normalisation
if self.revin is not None:
x = self.revin(x, mode="norm")
# Overlapping 1D conv tokenization
tokens = self.tokenizer(x) # [Batch, Patches, d_model]
# Transformer processing
for block in self.blocks:
tokens = block(tokens)
tokens = self.final_norm(tokens)
# Global aggregation: use final token (most recent causal context)
context = tokens[:, -1, :] # [Batch, d_model]
pred_returns = self.reg_head(context)
pred_logits = self.dir_head(context)
return pred_returns, pred_logits