Browse AI-generated trading strategies shared by the community. Fork, learn, and build on each other's work.
| Score▼ | Strategy | Author | Win Rate▼ | Return▼ | PF▼ | MDD▼ | Trades▼ | Actions | ||
|---|---|---|---|---|---|---|---|---|---|---|
|
🥇
|
GBP/USD SMA Trend + Multi-Indicator XGBoost Classifier
Maximize risk-adjusted return on GBP/USD 15-min bars. Strategy combines required SMA (20/50/200) distance and cross features with ADX trend …
|
E
@elastic-moose-350
|
GBPUSD | 15min | 43.4%58.3% | +7.34%+41.51% | 1.737.25 | 2.20%2.20% | 7612 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:27:43
# Model : XGBoost
# Feature Eng. : SMA (20,50,200) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/GBPUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Required SMAs and distance metrics ──────────────────────────────────
for p in [20, 50, 200]:
sma = close.rolling(p).mean()
df[f"sma_{p}"] = sma
df[f"dm_sma_{p}"] = (close - sma) / sma
# ── SMA slope (momentum of the moving average itself) ───────────────────
for p in [20, 50]:
sma = close.rolling(p).mean()
df[f"sma_{p}_slope"] = sma.diff(5) / sma.shift(5)
# ── SMA cross signals ────────────────────────────────────────────────────
sma20 = close.rolling(20).mean()
sma50 = close.rolling(50).mean()
sma200 = close.rolling(200).mean()
df["sma20_50_cross"] = (sma20 - sma50) / sma50
df["sma50_200_cross"] = (sma50 - sma200) / sma200
df["sma20_200_cross"] = (sma20 - sma200) / sma200
# ── Price momentum over multiple horizons ────────────────────────────────
for lag in [1, 3, 6, 12, 24, 48]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Volatility: rolling standard deviation of returns ───────────────────
ret1 = close.pct_change(1)
for w in [10, 20, 40]:
df[f"vol_{w}"] = ret1.rolling(w).std()
# ── ATR (Average True Range, normalised) ─────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
for w in [14, 28]:
atr = tr.ewm(span=w, adjust=False).mean()
df[f"natr_{w}"] = atr / close
# ── Bollinger Bands (20-period, 2σ) ──────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_up = bb_mid + 2 * bb_std
bb_lo = bb_mid - 2 * bb_std
bb_width = (bb_up - bb_lo) / bb_mid
df["bb_pct_b"] = (close - bb_lo) / (bb_up - bb_lo + 1e-12)
df["bb_width"] = bb_width
df["bb_squeeze"]= np.where(bb_width < bb_width.rolling(50).mean(), 1.0, 0.0)
# ── Keltner Channel (for squeeze confirmation) ───────────────────────────
kc_mid = close.ewm(span=20, adjust=False).mean()
kc_atr = tr.ewm(span=20, adjust=False).mean()
kc_up = kc_mid + 1.5 * kc_atr
kc_lo = kc_mid - 1.5 * kc_atr
df["kc_pct"] = (close - kc_lo) / (kc_up - kc_lo + 1e-12)
# ── RSI (Wilder) ─────────────────────────────────────────────────────────
def wilder_rsi(src, period):
delta = src.diff(1)
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_g = gain.ewm(alpha=1/period, adjust=False).mean()
avg_l = loss.ewm(alpha=1/period, adjust=False).mean()
rs = avg_g / (avg_l + 1e-12)
return 100 - 100 / (1 + rs)
rsi14 = wilder_rsi(close, 14)
rsi6 = wilder_rsi(close, 6)
rsi28 = wilder_rsi(close, 28)
df["rsi14"] = rsi14 / 100.0
df["rsi6"] = rsi6 / 100.0
df["rsi28"] = rsi28 / 100.0
df["rsi14_slope"] = rsi14.diff(3) / 100.0
# RSI divergence proxy: price new high/low but RSI doesn't confirm
price_high_12 = close.rolling(12).max()
price_low_12 = close.rolling(12).min()
rsi_high_12 = rsi14.rolling(12).max()
rsi_low_12 = rsi14.rolling(12).min()
df["rsi_bear_div"] = np.where(
(close >= price_high_12 * 0.999) & (rsi14 < rsi_high_12 * 0.97), 1.0, 0.0)
df["rsi_bull_div"] = np.where(
(close <= price_low_12 * 1.001) & (rsi14 > rsi_low_12 * 1.03), 1.0, 0.0)
# ── MACD ─────────────────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd = ema12 - ema26
signal = macd.ewm(span=9, adjust=False).mean()
hist = macd - signal
df["macd_norm"] = macd / close
df["macd_sig_norm"]= signal / close
df["macd_hist_norm"]= hist / close
df["macd_hist_slope"] = hist.diff(2) / close
# ── Stochastic Oscillator ─────────────────────────────────────────────────
for k_period in [14, 5]:
lo_k = low.rolling(k_period).min()
hi_k = high.rolling(k_period).max()
stoch_k = (close - lo_k) / (hi_k - lo_k + 1e-12) * 100
stoch_d = stoch_k.rolling(3).mean()
df[f"stoch_k_{k_period}"] = stoch_k / 100.0
df[f"stoch_d_{k_period}"] = stoch_d / 100.0
df[f"stoch_kd_{k_period}"] = (stoch_k - stoch_d) / 100.0
# ── Williams %R ───────────────────────────────────────────────────────────
hi14 = high.rolling(14).max()
lo14 = low.rolling(14).min()
df["williams_r"] = (hi14 - close) / (hi14 - lo14 + 1e-12)
# ── CCI (Commodity Channel Index) ────────────────────────────────────────
tp = (high + low + close) / 3.0
tp_sma = tp.rolling(20).mean()
tp_mad = tp.rolling(20).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
df["cci"] = (tp - tp_sma) / (0.015 * tp_mad + 1e-12) / 100.0
# ── Volume-like proxy: candle body and wick ratios ────────────────────────
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = (close - open_).abs() / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["bull_candle"] = np.where(close > open_, 1.0, 0.0)
# ── Mean reversion signal: z-score of close vs SMA20 ────────────────────
df["zscore_20"] = (close - sma20) / (close.rolling(20).std() + 1e-12)
df["zscore_50"] = (close - sma50) / (close.rolling(50).std() + 1e-12)
# ── Trend strength: ADX proxy ─────────────────────────────────────────────
plus_dm = (high.diff(1)).clip(lower=0)
minus_dm = (-low.diff(1)).clip(lower=0)
overlap = pd.concat([plus_dm, minus_dm], axis=1).min(axis=1)
plus_dm = plus_dm - overlap
minus_dm = minus_dm - overlap
atr14 = tr.ewm(span=14, adjust=False).mean()
plus_di = 100 * plus_dm.ewm(span=14, adjust=False).mean() / (atr14 + 1e-12)
minus_di = 100 * minus_dm.ewm(span=14, adjust=False).mean() / (atr14 + 1e-12)
dx = (plus_di - minus_di).abs() / (plus_di + minus_di + 1e-12) * 100
adx = dx.ewm(span=14, adjust=False).mean()
df["adx"] = adx / 100.0
df["plus_di"] = plus_di / 100.0
df["minus_di"] = minus_di / 100.0
df["di_diff"] = (plus_di - minus_di) / 100.0
# ── Regime detection: above/below long-term SMA ──────────────────────────
df["bull_regime"] = np.where(close > sma200, 1.0, 0.0)
df["mid_regime"] = np.where(close > sma50, 1.0, 0.0)
# ── Lag features (auto-regressive) ───────────────────────────────────────
for col, lags in [("rsi14", [1, 2, 4]), ("macd_hist_norm", [1, 2]), ("bb_pct_b", [1, 2])]:
for lag in lags:
df[f"{col}_lag{lag}"] = df[col].shift(lag)
# ── Time-of-day features ─────────────────────────────────────────────────
if hasattr(df.index, "hour"):
df["hour_sin"] = np.sin(2 * np.pi * df.index.hour / 24.0)
df["hour_cos"] = np.cos(2 * np.pi * df.index.hour / 24.0)
df["dow_sin"] = np.sin(2 * np.pi * df.index.dayofweek / 5.0)
df["dow_cos"] = np.cos(2 * np.pi * df.index.dayofweek / 5.0)
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD SMA Trend + Multi-Indicator XGBoost Classifier",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 600,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.10,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 18],
"min_atr": 0.0003,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return on GBP/USD 15-min bars. "
"Strategy combines required SMA (20/50/200) distance and cross features "
"with ADX trend strength, RSI divergence, Bollinger squeeze, Keltner, "
"MACD histogram slope, Stochastic, CCI, Williams %R, and candle-structure "
"ratios. XGBoost with strong regularisation and subsampling prevents "
"overfitting on the relatively short 1-year window. "
"Session filter 06-18 UTC keeps execution in liquid London/NY hours; "
"0.5% SL and 1.0% TP yield 1:2 R:R; sma_50 trend filter aligns trades "
"with intermediate momentum to improve win rate and Sharpe."
),
"notes": (
"Differs from prior RSI/MACD/BB/Stoch attempts by: (1) foregrounding "
"SMA cross and distance features as primary trend signals; (2) adding "
"ADX-based regime and DI differential; (3) including RSI divergence "
"proxy flags; (4) z-score mean-reversion features; (5) candle body/wick "
"structure ratios as micro-structure proxies; (6) time-of-day cyclical "
"encoding; (7) heavier regularisation (gamma, alpha, lambda) and higher "
"min_child_weight to reduce variance on the thin dataset."
),
}
|
||||||||||
|
🥈
|
AUD/USD Bollinger + ATR Mean-Rev (XGBoost)
Maximize risk-adjusted return (Sharpe). XGBoost with moderate depth and heavy regularisation (gamma, alpha, lambda) prevents overfit on AUD/…
|
E
@elastic-moose-350
|
AUDUSD | 15min | 63.9%66.7% | +6.79%+30.08% | 1.122.51 | 3.10%3.10% | 65684 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:44:26
# Model : XGBoost
# Feature Eng. : BB (20,2.0), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_sigma = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_sigma
bb_lower = bb_mid - bb_std * bb_sigma
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
# guard against zero range
bb_range = bb_upper - bb_lower
df["bb_pct"] = np.where(bb_range != 0, (close - bb_lower) / bb_range, 0.5)
# ── ATR (14) & Normalised ATR ────────────────────────────────────────────
atr_period = 14
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(span=atr_period, min_periods=atr_period, adjust=False).mean()
df["atr"] = atr
df["natr"] = np.where(close != 0, atr / close, 0.0)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(span=rsi_period, min_periods=rsi_period, adjust=False).mean()
avg_loss = loss.ewm(span=rsi_period, min_periods=rsi_period, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi
# ── MACD (12, 26, 9) ─────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
# ── EMA trend features ───────────────────────────────────────────────────
ema_20 = close.ewm(span=20, adjust=False).mean()
ema_50 = close.ewm(span=50, adjust=False).mean()
ema_200 = close.ewm(span=200, adjust=False).mean()
df["ema_20"] = ema_20
df["ema_50"] = ema_50
df["ema_200"] = ema_200
df["close_vs_ema20"] = (close - ema_20) / ema_20
df["close_vs_ema50"] = (close - ema_50) / ema_50
df["ema20_vs_ema50"] = (ema_20 - ema_50) / ema_50
df["ema50_vs_ema200"] = (ema_50 - ema_200) / ema_200
# ── Price momentum / rate-of-change ──────────────────────────────────────
for n in [1, 4, 8, 16]:
df[f"roc_{n}"] = close.pct_change(n)
# ── Rolling volatility ────────────────────────────────────────────────────
df["vol_10"] = close.pct_change().rolling(10).std()
df["vol_20"] = close.pct_change().rolling(20).std()
df["vol_ratio"] = np.where(df["vol_20"] != 0,
df["vol_10"] / df["vol_20"], 1.0)
# ── Stochastic %K / %D (14, 3) ───────────────────────────────────────────
low_14 = low.rolling(14).min()
high_14 = high.rolling(14).max()
stoch_range = high_14 - low_14
stoch_k = np.where(stoch_range != 0,
100 * (close - low_14) / stoch_range, 50.0)
df["stoch_k"] = stoch_k
df["stoch_d"] = pd.Series(stoch_k, index=close.index).rolling(3).mean()
# ── Candle body / wick features ──────────────────────────────────────────
body = (close - open_).abs()
total_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / total_rng
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / total_rng
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / total_rng
df["candle_dir"] = np.sign(close - open_)
# ── BB interaction features ───────────────────────────────────────────────
df["price_above_bb_upper"] = np.where(close > bb_upper, 1, 0)
df["price_below_bb_lower"] = np.where(close < bb_lower, 1, 0)
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).quantile(0.20), 1, 0)
# ── RSI regime bins (replacing pd.cut) ───────────────────────────────────
df["rsi_oversold"] = np.where(rsi < 30, 1, 0)
df["rsi_overbought"] = np.where(rsi > 70, 1, 0)
df["rsi_neutral"] = np.where((rsi >= 30) & (rsi <= 70), 1, 0)
# ── Volume proxy (if volume column exists) ───────────────────────────────
if "volume" in df.columns:
vol_ma = df["volume"].rolling(20).mean()
df["volume_ratio"] = np.where(vol_ma != 0,
df["volume"] / vol_ma, 1.0)
# ── Lagged features (1-bar lag to avoid lookahead) ───────────────────────
for feat in ["bb_pct", "rsi_14", "macd_hist", "natr", "stoch_k"]:
df[f"{feat}_lag1"] = df[feat].shift(1)
df[f"{feat}_lag2"] = df[feat].shift(2)
# ── Fill NaNs from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD Bollinger + ATR Mean-Rev (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 3,
"gamma": 0.10,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe). "
"XGBoost with moderate depth and heavy regularisation "
"(gamma, alpha, lambda) prevents overfit on AUD/USD 15-min data. "
"Bollinger Bands capture mean-reversion; ATR normalises volatility; "
"RSI + MACD confirm momentum; 2:1 TP:SL ratio supports positive expectancy."
),
"notes": (
"Features: BB (20,2) width/pct, ATR-14/NATR, RSI-14, MACD histogram, "
"EMA 20/50/200 spreads, Stochastic %K/%D, candle-body ratios, "
"ROC at multiple horizons, volatility ratio, BB squeeze flag, "
"lagged versions of key features. "
"Threshold 0.56 filters marginal signals, improving precision. "
"target_horizon=4 (1 hour) balances signal frequency vs. noise."
),
}
|
||||||||||
|
🥉
|
EUR/USD XGBoost SMA+RSI+MACD+BB Trend Rider
Maximize risk-adjusted return (Sharpe/Calmar) on EUR/USD 15-min data. XGBoost with moderate depth and regularisation to avoid overfitting on…
|
S
@still-lynx-704
|
EURUSD | 15min | 50.0%57.1% | +9.05%+11.19% | 2.642.15 | 1.05%1.05% | 6614 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-05 09:59:46
# Model : XGBoost
# Feature Eng. : SMA (20,50,200), BB (20,2.0), RSI 14, MACD (12,26,9), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-23"
END_DATE = "2026-04-23"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA 20, 50, 200 + distance from close ──────────────────────────────
for p in [20, 50, 200]:
sma = close.rolling(p).mean()
df[f"sma_{p}"] = sma
df[f"dm_sma_{p}"] = (close - sma) / sma
# ── Bollinger Bands (20, 2.0) ───────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
bb_upper = bb_mid + 2.0 * bb_std
bb_lower = bb_mid - 2.0 * bb_std
bb_range = bb_upper - bb_lower
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = bb_range / bb_mid
df["bb_pct"] = (close - bb_lower) / bb_range.replace(0, np.nan)
# ── RSI 14 ──────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=13, min_periods=14, adjust=False).mean()
avg_loss = loss.ewm(com=13, min_periods=14, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100.0 - (100.0 / (1.0 + rs))
# ── MACD (12, 26, 9) ────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
# ── ATR 14 + NATR ───────────────────────────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr = tr.ewm(com=13, min_periods=14, adjust=False).mean()
df["atr_14"] = atr
df["natr"] = atr / close
# ── Additional derived features ─────────────────────────────────────────
# Price momentum over multiple horizons
for lag in [1, 4, 8, 16]:
df[f"mom_{lag}"] = close.pct_change(lag)
# Log return
df["log_ret_1"] = np.log(close / close.shift(1))
# Candle body and wick ratios
body = (close - open_).abs()
candle_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng
df["upper_wick_ratio"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_rng
df["lower_wick_ratio"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_rng
df["candle_dir"] = np.where(close >= open_, 1.0, -1.0)
# Volume / volatility proxy: rolling std of returns
ret = close.pct_change()
df["vol_8"] = ret.rolling(8).std()
df["vol_20"] = ret.rolling(20).std()
# RSI-derived features
df["rsi_dist_50"] = df["rsi_14"] - 50.0
df["rsi_overbought"] = np.where(df["rsi_14"] > 70, 1.0, 0.0)
df["rsi_oversold"] = np.where(df["rsi_14"] < 30, 1.0, 0.0)
# MACD cross signal
df["macd_cross"] = np.where(
(df["macd_hist"] > 0) & (df["macd_hist"].shift(1) <= 0), 1.0,
np.where(
(df["macd_hist"] < 0) & (df["macd_hist"].shift(1) >= 0), -1.0,
0.0
)
)
# BB squeeze: narrow bands relative to recent average
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).mean(), 1.0, 0.0)
# Price relative to SMA crossovers
df["sma20_above_sma50"] = np.where(df["sma_20"] > df["sma_50"], 1.0, -1.0)
df["sma50_above_sma200"] = np.where(df["sma_50"] > df["sma_200"], 1.0, -1.0)
# High/low channel breakout features
df["high_20"] = high.rolling(20).max()
df["low_20"] = low.rolling(20).min()
df["chan_pos"] = (close - df["low_20"]) / (df["high_20"] - df["low_20"]).replace(0, np.nan)
# Lagged RSI and MACD hist
df["rsi_14_lag1"] = df["rsi_14"].shift(1)
df["rsi_14_lag4"] = df["rsi_14"].shift(4)
df["macd_hist_lag1"] = df["macd_hist"].shift(1)
# ATR trend: expanding vs contracting volatility
df["atr_ratio"] = df["atr_14"] / df["atr_14"].rolling(50).mean()
# Fill NaN from warm-up
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD XGBoost SMA+RSI+MACD+BB Trend Rider",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.2,
"reg_alpha": 0.1,
"reg_lambda": 1.5,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.54,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 20],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on EUR/USD 15-min data. "
"XGBoost with moderate depth and regularisation to avoid overfitting on "
"noisy FX data. Conservative SL/TP ratio of 1:2 improves expectancy. "
"Session filter keeps the model active during liquid London/NY overlap. "
"Min ATR filter avoids low-volatility noise. SMA-50 trend filter aligns "
"trades with the prevailing medium-term trend, reducing whipsaw."
),
"notes": (
"Feature set: SMA 20/50/200 distances, BB width/pct, RSI 14, MACD histogram, "
"ATR/NATR, multi-horizon momentum, candle structure, volatility, channel position, "
"lagged indicators, and cross-over binary signals. "
"Hyperparameters tuned for bias-variance balance: shallow trees (depth 4), "
"high n_estimators with low learning rate, stochastic sampling, and L1/L2 "
"regularisation reduce overfitting. Threshold 0.54 slightly above default to "
"filter marginal signals while maintaining trade frequency."
),
}
|
||||||||||
|
8.37
|
GBP/USD SMA Trend Gradient Boosting Risk-Adj
Maximize risk-adjusted return (Sharpe/Calmar) on GBP/USD 15-min data. GradientBoostingClassifier chosen for its strong bias-variance tradeof…
|
R
@ratio_witch
|
GBPUSD | 15min | 43.1%53.3% | +6.85%+21.29% | 1.711.98 | 2.69%2.69% | 7215 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:47:56
# Model : Gradient Boosting
# Feature Eng. : SMA (20,50,200) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/GBPUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA features (required) ──────────────────────────────────────────────
for period in [20, 50, 200]:
sma = close.rolling(period).mean()
df[f"sma_{period}"] = sma
df[f"dm_sma_{period}"] = (close - sma) / sma
# ── SMA crossover signals ────────────────────────────────────────────────
sma_20 = close.rolling(20).mean()
sma_50 = close.rolling(50).mean()
sma_200 = close.rolling(200).mean()
df["sma_20_50_cross"] = np.where(sma_20 > sma_50, 1.0, -1.0)
df["sma_20_200_cross"] = np.where(sma_20 > sma_200, 1.0, -1.0)
df["sma_50_200_cross"] = np.where(sma_50 > sma_200, 1.0, -1.0)
# ── Price momentum features ──────────────────────────────────────────────
for lag in [1, 2, 4, 8, 16]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Volatility: rolling std of returns ──────────────────────────────────
ret_1 = close.pct_change(1)
for window in [8, 20, 50]:
df[f"vol_{window}"] = ret_1.rolling(window).std()
# ── ATR (Average True Range) ─────────────────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
for atr_period in [14, 50]:
atr = tr.rolling(atr_period).mean()
df[f"atr_{atr_period}"] = atr
df[f"natr_{atr_period}"] = atr / close
# ── RSI ──────────────────────────────────────────────────────────────────
for rsi_period in [14, 28]:
delta = close.diff()
gain = delta.clip(lower=0).rolling(rsi_period).mean()
loss = (-delta.clip(upper=0)).rolling(rsi_period).mean()
rs = gain / (loss + 1e-10)
df[f"rsi_{rsi_period}"] = 100 - (100 / (1 + rs))
# ── MACD ─────────────────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
df["macd_hist_norm"] = (macd_line - signal_line) / (close + 1e-10)
# ── Bollinger Bands ───────────────────────────────────────────────────────
for bb_period in [20, 50]:
bb_mid = close.rolling(bb_period).mean()
bb_std = close.rolling(bb_period).std()
bb_upper = bb_mid + 2.0 * bb_std
bb_lower = bb_mid - 2.0 * bb_std
bb_width = (bb_upper - bb_lower) / (bb_mid + 1e-10)
bb_pos = (close - bb_lower) / (bb_upper - bb_lower + 1e-10)
df[f"bb_width_{bb_period}"] = bb_width
df[f"bb_pos_{bb_period}"] = bb_pos
# ── Stochastic Oscillator ────────────────────────────────────────────────
for stoch_period in [14, 28]:
lowest_low = low.rolling(stoch_period).min()
highest_high = high.rolling(stoch_period).max()
stoch_k = (close - lowest_low) / (highest_high - lowest_low + 1e-10) * 100
stoch_d = stoch_k.rolling(3).mean()
df[f"stoch_k_{stoch_period}"] = stoch_k
df[f"stoch_d_{stoch_period}"] = stoch_d
# ── Rate of Change (ROC) ──────────────────────────────────────────────────
for roc_period in [5, 10, 20]:
df[f"roc_{roc_period}"] = close.pct_change(roc_period)
# ── Candle body and shadow features ──────────────────────────────────────
body = (close - open_).abs()
candle_range = (high - low).abs()
df["body_ratio"] = body / (candle_range + 1e-10)
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / (candle_range + 1e-10)
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / (candle_range + 1e-10)
df["bullish_candle"] = np.where(close > open_, 1.0, -1.0)
# ── Volume-proxy: candle range as volatility proxy ────────────────────────
df["range_norm"] = candle_range / (close + 1e-10)
df["range_ma_ratio"] = candle_range / (candle_range.rolling(20).mean() + 1e-10)
# ── Lag features for return predictors ───────────────────────────────────
for col_lag in ["rsi_14", "macd_hist", "bb_pos_20"]:
for lag in [1, 2, 3]:
df[f"{col_lag}_lag{lag}"] = df[col_lag].shift(lag)
# ── Distance of close from recent high/low ────────────────────────────────
for lookback in [10, 20, 50]:
roll_high = high.rolling(lookback).max()
roll_low = low.rolling(lookback).min()
df[f"dist_high_{lookback}"] = (close - roll_high) / (roll_high + 1e-10)
df[f"dist_low_{lookback}"] = (close - roll_low) / (roll_low + 1e-10)
# ── Trend strength: ADX proxy ─────────────────────────────────────────────
adx_period = 14
tr_adx = tr.copy()
plus_dm = pd.Series(np.where((high.diff() > 0) & (high.diff() > -low.diff()), high.diff(), 0.0), index=close.index)
minus_dm = pd.Series(np.where((-low.diff() > 0) & (-low.diff() > high.diff()), -low.diff(), 0.0), index=close.index)
atr_adx = tr_adx.rolling(adx_period).mean()
plus_di = 100 * plus_dm.rolling(adx_period).mean() / (atr_adx + 1e-10)
minus_di = 100 * minus_dm.rolling(adx_period).mean() / (atr_adx + 1e-10)
dx = (100 * (plus_di - minus_di).abs() / (plus_di + minus_di + 1e-10))
df["adx"] = dx.rolling(adx_period).mean()
df["plus_di"] = plus_di
df["minus_di"] = minus_di
# ── Fill NaN from indicator warm-up ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD SMA Trend Gradient Boosting Risk-Adj",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"min_samples_leaf": 20,
"max_features": "sqrt",
"n_iter_no_change": 30,
"validation_fraction": 0.1,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.57,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 18],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on GBP/USD 15-min data. "
"GradientBoostingClassifier chosen for its strong bias-variance tradeoff "
"on medium-sized tabular datasets without needing GPU. "
"Hyperparameters: moderate depth=4 prevents overfitting, learning_rate=0.04 "
"with 400 estimators balances convergence vs generalisation, subsample=0.75 "
"adds stochasticity to reduce variance, min_samples_leaf=20 enforces statistical "
"significance at each leaf. Early stopping via n_iter_no_change guards against "
"overfit on the training fold. Signal threshold 0.57 filters marginal signals "
"to improve precision. SL=0.5%, TP=1.0% gives 1:2 RR. Session filter 6-18 UTC "
"covers London+NY overlap — highest GBP/USD liquidity and tighter spreads. "
"sma_50 trend filter ensures we only trade in the direction of medium-term trend, "
"reducing whipsaw losses. target_horizon=4 bars (1 hour) gives the model enough "
"time for moves to develop while staying relevant for intraday trading."
),
"notes": (
"Features: SMA 20/50/200 with distance metrics (core requirement), RSI 14/28, "
"MACD, Bollinger Bands 20/50, Stochastic 14/28, ATR 14/50, NATR, ROC, ADX, "
"candle body/shadow ratios, lagged RSI/MACD/BB features, distance from rolling "
"high/low, SMA crossover signals, multi-lag return features. "
"All features are backward-looking only (no lookahead bias). "
"on_opposite=reverse for fast trend-following entries without missing reversals."
),
}
|
||||||||||
|
7.18
|
AUD/USD EMA Cross RSI Gradient Boost Scalper
Maximize risk-adjusted return (Sharpe) on AUD/USD 15-min bars. GradientBoostingClassifier with shrinkage (lr=0.04), moderate depth (4), subs…
|
R
@rapid-shark-854
|
AUDUSD | 15min | 59.8%69.0% | +1.99%+30.64% | 1.052.04 | 6.00%6.00% | 336116 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:21:53
# Model : Gradient Boosting
# Feature Eng. : EMA (9,21), RSI 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# --- EMA 9 and EMA 21 (required) ---
ema_9 = close.ewm(span=9, adjust=False).mean()
ema_21 = close.ewm(span=21, adjust=False).mean()
df["ema_9"] = ema_9
df["ema_21"] = ema_21
df["dm_ema_9"] = (close - ema_9) / ema_9
df["dm_ema_21"] = (close - ema_21) / ema_21
# EMA crossover signal and spread
df["ema_cross"] = ema_9 - ema_21
df["ema_cross_prev"] = df["ema_cross"].shift(1)
df["ema_cross_sign"] = np.sign(df["ema_cross"])
df["ema_cross_change"] = df["ema_cross_sign"] - np.sign(df["ema_cross_prev"])
# --- RSI 14 (required) ---
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=13, adjust=False).mean()
avg_loss = loss.ewm(com=13, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi_14 = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi_14
# RSI derived features
df["rsi_14_norm"] = (rsi_14 - 50) / 50
df["rsi_overbought"] = np.where(rsi_14 > 70, 1, 0)
df["rsi_oversold"] = np.where(rsi_14 < 30, 1, 0)
df["rsi_mid_cross"] = np.where(rsi_14 > 50, 1, -1)
# --- Additional EMAs for context ---
ema_50 = close.ewm(span=50, adjust=False).mean()
ema_200 = close.ewm(span=200, adjust=False).mean()
df["ema_50"] = ema_50
df["ema_200"] = ema_200
df["dm_ema_50"] = (close - ema_50) / ema_50
df["dm_ema_200"] = (close - ema_200) / ema_200
df["ema_50_200_spread"] = (ema_50 - ema_200) / ema_200
# --- ATR (14 periods) ---
tr1 = high - low
tr2 = (high - close.shift(1)).abs()
tr3 = (low - close.shift(1)).abs()
true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
atr_14 = true_range.ewm(com=13, adjust=False).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / close
# --- Bollinger Bands (20, 2) ---
sma_20 = close.rolling(20).mean()
std_20 = close.rolling(20).std()
bb_upper = sma_20 + 2 * std_20
bb_lower = sma_20 - 2 * std_20
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_pct_b"] = (close - bb_lower) / (bb_upper - bb_lower).replace(0, np.nan)
df["bb_width"] = (bb_upper - bb_lower) / sma_20
# --- MACD (12, 26, 9) ---
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
macd_hist = macd_line - macd_signal
df["macd_line"] = macd_line
df["macd_signal_line"] = macd_signal
df["macd_hist"] = macd_hist
df["macd_hist_sign"] = np.sign(macd_hist)
df["macd_hist_change"] = np.sign(macd_hist) - np.sign(macd_hist.shift(1))
# --- Momentum & Rate of Change ---
df["mom_5"] = close.pct_change(5)
df["mom_10"] = close.pct_change(10)
df["mom_20"] = close.pct_change(20)
df["roc_3"] = close.pct_change(3)
# --- Candlestick features ---
df["body"] = (close - open_) / atr_14
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / atr_14
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / atr_14
df["bar_range"] = (high - low) / atr_14
# --- Volume-proxy: price range relative momentum ---
df["high_low_ratio"] = (high - low) / close
# --- Stochastic Oscillator (14, 3) ---
lowest_low = low.rolling(14).min()
highest_high = high.rolling(14).max()
stoch_k = 100 * (close - lowest_low) / (highest_high - lowest_low).replace(0, np.nan)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_kd_diff"] = stoch_k - stoch_d
# --- Rolling volatility ---
df["vol_10"] = close.pct_change().rolling(10).std()
df["vol_20"] = close.pct_change().rolling(20).std()
df["vol_ratio"] = df["vol_10"] / df["vol_20"].replace(0, np.nan)
# --- Lagged RSI and EMA cross (for sequential signal detection) ---
df["rsi_14_lag1"] = rsi_14.shift(1)
df["rsi_14_lag2"] = rsi_14.shift(2)
df["ema_cross_lag1"] = df["ema_cross"].shift(1)
df["ema_cross_lag2"] = df["ema_cross"].shift(2)
# --- Trend alignment: both EMAs agree ---
df["trend_aligned_bull"] = np.where((ema_9 > ema_21) & (ema_21 > ema_50), 1, 0)
df["trend_aligned_bear"] = np.where((ema_9 < ema_21) & (ema_21 < ema_50), 1, 0)
# --- RSI momentum divergence proxy ---
price_chg_5 = close.pct_change(5)
rsi_chg_5 = rsi_14.diff(5)
df["rsi_price_div"] = np.where(
(price_chg_5 > 0) & (rsi_chg_5 < 0), -1,
np.where((price_chg_5 < 0) & (rsi_chg_5 > 0), 1, 0)
)
# --- SMA 50 distance (for trend_filter compatibility) ---
df["sma_50"] = close.rolling(50).mean()
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD EMA Cross RSI Gradient Boost Scalper",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.8,
"max_features": "sqrt",
"min_samples_leaf": 20,
"min_samples_split": 40,
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"tol": 1e-4,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe) on AUD/USD 15-min bars. "
"GradientBoostingClassifier with shrinkage (lr=0.04), moderate depth (4), "
"subsampling (0.8) and sqrt feature fraction controls overfitting on a noisy FX "
"series. Early stopping (n_iter_no_change=30) prevents over-training. "
"SL=0.5%/TP=1.0% gives 1:2 RR. Threshold=0.55 filters low-confidence signals. "
"EMA 9/21 crossover with RSI 14 confirmation is the primary signal logic, "
"reinforced by MACD, Bollinger Bands, Stochastic, and multi-period momentum."
),
"notes": (
"Features: EMA 9, 21, 50, 200 distances; RSI 14 with overbought/oversold flags; "
"MACD histogram; Bollinger %B and width; Stochastic K/D; ATR-normalized candle "
"body/wicks; 5/10/20-bar momentum; rolling volatility ratio; trend alignment flags; "
"RSI-price divergence proxy. Target horizon 4 bars (1 hour ahead). "
"All features are lagged or rolling — no lookahead bias."
),
}
|
||||||||||
|
6.59
|
AUD/USD XGBoost SMA+RSI+MACD+BB Momentum
Maximize risk-adjusted return (Sharpe/Calmar) on AUD/USD 15-min. XGBoost with depth-4 trees and conservative regularization (reg_lambda=1.5,…
|
D
@delta-atlas-858
|
AUDUSD | 15min | 62.9%63.0% | +10.32%+21.93% | 1.171.89 | 3.96%3.96% | 74592 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:32:18
# Model : XGBoost
# Feature Eng. : SMA (20,50,200), BB (20,2.0), RSI 14, MACD (12,26,9), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# AUDUSD 15-min XGBoost Strategy
# SMA + RSI + MACD + Bollinger Bands + ATR Feature Set
# Optimized for Risk-Adjusted Return
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA 20, 50, 200 + distance from close ──────────────────────────────
for period in [20, 50, 200]:
sma = close.rolling(period).mean()
df[f"sma_{period}"] = sma
df[f"dm_sma_{period}"] = (close - sma) / sma
# ── Bollinger Bands (20, 2.0) ───────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
bb_upper = bb_mid + 2.0 * bb_std
bb_lower = bb_mid - 2.0 * bb_std
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
bb_range = bb_upper - bb_lower
df["bb_pct"] = (close - bb_lower) / bb_range
# ── RSI 14 ──────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=13, min_periods=14).mean()
avg_loss = loss.ewm(com=13, min_periods=14).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100.0 - (100.0 / (1.0 + rs))
# ── MACD (12, 26, 9) ────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
# ── ATR 14 + Normalised ATR ─────────────────────────────────────────────
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(com=13, min_periods=14).mean()
df["atr_14"] = atr
df["natr"] = atr / close
# ── Price momentum / rate-of-change ────────────────────────────────────
for n in [1, 4, 8, 16]:
df[f"roc_{n}"] = close.pct_change(n)
# ── Candle body & wick features ─────────────────────────────────────────
body = (close - open_).abs()
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["candle_dir"] = np.where(close >= open_, 1.0, -1.0)
# ── Volume (if present) ─────────────────────────────────────────────────
if "volume" in df.columns:
vol_ma = df["volume"].rolling(20).mean()
df["vol_ratio"] = df["volume"] / vol_ma.replace(0, np.nan)
# ── Lagged RSI & MACD histogram ─────────────────────────────────────────
for lag in [1, 2, 3]:
df[f"rsi_14_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
# ── RSI overbought / oversold zones ─────────────────────────────────────
df["rsi_ob"] = np.where(df["rsi_14"] > 70, 1.0, 0.0)
df["rsi_os"] = np.where(df["rsi_14"] < 30, 1.0, 0.0)
df["rsi_mid_up"] = np.where((df["rsi_14"] > 50) & (df["rsi_14"] <= 70), 1.0, 0.0)
df["rsi_mid_dn"] = np.where((df["rsi_14"] >= 30) & (df["rsi_14"] < 50), 1.0, 0.0)
# ── MACD cross signals ───────────────────────────────────────────────────
df["macd_cross_up"] = np.where(
(df["macd_line"] > df["macd_signal"]) &
(df["macd_line"].shift(1) <= df["macd_signal"].shift(1)),
1.0, 0.0
)
df["macd_cross_dn"] = np.where(
(df["macd_line"] < df["macd_signal"]) &
(df["macd_line"].shift(1) >= df["macd_signal"].shift(1)),
1.0, 0.0
)
# ── Price position relative to SMA alignment ────────────────────────────
df["trend_aligned_bull"] = np.where(
(close > df["sma_20"]) & (df["sma_20"] > df["sma_50"]) & (df["sma_50"] > df["sma_200"]),
1.0, 0.0
)
df["trend_aligned_bear"] = np.where(
(close < df["sma_20"]) & (df["sma_20"] < df["sma_50"]) & (df["sma_50"] < df["sma_200"]),
1.0, 0.0
)
# ── Bollinger Band squeeze (low volatility) ──────────────────────────────
bb_width_ma = df["bb_width"].rolling(20).mean()
df["bb_squeeze"] = np.where(df["bb_width"] < bb_width_ma, 1.0, 0.0)
# ── Rolling close statistics ─────────────────────────────────────────────
df["close_zscore_20"] = (close - close.rolling(20).mean()) / close.rolling(20).std(ddof=0)
df["close_zscore_50"] = (close - close.rolling(50).mean()) / close.rolling(50).std(ddof=0)
# ── Volatility regime ────────────────────────────────────────────────────
natr_ma = df["natr"].rolling(20).mean()
df["vol_regime_high"] = np.where(df["natr"] > natr_ma, 1.0, 0.0)
# ── Fill NaN from indicator warm-up ─────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD XGBoost SMA+RSI+MACD+BB Momentum",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 500,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.1,
"reg_alpha": 0.1,
"reg_lambda": 1.5,
"scale_pos_weight": 1.0,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on AUD/USD 15-min. "
"XGBoost with depth-4 trees and conservative regularization (reg_lambda=1.5, "
"min_child_weight=5) to reduce overfitting on FX data. "
"2:1 RR (SL=0.5%, TP=1.0%) ensures positive expectancy with ~40%+ win rate. "
"Subsample + colsample add stochastic diversity. 500 estimators with lr=0.04 "
"balances bias-variance. Threshold 0.55 filters marginal signals."
),
"notes": (
"Features: SMA(20/50/200) with distances, Bollinger Bands width+pct, RSI-14 "
"with zone flags, MACD histogram + crosses, ATR-14 + NATR, momentum ROC(1/4/8/16), "
"candle body/wick ratios, trend alignment flags, BB squeeze, z-scores, vol regime. "
"Reverse on opposite signal to capture trend reversals. Session filter disabled "
"to capture AUD/USD Asian + London + NY sessions. Target horizon = 4 bars (1 hour)."
),
}
|
||||||||||
|
5.25
|
AUD/USD RSI+MACD Gradient Boosting Scalper
Maximize risk-adjusted return on AUD/USD 15-min data using GradientBoostingClassifier. RSI-14 captures momentum extremes and divergence cond…
|
R
@ratio_witch
|
AUDUSD | 15min | 61.3%66.7% | +4.80%+25.62% | 1.081.77 | 5.76%5.76% | 721126 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 03:05:53
# Model : Gradient Boosting
# Feature Eng. : RSI 14, MACD (12,26,9) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── RSI 14 ──────────────────────────────────────────────────────────────
period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=period - 1, min_periods=period).mean()
avg_loss = loss.ewm(com=period - 1, min_periods=period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# ── MACD (12, 26, 9) ────────────────────────────────────────────────────
ema_fast = close.ewm(span=12, adjust=False).mean()
ema_slow = close.ewm(span=26, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=9, adjust=False).mean()
macd_hist = macd_line - signal_line
df["macd_line"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_hist
# ── RSI derived features ─────────────────────────────────────────────────
df["rsi_14_lag1"] = df["rsi_14"].shift(1)
df["rsi_14_lag2"] = df["rsi_14"].shift(2)
df["rsi_14_delta"] = df["rsi_14"] - df["rsi_14_lag1"]
df["rsi_ob"] = np.where(df["rsi_14"] > 70, 1, 0)
df["rsi_os"] = np.where(df["rsi_14"] < 30, 1, 0)
# ── MACD derived features ────────────────────────────────────────────────
df["macd_hist_lag1"] = macd_hist.shift(1)
df["macd_hist_delta"] = macd_hist - macd_hist.shift(1)
df["macd_cross_bull"] = np.where((macd_line > signal_line) & (macd_line.shift(1) <= signal_line.shift(1)), 1, 0)
df["macd_cross_bear"] = np.where((macd_line < signal_line) & (macd_line.shift(1) >= signal_line.shift(1)), 1, 0)
df["macd_above_zero"] = np.where(macd_line > 0, 1, 0)
df["macd_hist_positive"] = np.where(macd_hist > 0, 1, 0)
# ── ATR (14) ─────────────────────────────────────────────────────────────
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(span=14, adjust=False).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / close
# ── Bollinger Bands (20, 2) ───────────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_upper = bb_mid + 2 * bb_std
bb_lower = bb_mid - 2 * bb_std
df["bb_pct_b"] = (close - bb_lower) / (bb_upper - bb_lower).replace(0, np.nan)
df["bb_width"] = (bb_upper - bb_lower) / bb_mid.replace(0, np.nan)
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).mean(), 1, 0)
# ── Price momentum features ───────────────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_4"] = close.pct_change(4)
df["ret_8"] = close.pct_change(8)
df["ret_16"] = close.pct_change(16)
# ── Rolling volatility ────────────────────────────────────────────────────
df["vol_8"] = df["ret_1"].rolling(8).std()
df["vol_20"] = df["ret_1"].rolling(20).std()
# ── EMA trend features ───────────────────────────────────────────────────
ema_20 = close.ewm(span=20, adjust=False).mean()
ema_50 = close.ewm(span=50, adjust=False).mean()
df["ema_20"] = ema_20
df["ema_50"] = ema_50
df["price_vs_ema20"] = (close - ema_20) / ema_20
df["price_vs_ema50"] = (close - ema_50) / ema_50
df["ema20_vs_ema50"] = (ema_20 - ema_50) / ema_50
# ── Candle body / wick features ──────────────────────────────────────────
body = (close - open_).abs()
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["bull_candle"] = np.where(close > open_, 1, 0)
# ── Rolling high/low position ─────────────────────────────────────────────
roll_high_20 = high.rolling(20).max()
roll_low_20 = low.rolling(20).min()
roll_range_20 = (roll_high_20 - roll_low_20).replace(0, np.nan)
df["price_position_20"] = (close - roll_low_20) / roll_range_20
# ── RSI + MACD interaction ────────────────────────────────────────────────
df["rsi_macd_product"] = df["rsi_14"] * macd_hist
df["rsi_norm"] = (df["rsi_14"] - 50) / 50
# ── Session / time features ───────────────────────────────────────────────
if hasattr(close.index, "hour"):
df["hour"] = close.index.hour
df["session_london"] = np.where((close.index.hour >= 7) & (close.index.hour < 16), 1, 0)
df["session_ny"] = np.where((close.index.hour >= 13) & (close.index.hour < 21), 1, 0)
df["session_asia"] = np.where((close.index.hour >= 22) | (close.index.hour < 7), 1, 0)
else:
df["hour"] = 0
df["session_london"] = 0
df["session_ny"] = 0
df["session_asia"] = 0
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD RSI+MACD Gradient Boosting Scalper",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.05,
"subsample": 0.8,
"min_samples_leaf": 20,
"max_features": "sqrt",
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"tol": 1e-4,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.01,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": None,
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return on AUD/USD 15-min data using GradientBoostingClassifier. "
"RSI-14 captures momentum extremes and divergence conditions; MACD (12,26,9) provides "
"trend direction and momentum shifts via crossovers and histogram slope. Additional "
"features (BB, ATR, EMA trend, candle structure, session timing) enrich the feature "
"space. GradientBoosting with shallow trees (depth=4), moderate learning rate (0.05), "
"and early stopping via n_iter_no_change prevents overfitting. SL=0.5%, TP=1.0% gives "
"a 1:2 risk/reward ratio, improving Sharpe and Calmar. Threshold=0.55 filters low-confidence "
"signals to reduce noise. Reverse on opposite signal maximizes capital efficiency."
),
"notes": (
"n_estimators=400 with early stopping balances bias-variance. max_depth=4 keeps trees "
"shallow to reduce overfitting on FX microstructure noise. subsample=0.8 adds stochastic "
"gradient boosting regularization. min_samples_leaf=20 prevents fitting to outlier bars. "
"max_features='sqrt' adds feature randomization similar to random forests. "
"target_horizon=4 (1 hour) aligns with typical AUD/USD intraday swing durations."
),
}
|
||||||||||
|
5.12
|
AUD/USD Stoch+BB+RSI Mean-Reversion XGBoost
Maximize risk-adjusted return (Sharpe / Calmar). XGBoost chosen for its ability to capture non-linear interactions between Stochastic, Bolli…
|
S
@still-lynx-704
|
AUDUSD | 15min | 62.5%62.1% | +10.93%+19.40% | 1.181.70 | 4.00%4.00% | 74295 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:51:31
# Model : XGBoost
# Feature Eng. : BB (20,2.0), RSI 14, Stochastic (14,3) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_std_val = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_std_val
bb_lower = bb_mid - bb_std * bb_std_val
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
avg_loss = loss.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi"] = 100 - (100 / (1 + rs))
# ── Stochastic Oscillator (K=14, D=3) ────────────────────────────────────
stoch_k_period = 14
stoch_d_period = 3
lowest_low = low.rolling(stoch_k_period).min()
highest_high = high.rolling(stoch_k_period).max()
denom = (highest_high - lowest_low).replace(0, np.nan)
df["stoch_k"] = 100 * (close - lowest_low) / denom
df["stoch_d"] = df["stoch_k"].rolling(stoch_d_period).mean()
df["stoch_kd_diff"] = df["stoch_k"] - df["stoch_d"]
# ── Additional derived features ──────────────────────────────────────────
# RSI overbought / oversold zone flags
df["rsi_ob"] = np.where(df["rsi"] > 70, 1, 0)
df["rsi_os"] = np.where(df["rsi"] < 30, 1, 0)
df["rsi_mid"] = df["rsi"] - 50.0
# Stochastic overbought / oversold zone flags
df["stoch_ob"] = np.where(df["stoch_k"] > 80, 1, 0)
df["stoch_os"] = np.where(df["stoch_k"] < 20, 1, 0)
# BB position regime: price relative to bands
df["price_above_bb_upper"] = np.where(close > bb_upper, 1, 0)
df["price_below_bb_lower"] = np.where(close < bb_lower, 1, 0)
df["price_vs_bb_mid"] = close - bb_mid
# ATR-based volatility (14-bar)
atr_period = 14
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
df["atr14"] = tr.ewm(com=atr_period - 1, min_periods=atr_period).mean()
df["natr14"] = df["atr14"] / close
# SMA trend context
df["sma_20"] = close.rolling(20).mean()
df["sma_50"] = close.rolling(50).mean()
df["sma_200"] = close.rolling(200).mean()
df["price_vs_sma20"] = (close - df["sma_20"]) / df["sma_20"]
df["price_vs_sma50"] = (close - df["sma_50"]) / df["sma_50"]
df["sma20_vs_sma50"] = (df["sma_20"] - df["sma_50"]) / df["sma_50"]
# Momentum: rate of change
df["roc_5"] = close.pct_change(5)
df["roc_10"] = close.pct_change(10)
df["roc_20"] = close.pct_change(20)
# MACD-style (EMA 12 - EMA 26)
ema12 = close.ewm(span=12, min_periods=12).mean()
ema26 = close.ewm(span=26, min_periods=26).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, min_periods=9).mean()
df["macd"] = macd_line
df["macd_signal"] = macd_signal
df["macd_hist"] = macd_line - macd_signal
# Candle body / wick ratios
body = (close - open_).abs()
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["bullish_bar"] = np.where(close > open_, 1, 0)
# Lagged RSI / Stoch features (1 and 2 bars back)
df["rsi_lag1"] = df["rsi"].shift(1)
df["rsi_lag2"] = df["rsi"].shift(2)
df["stoch_k_lag1"] = df["stoch_k"].shift(1)
df["bb_pct_lag1"] = df["bb_pct"].shift(1)
# RSI slope
df["rsi_slope"] = df["rsi"] - df["rsi"].shift(3)
# Stoch K crossing D (momentum signal)
df["stoch_cross_up"] = np.where((df["stoch_k"] > df["stoch_d"]) &
(df["stoch_k"].shift(1) <= df["stoch_d"].shift(1)), 1, 0)
df["stoch_cross_down"] = np.where((df["stoch_k"] < df["stoch_d"]) &
(df["stoch_k"].shift(1) >= df["stoch_d"].shift(1)), 1, 0)
# Volume (if present)
if "volume" in df.columns:
vol_ma = df["volume"].rolling(20).mean()
df["vol_ratio"] = df["volume"] / vol_ma.replace(0, np.nan)
else:
df["vol_ratio"] = 1.0
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD Stoch+BB+RSI Mean-Reversion XGBoost",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.15,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe / Calmar). "
"XGBoost chosen for its ability to capture non-linear interactions "
"between Stochastic, Bollinger Bands, and RSI regimes. "
"Shallow trees (max_depth=4) + high regularisation (reg_lambda=1.5, gamma=0.15) "
"prevent overfitting on 15-min FX data. "
"2:1 TP:SL ratio (1.0% / 0.5%) improves expectancy per trade. "
"Reverse on opposite signal minimises flat time and captures regime flips."
),
"notes": (
"Features include BB width/pct, RSI(14) with overbought/oversold flags, "
"Stochastic K/D crossovers, MACD histogram, ATR volatility, SMA trend context, "
"candle body ratios, lagged indicators, and momentum ROC. "
"signal_threshold=0.55 balances precision vs recall on directional calls. "
"session_filter covers full 24h to capture Asia + London + NY sessions for AUD/USD."
),
}
|
||||||||||
|
4.78
|
AUD/USD Stochastic BB Mean-Reversion (GBM)
Maximize risk-adjusted return (Sharpe/Calmar) on AUD/USD 15-min. GradientBoostingClassifier with moderate depth and learning rate chosen to …
|
P
@pivot_kid
|
AUDUSD | 15min | 64.8%67.0% | +7.88%+20.60% | 1.201.70 | 4.91%4.91% | 358112 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:24:20
# Model : Gradient Boosting
# Feature Eng. : BB (20,2.0), RSI 14, Stochastic (14,3) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_std_ = close.rolling(bb_period).std(ddof=1)
bb_upper = bb_mid + bb_std * bb_std_
bb_lower = bb_mid - bb_std * bb_std_
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
avg_loss = loss.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi"] = 100.0 - (100.0 / (1.0 + rs))
# ── Stochastic Oscillator (K=14, D=3) ────────────────────────────────────
stoch_k = 14
stoch_d = 3
low_min = low.rolling(stoch_k).min()
high_max = high.rolling(stoch_k).max()
k_raw = 100.0 * (close - low_min) / (high_max - low_min).replace(0, np.nan)
df["stoch_k"] = k_raw
df["stoch_d"] = k_raw.rolling(stoch_d).mean()
# ── ATR (14) ─────────────────────────────────────────────────────────────
atr_period = 14
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
df["atr"] = tr.ewm(com=atr_period - 1, min_periods=atr_period).mean()
df["natr"] = df["atr"] / close
# ── Trend / Momentum features ─────────────────────────────────────────────
df["sma_20"] = close.rolling(20).mean()
df["sma_50"] = close.rolling(50).mean()
df["sma_100"] = close.rolling(100).mean()
df["price_vs_sma20"] = (close - df["sma_20"]) / df["sma_20"]
df["price_vs_sma50"] = (close - df["sma_50"]) / df["sma_50"]
df["price_vs_sma100"] = (close - df["sma_100"]) / df["sma_100"]
df["sma20_vs_sma50"] = (df["sma_20"] - df["sma_50"]) / df["sma_50"]
# ── MACD (12, 26, 9) ─────────────────────────────────────────────────────
ema12 = close.ewm(span=12, min_periods=12).mean()
ema26 = close.ewm(span=26, min_periods=26).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, min_periods=9).mean()
df["macd"] = macd_line
df["macd_signal"] = macd_signal
df["macd_hist"] = macd_line - macd_signal
# ── Rate-of-Change features ───────────────────────────────────────────────
for p in [4, 8, 16]:
df[f"roc_{p}"] = close.pct_change(p)
# ── Volatility regime ────────────────────────────────────────────────────
df["vol_8"] = close.pct_change().rolling(8).std()
df["vol_20"] = close.pct_change().rolling(20).std()
df["vol_ratio"] = df["vol_8"] / df["vol_20"].replace(0, np.nan)
# ── Candle body / shadow features ────────────────────────────────────────
df["body"] = (close - open_).abs()
df["upper_shadow"] = high - pd.concat([close, open_], axis=1).max(axis=1)
df["lower_shadow"] = pd.concat([close, open_], axis=1).min(axis=1) - low
df["body_ratio"] = df["body"] / (high - low).replace(0, np.nan)
# ── RSI-derived features ──────────────────────────────────────────────────
df["rsi_above_50"] = np.where(df["rsi"] > 50, 1, 0)
df["rsi_overbought"] = np.where(df["rsi"] > 70, 1, 0)
df["rsi_oversold"] = np.where(df["rsi"] < 30, 1, 0)
df["rsi_lag1"] = df["rsi"].shift(1)
df["rsi_lag4"] = df["rsi"].shift(4)
# ── Stochastic-derived features ───────────────────────────────────────────
df["stoch_cross_up"] = np.where((df["stoch_k"] > df["stoch_d"]) &
(df["stoch_k"].shift(1) <= df["stoch_d"].shift(1)), 1, 0)
df["stoch_cross_down"] = np.where((df["stoch_k"] < df["stoch_d"]) &
(df["stoch_k"].shift(1) >= df["stoch_d"].shift(1)), 1, 0)
df["stoch_oversold"] = np.where(df["stoch_k"] < 20, 1, 0)
df["stoch_overbought"] = np.where(df["stoch_k"] > 80, 1, 0)
# ── BB-derived features ───────────────────────────────────────────────────
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).quantile(0.20), 1, 0)
df["above_bb_upper"] = np.where(close > bb_upper, 1, 0)
df["below_bb_lower"] = np.where(close < bb_lower, 1, 0)
df["bb_pct_lag1"] = df["bb_pct"].shift(1)
df["bb_pct_lag4"] = df["bb_pct"].shift(4)
# ── Session hour (UTC) ────────────────────────────────────────────────────
df["hour_utc"] = df.index.hour if hasattr(df.index, "hour") else 0
# ── Fill NaN from indicator warm-up ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD Stochastic BB Mean-Reversion (GBM)",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.80,
"min_samples_leaf": 20,
"max_features": "sqrt",
"n_iter_no_change": 30,
"validation_fraction": 0.10,
"tol": 1e-4,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": None,
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on AUD/USD 15-min. "
"GradientBoostingClassifier with moderate depth and learning rate chosen "
"to balance bias-variance. 2:1 reward-to-risk (SL=0.5%, TP=1.0%). "
"Stochastic crossovers, BB mean-reversion, and RSI regime signals "
"form the core feature set; MACD, volatility, and candle features add "
"context. Early stopping (n_iter_no_change=30) prevents overfitting."
),
"notes": (
"Features: Bollinger Bands (20,2) width/pct, RSI(14) with lag/regime flags, "
"Stochastic(14,3) K/D with crossover detection, ATR/NATR volatility, MACD "
"histogram, short/medium SMAs, ROC(4/8/16), volatility ratio, candle body "
"ratios, and UTC session hour. No session or trend filter to allow full "
"mean-reversion opportunities across all sessions."
),
}
|
||||||||||
|
3.97
|
NZD/USD RSI-MACD Gradient Boost Risk-Adjusted
Maximize risk-adjusted return (Sharpe/Calmar) using a deep GradientBoostingClassifier with many slow-learning trees and aggressive regularis…
|
S
@silver-bull-130
|
NZDUSD | 15min | 60.9%0.0% | +18.36%+0.00% | 1.35— | 3.80%3.80% | 7320 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:35:33
# Model : Gradient Boosting
# Feature Eng. : RSI 14, MACD (12,26,9) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/NZDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── RSI 14 ──────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(alpha=1/14, min_periods=14, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/14, min_periods=14, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI derived signals
df["rsi_ob"] = np.where(df["rsi_14"] > 70, 1, 0) # overbought flag
df["rsi_os"] = np.where(df["rsi_14"] < 30, 1, 0) # oversold flag
df["rsi_mid"] = df["rsi_14"] - 50 # centred
df["rsi_slope"] = df["rsi_14"].diff(3) # momentum of RSI
df["rsi_accel"] = df["rsi_slope"].diff(2) # acceleration
# RSI regime: above/below 50
df["rsi_bull"] = np.where(df["rsi_14"] > 50, 1, -1)
# ── MACD (12, 26, 9) ────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
macd_hist = macd_line - signal_line
df["macd_line"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_hist
# MACD derived
df["macd_cross"] = np.where(macd_line > signal_line, 1, -1)
df["macd_hist_sign"] = np.where(macd_hist > 0, 1, -1)
df["macd_hist_chg"] = macd_hist.diff(1) # histogram change
df["macd_hist_accel"]= df["macd_hist_chg"].diff(1) # second derivative
df["macd_zero_cross"]= np.where(macd_line > 0, 1, -1)
# ── ATR 14 ──────────────────────────────────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr14 = tr.ewm(alpha=1/14, min_periods=14, adjust=False).mean()
df["atr_14"] = atr14
df["natr_14"] = atr14 / close # normalised ATR
df["atr_ratio"]= atr14 / atr14.rolling(50).mean() # current vs recent vol
# ── Volatility regime ───────────────────────────────────────────────────
df["vol_high"] = np.where(df["natr_14"] > df["natr_14"].rolling(100).median(), 1, 0)
# ── Price momentum ──────────────────────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_3"] = close.pct_change(3)
df["ret_8"] = close.pct_change(8)
df["ret_16"] = close.pct_change(16)
# Scaled by ATR so the model sees normalised moves
df["ret_1_atr"] = df["ret_1"] / (atr14 / close).replace(0, np.nan)
df["ret_3_atr"] = df["ret_3"] / (atr14 / close).replace(0, np.nan)
df["ret_8_atr"] = df["ret_8"] / (atr14 / close).replace(0, np.nan)
# ── EMAs & trend structure ───────────────────────────────────────────────
ema8 = close.ewm(span=8, adjust=False).mean()
ema21 = close.ewm(span=21, adjust=False).mean()
ema50 = close.ewm(span=50, adjust=False).mean()
ema100= close.ewm(span=100,adjust=False).mean()
df["ema8_21_spread"] = (ema8 - ema21) / close
df["ema21_50_spread"]= (ema21 - ema50) / close
df["ema50_100_spread"]= (ema50 - ema100) / close
df["price_vs_ema21"] = (close - ema21) / close
df["price_vs_ema50"] = (close - ema50) / close
df["trend_align"] = np.where(
(ema8 > ema21) & (ema21 > ema50), 1,
np.where((ema8 < ema21) & (ema21 < ema50), -1, 0)
)
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
bb_up = bb_mid + 2 * bb_std
bb_lo = bb_mid - 2 * bb_std
bb_bw = (bb_up - bb_lo) / bb_mid # bandwidth
bb_pct = (close - bb_lo) / (bb_up - bb_lo) # %B
df["bb_pct"] = bb_pct
df["bb_bw"] = bb_bw
df["bb_bw_ratio"] = bb_bw / bb_bw.rolling(50).mean() # squeeze detector
df["bb_upper_touch"] = np.where(close >= bb_up, 1, 0)
df["bb_lower_touch"] = np.where(close <= bb_lo, 1, 0)
# ── Stochastic %K %D (14, 3) ────────────────────────────────────────────
lo14 = low.rolling(14).min()
hi14 = high.rolling(14).max()
stoch_k = 100 * (close - lo14) / (hi14 - lo14).replace(0, np.nan)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_kd_diff"]= stoch_k - stoch_d
df["stoch_ob"] = np.where(stoch_k > 80, 1, 0)
df["stoch_os"] = np.where(stoch_k < 20, 1, 0)
# ── Candle structure ────────────────────────────────────────────────────
body = (close - open_).abs()
candle_rng= (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng # body vs full range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_rng
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_rng
df["candle_dir"] = np.where(close > open_, 1, -1)
df["candle_dir_3"] = df["candle_dir"].rolling(3).sum() # short-term bias
# ── Volume-less momentum oscillator (Williams %R 14) ───────────────────
df["williams_r"] = -100 * (hi14 - close) / (hi14 - lo14).replace(0, np.nan)
# ── RSI x MACD composite signal ─────────────────────────────────────────
df["rsi_macd_bull"] = np.where(
(df["rsi_14"] > 50) & (macd_hist > 0), 1,
np.where((df["rsi_14"] < 50) & (macd_hist < 0), -1, 0)
)
# ── Divergence proxy: price vs RSI direction (3-bar) ────────────────────
price_dir3 = np.sign(close.diff(3))
rsi_dir3 = np.sign(df["rsi_14"].diff(3))
df["rsi_div"] = np.where(price_dir3 != rsi_dir3, 1, 0)
# ── Mean-reversion signal: distance from 50-bar mean normalised by ATR ──
sma50 = close.rolling(50).mean()
df["zscore_50"] = (close - sma50) / (close.rolling(50).std(ddof=0).replace(0, np.nan))
df["mean_rev_long"] = np.where(df["zscore_50"] < -1.5, 1, 0)
df["mean_rev_short"] = np.where(df["zscore_50"] > 1.5, 1, 0)
# ── Interaction features ─────────────────────────────────────────────────
df["rsi_bb_pct"] = df["rsi_14"] * df["bb_pct"]
df["macd_hist_rsi_mid"] = df["macd_hist"] * df["rsi_mid"]
df["stoch_rsi"] = df["stoch_k"] * df["rsi_14"] / 1e4 # normalised product
# ── Lag features (avoid lookahead) ──────────────────────────────────────
for lag in [1, 2, 4, 8]:
df[f"rsi_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
df[f"ret_lag{lag}"] = df["ret_1"].shift(lag)
# ── Hour-of-day & day-of-week cyclic encoding ───────────────────────────
if hasattr(df.index, "hour"):
hour = df.index.hour
dow = df.index.dayofweek
df["hour_sin"] = np.sin(2 * np.pi * hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24)
df["dow_sin"] = np.sin(2 * np.pi * dow / 5)
df["dow_cos"] = np.cos(2 * np.pi * dow / 5)
# ── Final fill ───────────────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD RSI-MACD Gradient Boost Risk-Adjusted",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 600,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.75,
"max_features": "sqrt",
"min_samples_leaf": 20,
"min_samples_split":40,
"warm_start": False,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [21, 21],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) using a deep "
"GradientBoostingClassifier with many slow-learning trees and "
"aggressive regularisation (min_samples_leaf=20, subsample=0.75). "
"Feature set deliberately differs from prior RSI+BB+Stoch attempts "
"by adding: ATR-normalised returns, z-score mean-reversion signals, "
"RSI divergence proxy, Williams %R, candle structure ratios, cyclic "
"time encoding, and interaction/lag features to give the model richer "
"multi-timeframe context. SL=0.5%/TP=1% gives 1:2 RR aligned with "
"maximising Sharpe."
),
"notes": (
"Prior PF=1.35 / ret=+18.36% used standard RSI+MACD+BB+Stoch without "
"ATR normalisation or divergence detection. This version adds z-score "
"mean-reversion context, candle structure, and temporal encoding to "
"reduce false positives. session_filter=[21,21] is intentionally "
"narrow — set to None if you want 24h coverage. min_atr=0.0002 "
"avoids dead-market signals."
),
}
|
||||||||||
|
3.58
|
EMA Cross 9/21 + RSI14 Gradient Boost Scalper
Maximize risk-adjusted return (Sharpe/Calmar) using a GradientBoostingClassifier with EMA 9/21 crossover as the primary signal source and RS…
|
P
@pivot_kid
|
EURUSD | 15min | 43.3%50.0% | +11.79%+4.30% | 2.921.38 | 0.83%0.83% | 6712 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:37:57
# Model : Gradient Boosting
# Feature Eng. : EMA (9,21), RSI 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# --- EMA 9 and EMA 21 (required) ---
ema_9 = close.ewm(span=9, adjust=False).mean()
ema_21 = close.ewm(span=21, adjust=False).mean()
df["ema_9"] = ema_9
df["ema_21"] = ema_21
df["dm_ema_9"] = (close - ema_9) / ema_9
df["dm_ema_21"] = (close - ema_21) / ema_21
# EMA crossover signal: positive when fast > slow
df["ema_cross"] = ema_9 - ema_21
# Crossover direction change (sign flip)
df["ema_cross_signal"] = np.sign(df["ema_cross"])
df["ema_cross_prev"] = df["ema_cross_signal"].shift(1)
df["ema_cross_flip"] = (df["ema_cross_signal"] != df["ema_cross_prev"]).astype(float)
# --- RSI 14 (required) ---
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=13, adjust=False).mean()
avg_loss = loss.ewm(com=13, adjust=False).mean()
rs = avg_gain / (avg_loss + 1e-10)
rsi_14 = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi_14
# RSI normalised to [-1, 1] range
df["rsi_norm"] = (rsi_14 - 50) / 50
# RSI overbought/oversold flags
df["rsi_ob"] = np.where(rsi_14 > 70, 1.0, 0.0)
df["rsi_os"] = np.where(rsi_14 < 30, 1.0, 0.0)
# --- Additional momentum and volatility features ---
# EMA 50 for trend context
ema_50 = close.ewm(span=50, adjust=False).mean()
df["ema_50"] = ema_50
df["dm_ema_50"] = (close - ema_50) / ema_50
# Price momentum: rate of change over multiple horizons
df["roc_4"] = close.pct_change(4)
df["roc_8"] = close.pct_change(8)
df["roc_16"] = close.pct_change(16)
# ATR (Average True Range) for volatility
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(span=14, adjust=False).mean()
df["atr_14"] = atr_14
# Normalised ATR
df["natr_14"] = atr_14 / close
# Bollinger Bands (20-period, 2 std)
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_upper = bb_mid + 2 * bb_std
bb_lower = bb_mid - 2 * bb_std
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower + 1e-10)
df["bb_width"] = (bb_upper - bb_lower) / (bb_mid + 1e-10)
# BB squeeze: narrow bands signal potential breakout
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).mean(), 1.0, 0.0)
# MACD-like: difference between two EMAs
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line / (close + 1e-10)
df["macd_signal"] = macd_signal / (close + 1e-10)
df["macd_hist"] = (macd_line - macd_signal) / (close + 1e-10)
df["macd_cross"] = np.sign(macd_line - macd_signal)
# Stochastic oscillator (14-period)
lowest_low = low.rolling(14).min()
highest_high = high.rolling(14).max()
stoch_k = 100 * (close - lowest_low) / (highest_high - lowest_low + 1e-10)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_diff"] = stoch_k - stoch_d
# Volume of price movement (candle body and shadows)
df["body"] = (close - open_).abs() / (atr_14 + 1e-10)
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / (atr_14 + 1e-10)
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / (atr_14 + 1e-10)
df["candle_dir"] = np.sign(close - open_)
# Rolling volatility (realised vol over 20 bars)
log_ret = np.log(close / close.shift(1))
df["realvol_20"] = log_ret.rolling(20).std()
# Close relative to recent high/low channel (20-bar)
roll_high_20 = high.rolling(20).max()
roll_low_20 = low.rolling(20).min()
df["chan_pct_20"] = (close - roll_low_20) / (roll_high_20 - roll_low_20 + 1e-10)
# Lagged RSI and EMA cross for temporal context
df["rsi_14_lag1"] = rsi_14.shift(1)
df["rsi_14_lag2"] = rsi_14.shift(2)
df["ema_cross_lag1"] = df["ema_cross"].shift(1)
df["ema_cross_lag2"] = df["ema_cross"].shift(2)
df["macd_hist_lag1"] = df["macd_hist"].shift(1)
# RSI momentum: change in RSI
df["rsi_delta_1"] = rsi_14.diff(1)
df["rsi_delta_4"] = rsi_14.diff(4)
# EMA9 slope (normalised)
df["ema9_slope"] = ema_9.diff(3) / (ema_9.shift(3) + 1e-10)
df["ema21_slope"] = ema_21.diff(3) / (ema_21.shift(3) + 1e-10)
# Interaction: RSI * EMA cross direction
df["rsi_ema_cross_interact"] = df["rsi_norm"] * df["ema_cross_signal"]
# Fill NaN from indicator warm-up
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EMA Cross 9/21 + RSI14 Gradient Boost Scalper",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.8,
"min_samples_leaf": 20,
"min_samples_split": 40,
"max_features": "sqrt",
"n_iter_no_change": 30,
"validation_fraction": 0.1,
"tol": 1e-4,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 17],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) using a GradientBoostingClassifier "
"with EMA 9/21 crossover as the primary signal source and RSI 14 as confirmation. "
"Deep feature set includes MACD, Bollinger Bands, Stochastic, ATR normalisation, "
"candle structure, lagged features and RSI/EMA interaction terms. "
"Gradient boosting chosen for its ability to capture non-linear interactions between "
"trend, momentum and volatility features without overfitting when regularised via "
"subsample, max_features, and early stopping. Threshold 0.56 filters marginal signals. "
"Session filter [7,17] focuses on London/NY overlap for highest EUR/USD liquidity. "
"SL 0.5% / TP 1.0% gives 1:2 risk-reward aligned with scalper momentum targets. "
"Reverse on opposite signal to stay in sync with fast EMA crossover momentum."
),
"notes": (
"EMA 9/21 cross captures short-term momentum shifts typical of active EUR/USD sessions. "
"RSI 14 filters entries in extreme overbought/oversold conditions. "
"NATR min_atr filter removes flat/low-vol periods. "
"Trend filter (SMA 50) ensures longs only above and shorts only below the medium-term trend. "
"n_iter_no_change=30 provides early stopping to prevent overfitting on the training split. "
"400 estimators with depth 4 and lr 0.04 balance bias-variance tradeoff for intraday data."
),
}
|
||||||||||
|
3.47
|
BB Squeeze Breakout + ATR Filter (GBM)
Maximize risk-adjusted return (Sharpe/Calmar). GradientBoosting chosen for its strong performance on tabular data with structured non-linear…
|
D
@delta_one
|
EURUSD | 15min | 59.9%58.3% | +1.64%+12.51% | 1.071.38 | 2.90%2.90% | 30272 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 03:01:00
# Model : Gradient Boosting
# Feature Eng. : BB (20,2.0), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# Bollinger Bands Squeeze Breakout — GradientBoosting Strategy
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_sigma = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_sigma
bb_lower = bb_mid - bb_std * bb_sigma
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
# Required derived features
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── ATR (14) ─────────────────────────────────────────────────────────────
atr_period = 14
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr = tr.ewm(span=atr_period, min_periods=atr_period, adjust=False).mean()
df["atr"] = atr
df["natr"] = atr / close
# ── Squeeze Detection ────────────────────────────────────────────────────
# Squeeze = BB width is near its lowest over recent N bars (compression)
squeeze_window = 20
bb_width_min = df["bb_width"].rolling(squeeze_window).min()
bb_width_max = df["bb_width"].rolling(squeeze_window).max()
# Normalised squeeze score: 0 = fully squeezed, 1 = fully expanded
df["squeeze_score"] = (df["bb_width"] - bb_width_min) / (bb_width_max - bb_width_min + 1e-10)
# Squeeze flag: 1 if currently squeezed (bottom 20th percentile of width)
df["in_squeeze"] = np.where(df["squeeze_score"] < 0.20, 1, 0)
# Breakout direction: momentum after squeeze
df["squeeze_breakout_up"] = np.where((df["in_squeeze"].shift(1) == 1) & (close > bb_upper), 1, 0)
df["squeeze_breakout_down"] = np.where((df["in_squeeze"].shift(1) == 1) & (close < bb_lower), 1, 0)
# ── Momentum / Rate-of-Change ────────────────────────────────────────────
df["roc_5"] = close.pct_change(5)
df["roc_10"] = close.pct_change(10)
df["roc_20"] = close.pct_change(20)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(span=rsi_period, min_periods=rsi_period, adjust=False).mean()
avg_loss = loss.ewm(span=rsi_period, min_periods=rsi_period, adjust=False).mean()
rs = avg_gain / (avg_loss + 1e-10)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI distance from 50 (overbought/oversold)
df["rsi_dist50"] = df["rsi_14"] - 50.0
# ── MACD (12, 26, 9) ────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line
df["macd_signal"] = macd_signal
df["macd_hist"] = macd_line - macd_signal
# Normalise MACD by ATR to make scale-invariant
df["macd_hist_norm"] = df["macd_hist"] / (atr + 1e-10)
# ── Volume / Candle Body Features ────────────────────────────────────────
body = (close - open_).abs()
candle_range = high - low
df["body_ratio"] = body / (candle_range + 1e-10) # 0=doji, 1=full body
df["close_pos"] = (close - low) / (candle_range + 1e-10) # position within bar
# ── Trend / SMA Features ─────────────────────────────────────────────────
df["sma_20"] = bb_mid # reuse already computed
df["sma_50"] = close.rolling(50).mean()
df["sma_100"] = close.rolling(100).mean()
df["price_vs_sma20"] = (close - df["sma_20"]) / (atr + 1e-10)
df["price_vs_sma50"] = (close - df["sma_50"]) / (atr + 1e-10)
df["price_vs_sma100"] = (close - df["sma_100"]) / (atr + 1e-10)
# SMA cross: 20 vs 50
df["sma20_vs_sma50"] = (df["sma_20"] - df["sma_50"]) / (atr + 1e-10)
# ── BB Width Rate-of-Change (squeeze momentum) ───────────────────────────
df["bb_width_roc3"] = df["bb_width"].pct_change(3)
df["bb_width_roc8"] = df["bb_width"].pct_change(8)
# ── Lagged BB features ───────────────────────────────────────────────────
df["bb_pct_lag1"] = df["bb_pct"].shift(1)
df["bb_pct_lag2"] = df["bb_pct"].shift(2)
df["bb_pct_lag4"] = df["bb_pct"].shift(4)
df["bb_width_lag1"] = df["bb_width"].shift(1)
df["bb_width_lag4"] = df["bb_width"].shift(4)
# ── Volatility Regime ────────────────────────────────────────────────────
natr_ma = df["natr"].rolling(40).mean()
df["vol_regime"] = np.where(df["natr"] > natr_ma, 1, 0) # 1=high vol, 0=low vol
# ── Price Distance from Bands (ATR-normalised) ───────────────────────────
df["dist_upper"] = (bb_upper - close) / (atr + 1e-10)
df["dist_lower"] = (close - bb_lower) / (atr + 1e-10)
# ── Hour / Session (cyclical) ────────────────────────────────────────────
if hasattr(df.index, "hour"):
hour = df.index.hour
df["hour_sin"] = np.sin(2 * np.pi * hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24)
# ── Fill warm-up NaN ────────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "BB Squeeze Breakout + ATR Filter (GBM)",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"max_features": "sqrt",
"min_samples_leaf": 20,
"min_samples_split": 40,
"n_iter_no_change": 30,
"validation_fraction": 0.12,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 20],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar). "
"GradientBoosting chosen for its strong performance on tabular data with "
"structured non-linear interactions. Shallow trees (depth=4) with high "
"n_estimators and low learning_rate reduce overfitting. subsample=0.75 "
"adds stochasticity. Early stopping via n_iter_no_change avoids "
"over-training on the 15-min EURUSD regime. SL=0.5%/TP=1.0% gives 2:1 "
"reward-risk, consistent with a squeeze-breakout edge. session_filter "
"[6,20] UTC captures London+NY sessions where BB squeezes resolve cleanly. "
"min_atr guards against ultra-low-volatility false breakouts."
),
"notes": (
"Features centre on BB squeeze mechanics: bb_width, squeeze_score, "
"in_squeeze flag, breakout_up/down, width RoC, and lagged bb_pct. "
"Complemented by RSI, MACD histogram (ATR-normalised), SMA trend "
"distances, candle body ratio, and cyclical hour encoding. "
"target_horizon=4 bars (1 hour) aligns with the typical squeeze "
"resolution time on 15-min EURUSD. Signal threshold 0.55 filters "
"marginal signals while preserving sufficient trade frequency."
),
}
|
||||||||||
|
3.30
|
AUD/USD EMA Cross (9/21) + RSI14 XGBoost Scalper
Maximise risk-adjusted return on AUD/USD 15-min bars. XGBoost chosen for its ability to capture non-linear interactions between the EMA-cros…
|
E
@echo-quanta-127
|
AUDUSD | 15min | 63.6%64.7% | +13.00%+16.44% | 1.191.47 | 4.73%4.73% | 1063133 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:40:19
# Model : XGBoost
# Feature Eng. : EMA (9,21), RSI 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── EMA 9 and EMA 21 ──────────────────────────────────────────────────
ema_9 = close.ewm(span=9, adjust=False).mean()
ema_21 = close.ewm(span=21, adjust=False).mean()
df["ema_9"] = ema_9
df["ema_21"] = ema_21
df["dm_ema_9"] = (close - ema_9) / ema_9
df["dm_ema_21"] = (close - ema_21) / ema_21
# EMA crossover signal: positive when fast > slow
df["ema_cross"] = ema_9 - ema_21
df["ema_cross_prev"] = df["ema_cross"].shift(1)
# Binary: did a cross just occur?
df["ema_cross_up"] = np.where((df["ema_cross"] > 0) & (df["ema_cross_prev"] <= 0), 1, 0)
df["ema_cross_down"] = np.where((df["ema_cross"] < 0) & (df["ema_cross_prev"] >= 0), 1, 0)
# Trend direction encoded as -1 / 1
df["ema_trend"] = np.where(ema_9 > ema_21, 1, -1)
# ── RSI 14 ────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=13, adjust=False).mean()
avg_loss = loss.ewm(com=13, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi_14 = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi_14
# RSI regime flags
df["rsi_oversold"] = np.where(rsi_14 < 30, 1, 0)
df["rsi_overbought"] = np.where(rsi_14 > 70, 1, 0)
df["rsi_mid"] = rsi_14 - 50 # centred
# RSI momentum (1-bar change in RSI)
df["rsi_delta"] = rsi_14.diff(1)
df["rsi_delta2"] = rsi_14.diff(3)
# ── Additional momentum / volatility features ─────────────────────────
# ATR-like normalised true range
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(span=14, adjust=False).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / close # normalised ATR
# Rate-of-change over various horizons
for n in [4, 8, 16]:
df[f"roc_{n}"] = close.pct_change(n)
# Bollinger Band width and %B (using 20-period SMA)
sma_20 = close.rolling(20).mean()
std_20 = close.rolling(20).std()
bb_upper = sma_20 + 2 * std_20
bb_lower = sma_20 - 2 * std_20
df["bb_width"] = (bb_upper - bb_lower) / sma_20
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower).replace(0, np.nan)
# Candle body and wick features
df["body"] = (close - open_) / close
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / close
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / close
# Volume-normalised momentum proxy: price range relative to ATR
df["range_vs_atr"] = (high - low) / atr_14.replace(0, np.nan)
# Lagged EMA cross signal
df["ema_cross_lag1"] = df["ema_cross"].shift(1)
df["ema_cross_lag2"] = df["ema_cross"].shift(2)
# Combined signal: RSI and EMA cross alignment
df["rsi_ema_bull"] = np.where((rsi_14 > 50) & (ema_9 > ema_21), 1, 0)
df["rsi_ema_bear"] = np.where((rsi_14 < 50) & (ema_9 < ema_21), 1, 0)
# Hour-of-day (cyclical encoding) — no lookahead
hour = df.index.hour
df["hour_sin"] = np.sin(2 * np.pi * hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24)
# Day-of-week (cyclical encoding)
dow = df.index.dayofweek
df["dow_sin"] = np.sin(2 * np.pi * dow / 5)
df["dow_cos"] = np.cos(2 * np.pi * dow / 5)
# Fill NaN from warm-up periods
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD EMA Cross (9/21) + RSI14 XGBoost Scalper",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.8,
"colsample_bytree": 0.75,
"min_child_weight": 3,
"gamma": 0.1,
"reg_alpha": 0.05,
"reg_lambda": 1.5,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.54,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return on AUD/USD 15-min bars. "
"XGBoost chosen for its ability to capture non-linear interactions between "
"the EMA-cross regime, RSI momentum, volatility (NATR/BB width), and time-of-day. "
"Shallow trees (max_depth=4) with strong regularisation (reg_lambda=1.5, gamma=0.1) "
"reduce overfitting on the limited 1-year window. "
"2:1 R:R (SL=0.5%, TP=1.0%) improves Sharpe; reverse on opposite signal captures "
"trend momentum without missing transitions."
),
"notes": (
"Features: EMA-9/21 cross and distances, RSI-14 with regime flags and delta, "
"ATR-14, NATR, Bollinger Band width/%B, 4/8/16-bar ROC, candle anatomy, "
"time cyclical encodings. Threshold 0.54 filters marginal signals to raise precision. "
"No session filter applied — AUD/USD has meaningful moves across Asian and London sessions."
),
}
|
||||||||||
|
2.88
|
USD/CAD Stoch+BB+RSI Mean-Reversion (XGBoost)
Maximize risk-adjusted return (Sharpe/Calmar) by combining Stochastic (14,3), Bollinger Bands (20,2) and RSI(14) mean-reversion signals with…
|
V
@vega-puma-338
|
USDCAD | 15min | 58.2%64.8% | +2.45%+5.70% | 1.151.29 | 1.65%1.65% | 30954 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:58:30
# Model : XGBoost
# Feature Eng. : BB (20,2.0), RSI 14, Stochastic (14,3) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/USDCAD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_std_s = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_std_s
bb_lower = bb_mid - bb_std * bb_std_s
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
avg_loss = loss.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi"] = 100 - (100 / (1 + rs))
# ── Stochastic Oscillator (K=14, D=3) ────────────────────────────────────
stoch_k_period = 14
stoch_d_period = 3
lowest_low = low.rolling(stoch_k_period).min()
highest_high = high.rolling(stoch_k_period).max()
stoch_k_raw = 100 * (close - lowest_low) / (highest_high - lowest_low).replace(0, np.nan)
df["stoch_k"] = stoch_k_raw
df["stoch_d"] = stoch_k_raw.rolling(stoch_d_period).mean()
# ── Derived Stochastic features ──────────────────────────────────────────
df["stoch_kd_diff"] = df["stoch_k"] - df["stoch_d"] # K-D divergence
df["stoch_k_prev"] = df["stoch_k"].shift(1)
df["stoch_d_prev"] = df["stoch_d"].shift(1)
# Bullish crossover: K crosses above D
df["stoch_cross_up"] = np.where(
(df["stoch_k"] > df["stoch_d"]) & (df["stoch_k_prev"] <= df["stoch_d_prev"]), 1.0, 0.0
)
# Bearish crossover: K crosses below D
df["stoch_cross_dn"] = np.where(
(df["stoch_k"] < df["stoch_d"]) & (df["stoch_k_prev"] >= df["stoch_d_prev"]), 1.0, 0.0
)
# ── RSI-derived features ─────────────────────────────────────────────────
df["rsi_prev"] = df["rsi"].shift(1)
df["rsi_slope"] = df["rsi"] - df["rsi_prev"]
df["rsi_ob"] = np.where(df["rsi"] >= 70, 1.0, 0.0) # overbought flag
df["rsi_os"] = np.where(df["rsi"] <= 30, 1.0, 0.0) # oversold flag
# ── BB-derived features ──────────────────────────────────────────────────
df["bb_pct_prev"] = df["bb_pct"].shift(1)
df["bb_pct_slope"] = df["bb_pct"] - df["bb_pct_prev"]
df["price_vs_mid"] = (close - bb_mid) / bb_mid # normalised distance from mid
# Squeeze: narrow bands relative to recent history
df["bb_squeeze"] = np.where(
df["bb_width"] < df["bb_width"].rolling(50).mean(), 1.0, 0.0
)
# ── ATR (14) — volatility context ────────────────────────────────────────
atr_period = 14
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
df["atr"] = tr.ewm(com=atr_period - 1, min_periods=atr_period).mean()
df["natr"] = df["atr"] / close
# ── Momentum / price change features ─────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_4"] = close.pct_change(4)
df["ret_16"] = close.pct_change(16)
# ── Trend context: SMA 50 & 200 ──────────────────────────────────────────
df["sma_50"] = close.rolling(50).mean()
df["sma_200"] = close.rolling(200).mean()
df["price_vs_50"] = (close - df["sma_50"]) / df["sma_50"]
df["price_vs_200"] = (close - df["sma_200"]) / df["sma_200"]
df["trend_up"] = np.where(df["sma_50"] > df["sma_200"], 1.0, 0.0)
# ── Volume proxy: candle body / range ratio ───────────────────────────────
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = (close - open_).abs() / candle_range
df["bull_bar"] = np.where(close > open_, 1.0, 0.0)
# ── MACD-like momentum: EMA12 - EMA26 ────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
df["macd"] = ema12 - ema26
df["macd_signal"] = df["macd"].ewm(span=9, adjust=False).mean()
df["macd_hist"] = df["macd"] - df["macd_signal"]
# ── Rolling volatility (std of returns) ──────────────────────────────────
df["vol_10"] = df["ret_1"].rolling(10).std()
# ── Hour-of-day and day-of-week (cyclical) ────────────────────────────────
if hasattr(df.index, "hour"):
df["hour_sin"] = np.sin(2 * np.pi * df.index.hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * df.index.hour / 24)
df["dow_sin"] = np.sin(2 * np.pi * df.index.dayofweek / 5)
df["dow_cos"] = np.cos(2 * np.pi * df.index.dayofweek / 5)
# ── Combined signal: RSI + Stoch confluence ───────────────────────────────
df["conf_bull"] = np.where((df["rsi"] < 50) & (df["stoch_k"] < 50), 1.0, 0.0)
df["conf_bear"] = np.where((df["rsi"] > 50) & (df["stoch_k"] > 50), 1.0, 0.0)
# ── Fill NaN from warm-up periods ────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "USD/CAD Stoch+BB+RSI Mean-Reversion (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.80,
"colsample_bytree": 0.75,
"min_child_weight": 3,
"gamma": 0.10,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 20],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) by combining "
"Stochastic (14,3), Bollinger Bands (20,2) and RSI(14) mean-reversion "
"signals with XGBoost. Regularisation (reg_alpha, reg_lambda, gamma, "
"min_child_weight) and column/row subsampling control overfitting. "
"A 0.55 confidence threshold filters low-conviction trades. "
"Session filter [7,20] UTC focuses on liquid London+NY overlap hours. "
"SL=0.5% / TP=1.0% gives a 1:2 risk-reward per trade."
),
"notes": (
"target_horizon=4 bars (1 hour on 15-min data) suits intraday mean-reversion. "
"Cyclical time features (hour_sin/cos, dow_sin/cos) capture intraday seasonality. "
"MACD histogram and rolling volatility provide trend/momentum context alongside "
"the core BB/RSI/Stoch mean-reversion suite. "
"reverse on_opposite allows the model to flip positions when conviction is high "
"in the opposing direction without waiting for flat cooldown."
),
}
|
||||||||||
|
2.57
|
EUR/USD Stoch+BB+RSI Gradient Boosting Mean-Rev
Maximize risk-adjusted return (Sharpe/Calmar) on EUR/USD 15-min data. GradientBoostingClassifier chosen for strong out-of-bag regularisation…
|
E
@echo-quanta-127
|
EURUSD | 15min | 61.2%52.3% | +1.02%+8.97% | 1.061.42 | 2.59%2.59% | 21444 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:33:17
# Model : Gradient Boosting
# Feature Eng. : BB (20,2.0), RSI 14, Stochastic (14,3) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_std_v = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_std_v
bb_lower = bb_mid - bb_std * bb_std_v
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── RSI 14 ───────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
avg_loss = loss.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi"] = 100 - (100 / (1 + rs))
# ── Stochastic Oscillator (K=14, D=3) ────────────────────────────────────
stoch_k_period = 14
stoch_d_period = 3
lowest_low = low.rolling(stoch_k_period).min()
highest_high = high.rolling(stoch_k_period).max()
range_hl = (highest_high - lowest_low).replace(0, np.nan)
df["stoch_k"] = 100 * (close - lowest_low) / range_hl
df["stoch_d"] = df["stoch_k"].rolling(stoch_d_period).mean()
df["stoch_kd_diff"] = df["stoch_k"] - df["stoch_d"]
# ── ATR (14) ──────────────────────────────────────────────────────────────
atr_period = 14
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
df["atr"] = tr.ewm(com=atr_period - 1, min_periods=atr_period).mean()
df["natr"] = df["atr"] / close
# ── SMA filters ──────────────────────────────────────────────────────────
df["sma_20"] = close.rolling(20).mean()
df["sma_50"] = close.rolling(50).mean()
df["sma_200"] = close.rolling(200).mean()
df["price_vs_sma50"] = close / df["sma_50"] - 1
df["price_vs_sma200"] = close / df["sma_200"] - 1
# ── EMA cross ────────────────────────────────────────────────────────────
ema_fast = close.ewm(span=8, adjust=False).mean()
ema_slow = close.ewm(span=21, adjust=False).mean()
df["ema_cross"] = ema_fast - ema_slow
# ── MACD ─────────────────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_sig"] = macd_signal
df["macd_hist"] = macd_line - macd_signal
# ── Momentum / Rate-of-change ────────────────────────────────────────────
df["roc_4"] = close.pct_change(4)
df["roc_8"] = close.pct_change(8)
df["roc_16"] = close.pct_change(16)
# ── Candle features ───────────────────────────────────────────────────────
df["candle_body"] = (close - open_) / close
df["candle_range"] = (high - low) / close
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / close
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / close
# ── Volatility regime ─────────────────────────────────────────────────────
df["vol_ratio"] = df["atr"] / df["atr"].rolling(50).mean()
# ── RSI regime bins (np.where instead of pd.cut) ─────────────────────────
df["rsi_oversold"] = np.where(df["rsi"] < 30, 1, 0)
df["rsi_overbought"]= np.where(df["rsi"] > 70, 1, 0)
df["rsi_mid"] = np.where((df["rsi"] >= 40) & (df["rsi"] <= 60), 1, 0)
# ── Stochastic regime bins ────────────────────────────────────────────────
df["stoch_oversold"] = np.where(df["stoch_k"] < 20, 1, 0)
df["stoch_overbought"] = np.where(df["stoch_k"] > 80, 1, 0)
# ── BB regime bins ────────────────────────────────────────────────────────
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).quantile(0.20), 1, 0)
df["bb_expansion"] = np.where(df["bb_width"] > df["bb_width"].rolling(50).quantile(0.80), 1, 0)
df["price_below_bb_lower"] = np.where(close < bb_lower, 1, 0)
df["price_above_bb_upper"] = np.where(close > bb_upper, 1, 0)
# ── Volume proxy — bar range z-score ──────────────────────────────────────
range_series = high - low
range_mean = range_series.rolling(20).mean()
range_std = range_series.rolling(20).std(ddof=0)
df["range_zscore"] = (range_series - range_mean) / range_std.replace(0, np.nan)
# ── Lagged features ───────────────────────────────────────────────────────
for lag in [1, 2, 3, 4]:
df[f"rsi_lag{lag}"] = df["rsi"].shift(lag)
df[f"stoch_k_lag{lag}"] = df["stoch_k"].shift(lag)
df[f"bb_pct_lag{lag}"] = df["bb_pct"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
# ── Interaction features ──────────────────────────────────────────────────
df["rsi_x_bb_pct"] = df["rsi"] * df["bb_pct"]
df["stoch_x_bb_pct"] = df["stoch_k"] * df["bb_pct"]
df["macd_x_ema_cross"] = df["macd_hist"] * df["ema_cross"]
df["rsi_x_stoch_kd"] = df["rsi"] * df["stoch_kd_diff"]
# ── Fill NaN from warm-up ─────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD Stoch+BB+RSI Gradient Boosting Mean-Rev",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"min_samples_leaf": 20,
"max_features": "sqrt",
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 18],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on EUR/USD 15-min data. "
"GradientBoostingClassifier chosen for strong out-of-bag regularisation "
"via subsample=0.75 and early stopping (n_iter_no_change=30). "
"max_depth=4 limits overfitting on mean-reversion regime. "
"learning_rate=0.04 with 400 trees balances bias-variance. "
"Signal threshold 0.56 filters low-confidence signals for better precision. "
"Session filter 06-18 UTC targets London+NY overlap with highest liquidity. "
"SL=0.5%, TP=1.0% gives 1:2 R:R aligned with mean-reversion edge. "
"Target horizon=4 bars (1 hour) captures short-term mean-reversion cycles."
),
"notes": (
"Features: Stochastic(14,3), BB(20,2), RSI(14) as primary signals. "
"Supplemented by MACD, EMA cross, ATR volatility filter, candle body/shadow, "
"range z-score, lagged versions of key oscillators, and interaction terms. "
"Regime bins (oversold/overbought/squeeze/expansion) add non-linear context. "
"min_atr=0.0002 avoids trading during dead/illiquid periods."
),
}
|
||||||||||