168 lines
4.9 KiB
Python
168 lines
4.9 KiB
Python
"""
|
|
Feature pipeline: base 13 (EA-compatible) + 11 RSI / session features.
|
|
|
|
Same 24 dims as XAUUSD M15 EA (see ../xauusd_m15/FRONTLINE_RSI_INTEGRATION.md).
|
|
RSI uses Wilder smoothing (ewm alpha=1/period) to align with MT5 iRSI.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
NUM_BASE_FEATURES = 13
|
|
NUM_RSI_EXTRA = 11
|
|
NUM_FEATURES = NUM_BASE_FEATURES + NUM_RSI_EXTRA # 24
|
|
|
|
RSI_OVERBOUGHT = 70.0
|
|
RSI_OVERSOLD = 30.0
|
|
|
|
|
|
def wilder_rsi(close: pd.Series, period: int) -> np.ndarray:
|
|
"""Wilder RSI (matches MetaTrader iRSI closely)."""
|
|
delta = close.diff()
|
|
gain = delta.clip(lower=0.0)
|
|
loss = (-delta).clip(lower=0.0)
|
|
avg_g = gain.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
|
|
avg_l = loss.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
|
|
rs = avg_g / avg_l.replace(0, np.nan)
|
|
rsi = 100.0 - (100.0 / (1.0 + rs))
|
|
return rsi.fillna(50.0).to_numpy(dtype=np.float64)
|
|
|
|
|
|
def prepare_features_full(
|
|
df: pd.DataFrame,
|
|
*,
|
|
session_hour_offset: int | None = None,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Build (N, 24) feature table, chronological index matching df.
|
|
Drops first ~50 rows (warmup).
|
|
"""
|
|
if session_hour_offset is None:
|
|
session_hour_offset = int(os.environ.get("SESSION_HOUR_OFFSET", "0"))
|
|
|
|
o = df["open"].to_numpy(dtype=np.float64)
|
|
h = df["high"].to_numpy(dtype=np.float64)
|
|
l = df["low"].to_numpy(dtype=np.float64)
|
|
c = df["close"].astype(float)
|
|
vol = df["tick_volume"].to_numpy(dtype=np.float64)
|
|
n = len(df)
|
|
idx = df.index
|
|
|
|
rsi7 = wilder_rsi(c, 7)
|
|
rsi14 = wilder_rsi(c, 14)
|
|
rsi21 = wilder_rsi(c, 21)
|
|
|
|
ema20 = c.ewm(span=20, adjust=False).mean().to_numpy()
|
|
ema50 = c.ewm(span=50, adjust=False).mean().to_numpy()
|
|
|
|
tr = np.maximum(
|
|
h - l,
|
|
np.maximum(np.abs(h - np.roll(c.to_numpy(), 1)), np.abs(l - np.roll(c.to_numpy(), 1))),
|
|
)
|
|
tr[0] = h[0] - l[0]
|
|
atr = pd.Series(tr).rolling(14).mean().to_numpy()
|
|
|
|
vol_ma = np.zeros(n)
|
|
for j in range(n):
|
|
s = 0.0
|
|
cnt = 0
|
|
for k in range(j, min(j + 20, n)):
|
|
s += vol[k]
|
|
cnt += 1
|
|
vol_ma[j] = s / cnt if cnt else vol[j]
|
|
|
|
pc_ea = np.zeros(n)
|
|
cvals = c.to_numpy()
|
|
for j in range(1, n):
|
|
den = cvals[j - 1]
|
|
pc_ea[j] = (cvals[j] - den) / den if den else 0.0
|
|
|
|
hours = np.zeros(n, dtype=np.int32)
|
|
for j in range(n):
|
|
ts = idx[j]
|
|
try:
|
|
hts = int(ts.hour)
|
|
except Exception:
|
|
hts = 0
|
|
hours[j] = (hts + session_hour_offset) % 24
|
|
|
|
rows = []
|
|
for j in range(n):
|
|
r0 = rsi14[j]
|
|
r1 = rsi14[j - 1] if j > 0 else r0
|
|
r2 = rsi14[j - 2] if j > 1 else r1
|
|
|
|
spread = np.clip((r0 - rsi7[j]) / 50.0, -1.0, 1.0)
|
|
vel = (r0 - r1) / 25.0
|
|
acc = ((r0 - r1) - (r1 - r2)) / 25.0
|
|
dist_mid = abs(r0 - 50.0) / 50.0
|
|
|
|
cross_ob = 1.0 if (r1 < RSI_OVERBOUGHT and r0 >= RSI_OVERBOUGHT) else 0.0
|
|
cross_os = 1.0 if (r1 > RSI_OVERSOLD and r0 <= RSI_OVERSOLD) else 0.0
|
|
cross_50_up = 1.0 if (r1 < 50.0 and r0 >= 50.0) else 0.0
|
|
cross_50_dn = 1.0 if (r1 > 50.0 and r0 <= 50.0) else 0.0
|
|
asian = 1.0 if (0 <= hours[j] < 8) else 0.0
|
|
|
|
rows.append(
|
|
[
|
|
float(o[j]),
|
|
float(h[j]),
|
|
float(l[j]),
|
|
float(cvals[j]),
|
|
float(vol[j] / 1_000_000.0),
|
|
float(rsi14[j] / 100.0),
|
|
float((ema20[j] - cvals[j]) / cvals[j]) if cvals[j] else 0.0,
|
|
float((ema50[j] - cvals[j]) / cvals[j]) if cvals[j] else 0.0,
|
|
float(atr[j] / cvals[j]) if cvals[j] else 0.0,
|
|
float(pc_ea[j]),
|
|
float(h[j] / l[j]) if l[j] else 1.0,
|
|
float(vol_ma[j] / 1_000_000.0),
|
|
float(vol[j] / vol_ma[j]) if vol_ma[j] > 0 else 1.0,
|
|
float(rsi7[j] / 100.0),
|
|
float(rsi21[j] / 100.0),
|
|
float(spread),
|
|
float(vel),
|
|
float(acc),
|
|
float(dist_mid),
|
|
float(cross_ob),
|
|
float(cross_os),
|
|
float(cross_50_up),
|
|
float(cross_50_dn),
|
|
float(asian),
|
|
]
|
|
)
|
|
|
|
cols = [
|
|
"open",
|
|
"high",
|
|
"low",
|
|
"close",
|
|
"tick_volume",
|
|
"rsi",
|
|
"ema20_n",
|
|
"ema50_n",
|
|
"atr_n",
|
|
"price_change",
|
|
"high_low_ratio",
|
|
"volume_ma",
|
|
"volume_ratio",
|
|
"rsi7_n",
|
|
"rsi21_n",
|
|
"rsi_fast_slow_spread",
|
|
"rsi_velocity",
|
|
"rsi_accel",
|
|
"rsi_dist_mid_50",
|
|
"rsi_cross_overbought",
|
|
"rsi_cross_oversold",
|
|
"rsi_cross_50_up",
|
|
"rsi_cross_50_down",
|
|
"session_asian_utc",
|
|
]
|
|
|
|
out = pd.DataFrame(rows, index=idx, columns=cols)
|
|
return out.iloc[50:].copy()
|