ohlcv-transformer / features.py
Ultron28's picture
⚡ ULTRON: Sync features.py
9897e89 verified
Raw History Blame Contribute Delete
4.49 kB
"""
⚡ ULTRON Systems // Stationary Feature Engineering Engine for OHLCV Bars
Transforms non-stationary price levels into normalized, stationary statistical distributions:
- Multi-scale log returns
- Intra-bar candle morphology (body, wicks, range)
- Parkinson & Garman-Klass micro-volatility
- Normalized volume dynamics
- Multi-horizon forward return & directional targets
"""
import numpy as np
import pandas as pd
from typing import Tuple, List
def compute_stationary_features(df: pd.DataFrame, pred_horizons: List[int] = [1, 3, 5, 10, 15]) -> Tuple[np.ndarray, np.ndarray]:
"""
Computes 8 stationary features and multi-horizon target returns from raw OHLCV.
Input df columns: ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'ticker']
Returns:
features: np.ndarray [N_bars, 8], float32
targets: np.ndarray [N_bars, len(pred_horizons)], float32 (forward log returns)
"""
# Sort strictly by timestamp
df = df.sort_values("timestamp").reset_index(drop=True)
close = df["close"].values.astype(np.float64)
open_p = df["open"].values.astype(np.float64)
high = df["high"].values.astype(np.float64)
low = df["low"].values.astype(np.float64)
volume = df["volume"].values.astype(np.float64)
eps = 1e-7
n_bars = len(close)
if n_bars < 60:
return np.empty((0, 8), dtype=np.float32), np.empty((0, len(pred_horizons)), dtype=np.float32)
# 1. Log Returns: ln(C_t / C_{t-1})
close_prev = np.roll(close, 1)
close_prev[0] = close[0]
log_ret = np.log(np.maximum(close, eps) / np.maximum(close_prev, eps))
log_ret[0] = 0.0
# 2. Inter-bar Gap: (O_t - C_{t-1}) / C_{t-1}
co_gap = (open_p - close_prev) / np.maximum(close_prev, eps)
co_gap[0] = 0.0
# 3. High-Low Spread / Range: (H_t - L_t) / C_t
hl_range = high - low
hl_spread = hl_range / np.maximum(close, eps)
# 4. Normalized Candle Body: (C_t - O_t) / (H_t - L_t + eps) -> range in [-1, 1]
candle_body = (close - open_p) / (hl_range + eps)
# 5. Upper Wick: (H_t - max(O_t, C_t)) / (H_t - L_t + eps) -> range in [0, 1]
max_oc = np.maximum(open_p, close)
upper_wick = (high - max_oc) / (hl_range + eps)
# 6. Lower Wick: (min(O_t, C_t) - L_t) / (H_t - L_t + eps) -> range in [0, 1]
min_oc = np.minimum(open_p, close)
lower_wick = (min_oc - low) / (hl_range + eps)
# 7. Parkinson Micro-Volatility proxy: sqrt((ln(H/L))^2 / (4 * ln(2)))
hl_ratio = np.maximum(high, eps) / np.maximum(low, eps)
parkinson_vol = np.sqrt(np.maximum(0.0, np.log(hl_ratio)**2 / (4.0 * np.log(2.0))))
# 8. Normalized Volume Z-Score relative to rolling window
log_vol = np.log1p(np.maximum(volume, 0.0))
vol_series = pd.Series(log_vol)
vol_mean = vol_series.rolling(window=30, min_periods=1).mean().values
vol_std = vol_series.rolling(window=30, min_periods=1).std().fillna(1.0).values
vol_zscore = (log_vol - vol_mean) / np.maximum(vol_std, 1e-4)
# Stack features: shape [N, 8]
feats = np.column_stack([
log_ret, # Feature 0: 1-bar log return
co_gap, # Feature 1: Overnight / inter-bar gap
hl_spread, # Feature 2: Relative bar range
candle_body, # Feature 3: Directional body strength [-1, 1]
upper_wick, # Feature 4: Overhead rejection wick [0, 1]
lower_wick, # Feature 5: Underlying absorption wick [0, 1]
parkinson_vol, # Feature 6: Intraday continuous volatility
vol_zscore # Feature 7: Relative volume burst
])
# Clip extreme values for numerical stability (prevent gradient explosions)
feats = np.clip(feats, -10.0, 10.0).astype(np.float32)
# Compute multi-horizon forward returns: ln(C_{t + h} / C_t)
targets_list = []
max_horizon = max(pred_horizons)
for h in pred_horizons:
fwd_close = np.roll(close, -h)
fwd_ret = np.log(np.maximum(fwd_close, eps) / np.maximum(close, eps))
# Mask out boundary wrap-around
fwd_ret[-h:] = np.nan
targets_list.append(fwd_ret)
targets = np.column_stack(targets_list).astype(np.float32)
# Remove final `max_horizon` rows where future is unknown
valid_len = n_bars - max_horizon
if valid_len <= 0:
return np.empty((0, 8), dtype=np.float32), np.empty((0, len(pred_horizons)), dtype=np.float32)
return feats[:valid_len], targets[:valid_len]