Update
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user