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 | ||
|---|---|---|---|---|---|---|---|---|---|---|
|
🥇
|
EMA Cross 50/200 + ATR Momentum (XGBoost)
Maximize risk-adjusted return (Sharpe/Calmar) on EUR/USD 15-min data. EMA 50/200 cross provides the primary trend regime filter. ATR 14 gate…
|
D
@delta-atlas-858
|
EURUSD | 15min | 48.5%54.5% | +8.52%+15.51% | 2.413.04 | 0.83%0.83% | 6811 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-05 10:27:10
# Model : XGBoost
# Feature Eng. : EMA (50,200), 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):
# ── EMA 50 and EMA 200 ──────────────────────────────────────────────────
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
# EMA cross signal: positive when fast > slow
df["ema_cross"] = ema_50 - ema_200
df["ema_cross_norm"] = df["ema_cross"] / ema_200
# Cross direction change (momentum of the spread)
df["ema_cross_delta"] = df["ema_cross"].diff(1)
df["ema_cross_accel"] = df["ema_cross_delta"].diff(1)
# ── 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 = tr.ewm(span=14, adjust=False).mean()
df["atr"] = atr
df["natr"] = atr / close
# ── Price momentum features ─────────────────────────────────────────────
for lag in [1, 2, 4, 8, 16]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Volatility regime ───────────────────────────────────────────────────
df["atr_ratio"] = atr / atr.rolling(50).mean() # ATR vs its own MA
df["natr_ma20"] = df["natr"].rolling(20).mean()
# ── RSI 14 ──────────────────────────────────────────────────────────────
delta = close.diff(1)
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_g = gain.ewm(span=14, adjust=False).mean()
avg_l = loss.ewm(span=14, adjust=False).mean()
rs = avg_g / avg_l.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
df["rsi_delta"] = df["rsi_14"].diff(1)
# ── MACD (12/26/9) ──────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd = ema_12 - ema_26
signal = macd.ewm(span=9, adjust=False).mean()
df["macd"] = macd
df["macd_signal"] = signal
df["macd_hist"] = macd - signal
df["macd_hist_delta"] = df["macd_hist"].diff(1)
# ── Bollinger Bands (20, 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
df["bb_pos"] = (close - bb_lo) / (bb_up - bb_lo).replace(0, np.nan)
df["bb_width"] = (bb_up - bb_lo) / bb_mid
# ── Stochastic %K / %D (14, 3) ──────────────────────────────────────────
low14 = low.rolling(14).min()
high14 = high.rolling(14).max()
stoch_k = 100 * (close - low14) / (high14 - low14).replace(0, np.nan)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_kd"] = stoch_k - stoch_d
# ── Volume / body / wick features ───────────────────────────────────────
body = (close - open_).abs()
candle = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle
df["bull_candle"] = np.where(close > open_, 1, 0)
# ── Rolling z-score of close vs SMA 50 ──────────────────────────────────
sma_50 = close.rolling(50).mean()
sma_50_std = close.rolling(50).std()
df["zscore_50"] = (close - sma_50) / sma_50_std.replace(0, np.nan)
# ── High/Low breakout flags ──────────────────────────────────────────────
df["high_20_break"] = np.where(close > high.rolling(20).max().shift(1), 1, 0)
df["low_20_break"] = np.where(close < low.rolling(20).min().shift(1), 1, 0)
# ── Time-of-day features (cyclical encoding) ─────────────────────────────
if hasattr(df.index, 'hour'):
hour = df.index.hour + df.index.minute / 60.0
df["hour_sin"] = np.sin(2 * np.pi * hour / 24.0)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24.0)
# ── Fill NaN from indicator warm-up ─────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EMA Cross 50/200 + ATR Momentum (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.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": [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. "
"EMA 50/200 cross provides the primary trend regime filter. "
"ATR 14 gates entries by volatility (min_atr avoids dead-market noise). "
"XGBoost chosen for its ability to capture non-linear feature interactions. "
"Conservative depth=4 and regularisation (alpha/lambda) prevent overfitting "
"on the relatively short 1-year window. 2:1 reward/risk (SL=0.5%, TP=1.0%) "
"ensures positive expectancy even at modest hit-rates. Session filter 06-20 UTC "
"keeps the strategy in liquid London/NY hours only."
),
"notes": (
"Feature set combines trend (EMA cross, z-score), momentum (RSI, MACD, returns), "
"volatility (ATR ratio, BB width), and price structure (body/wick ratios, "
"stochastic). Cyclical hour encoding captures intraday seasonality without "
"introducing lookahead. bfill().ffill() handles EMA warm-up NaNs gracefully."
),
}
|
||||||||||
|
🥈
|
EUR/USD EMA Cross + ATR Momentum (XGBoost)
Maximize risk-adjusted return (Sharpe) by combining EMA crossover trend regime with ATR-normalised volatility, RSI, MACD, and Bollinger feat…
|
R
@rapid-shark-854
|
EURUSD | 15min | 44.1%64.3% | +4.76%+23.55% | 1.594.96 | 2.55%2.55% | 6814 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:19:50
# Model : XGBoost
# Feature Eng. : EMA (50,200), 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-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 50 / 200 and distance features ──────────────────────────────
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
# EMA cross signal: +1 when ema_50 > ema_200, -1 otherwise
df["ema_cross"] = np.where(ema_50 > ema_200, 1.0, -1.0)
# Spread between the two EMAs, normalised by price
df["ema_spread"] = (ema_50 - ema_200) / close
# Rate of change of ema_spread (momentum of the cross)
df["ema_spread_roc"] = df["ema_spread"].diff(4)
# ── ATR 14 & NATR ───────────────────────────────────────────────────
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=14, adjust=False).mean()
natr = atr / close
df["atr"] = atr
df["natr"] = natr
# ── RSI 14 ───────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(span=14, adjust=False).mean()
avg_loss = loss.ewm(span=14, adjust=False).mean()
rs = avg_gain / (avg_loss + 1e-12)
rsi = 100.0 - (100.0 / (1.0 + rs))
df["rsi_14"] = rsi
# Normalised RSI centred around 0
df["rsi_norm"] = (rsi - 50.0) / 50.0
# ── MACD (12/26/9) ───────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd = ema_12 - ema_26
signal = macd.ewm(span=9, adjust=False).mean()
df["macd"] = macd / close
df["macd_signal"] = signal / close
df["macd_hist"] = (macd - signal) / close
# ── Bollinger Bands (20, 2σ) ─────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
bb_up = bb_mid + 2.0 * bb_std
bb_lo = bb_mid - 2.0 * bb_std
df["bb_width"] = (bb_up - bb_lo) / (bb_mid + 1e-12)
df["bb_pct"] = (close - bb_lo) / (bb_up - bb_lo + 1e-12)
# ── Price momentum (log-returns at multiple horizons) ────────────────
for lag in [1, 4, 8, 16]:
df[f"logret_{lag}"] = np.log(close / close.shift(lag))
# ── Volume-less volatility proxy: high-low range / ATR ───────────────
df["hl_range_norm"] = (high - low) / (atr + 1e-12)
# ── Candle body and shadow ratios ────────────────────────────────────
body = (close - open_).abs()
range_ = (high - low + 1e-12)
df["body_ratio"] = body / range_
df["upper_shadow_ratio"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / range_
df["lower_shadow_ratio"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / range_
# ── Trend strength: close relative to recent N-bar high/low ──────────
for window in [20, 50]:
roll_hi = high.rolling(window).max()
roll_lo = low.rolling(window).min()
denom = (roll_hi - roll_lo + 1e-12)
df[f"close_rank_{window}"] = (close - roll_lo) / denom
# ── EMA 50 slope (rate of change) ────────────────────────────────────
df["ema_50_slope"] = ema_50.diff(4) / (close + 1e-12)
df["ema_200_slope"] = ema_200.diff(8) / (close + 1e-12)
# ── Hour-of-day and day-of-week as cyclic features ───────────────────
if hasattr(df.index, "hour"):
hour = df.index.hour
dow = df.index.dayofweek
df["hour_sin"] = np.sin(2.0 * np.pi * hour / 24.0)
df["hour_cos"] = np.cos(2.0 * np.pi * hour / 24.0)
df["dow_sin"] = np.sin(2.0 * np.pi * dow / 5.0)
df["dow_cos"] = np.cos(2.0 * np.pi * dow / 5.0)
# ── Fill NaNs from indicator warm-up ────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD EMA Cross + ATR Momentum (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.75,
"min_child_weight": 3,
"gamma": 0.1,
"reg_alpha": 0.05,
"reg_lambda": 1.5,
"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": [6, 20],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe) by combining EMA crossover "
"trend regime with ATR-normalised volatility, RSI, MACD, and Bollinger "
"features. XGBoost hyperparameters tuned for bias-variance balance: "
"moderate depth (4), aggressive shrinkage (lr=0.04), column/row "
"subsampling, and L1/L2 regularisation. SL=0.5% / TP=1.0% targets a "
"2:1 reward-risk ratio. Session filter [6,20] UTC focuses on the "
"liquid London/NY overlap. min_atr filters dead markets."
),
"notes": (
"EMA 50/200 cross provides the macro trend regime; distance features "
"capture how far price has stretched from trend. ATR/NATR quantifies "
"volatility regime. RSI, MACD histogram, and BB %b add mean-reversion "
"and momentum context. Candle body ratios encode micro-structure. "
"Cyclic time features allow the model to learn intraday seasonality. "
"target_horizon=4 bars (1 hour on 15-min data) balances trade frequency "
"against predictability. on_opposite=reverse reduces idle time and "
"captures trend continuation efficiently."
),
}
|
||||||||||
|
🥉
|
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%59.5% | +1.02%+19.97% | 1.061.87 | 2.59%2.59% | 21442 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||
|
7.01
|
EUR/USD SMA Trend + Multi-Indicator XGBoost
Maximize risk-adjusted return (Sharpe/Calmar) via XGBoost with deep SMA-based trend features (20/50/200), momentum, volatility, RSI, MACD, B…
|
E
@elastic-moose-350
|
EURUSD | 15min | 53.6%50.0% | +5.89%+11.08% | 1.972.07 | 1.64%1.64% | 5616 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-05 10:44:36
# 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/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 core features (required) ──────────────────────────────────────────
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 cross-ratios ──────────────────────────────────────────────────────
df["sma_20_50_ratio"] = df["sma_20"] / df["sma_50"]
df["sma_50_200_ratio"] = df["sma_50"] / df["sma_200"]
df["sma_20_200_ratio"] = df["sma_20"] / df["sma_200"]
# ── SMA slope (rate of change of SMA over N bars) ─────────────────────────
for p in [20, 50, 200]:
df[f"sma_{p}_slope5"] = df[f"sma_{p}"].diff(5) / df[f"sma_{p}"].shift(5)
# ── Price momentum / returns ───────────────────────────────────────────────
for lag in [1, 2, 3, 4, 8, 16]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Volatility (rolling std of returns) ───────────────────────────────────
ret1 = close.pct_change(1)
for w in [8, 20, 50]:
df[f"vol_std_{w}"] = ret1.rolling(w).std()
# ── ATR (manual) ──────────────────────────────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
for w in [8, 14, 20]:
atr = tr.rolling(w).mean()
df[f"atr_{w}"] = atr
df[f"natr_{w}"] = atr / close
# ── RSI (manual) ──────────────────────────────────────────────────────────
for period in [7, 14, 21]:
delta = close.diff(1)
gain = delta.clip(lower=0).rolling(period).mean()
loss = (-delta.clip(upper=0)).rolling(period).mean()
rs = gain / loss.replace(0, np.nan)
df[f"rsi_{period}"] = 100 - (100 / (1 + rs))
# ── MACD (manual) ─────────────────────────────────────────────────────────
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
df["macd_norm"] = macd_line / close
# ── Bollinger Bands (manual) ──────────────────────────────────────────────
for w in [20]:
mid = close.rolling(w).mean()
std = close.rolling(w).std()
upper = mid + 2 * std
lower = mid - 2 * std
bw = (upper - lower) / mid
pct_b = (close - lower) / (upper - lower).replace(0, np.nan)
df[f"bb_upper_{w}"] = upper
df[f"bb_lower_{w}"] = lower
df[f"bb_width_{w}"] = bw
df[f"bb_pct_{w}"] = pct_b
# ── Stochastic oscillator (manual) ────────────────────────────────────────
for k_period in [14]:
lo_k = low.rolling(k_period).min()
hi_k = high.rolling(k_period).max()
stoch_k = 100 * (close - lo_k) / (hi_k - lo_k).replace(0, np.nan)
stoch_d = stoch_k.rolling(3).mean()
df[f"stoch_k_{k_period}"] = stoch_k
df[f"stoch_d_{k_period}"] = stoch_d
# ── CCI (manual) ──────────────────────────────────────────────────────────
for w in [14, 20]:
tp = (high + low + close) / 3
tp_ma = tp.rolling(w).mean()
tp_md = tp.rolling(w).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
df[f"cci_{w}"] = (tp - tp_ma) / (0.015 * tp_md.replace(0, np.nan))
# ── Williams %R (manual) ──────────────────────────────────────────────────
for w in [14]:
hi_w = high.rolling(w).max()
lo_w = low.rolling(w).min()
df[f"willr_{w}"] = -100 * (hi_w - close) / (hi_w - lo_w).replace(0, np.nan)
# ── Donchian channel position ──────────────────────────────────────────────
for w in [20, 50]:
hi_d = high.rolling(w).max()
lo_d = low.rolling(w).min()
df[f"donch_pos_{w}"] = (close - lo_d) / (hi_d - lo_d).replace(0, np.nan)
# ── Rolling high / low distances ──────────────────────────────────────────
for w in [8, 20]:
df[f"dist_hi_{w}"] = (high.rolling(w).max() - close) / close
df[f"dist_lo_{w}"] = (close - low.rolling(w).min()) / close
# ── Bar body / wick features ───────────────────────────────────────────────
body = (close - open_).abs()
bar_range = (high - low).replace(0, np.nan)
df["body_ratio"] = body / bar_range
df["upper_wick_ratio"] = (high - close.clip(lower=open_)) / bar_range
df["lower_wick_ratio"] = (close.clip(upper=open_) - low) / bar_range
df["bar_direction"] = np.where(close >= open_, 1.0, -1.0)
# ── Trend regime flags (binary) ────────────────────────────────────────────
df["above_sma20"] = np.where(close > df["sma_20"], 1.0, 0.0)
df["above_sma50"] = np.where(close > df["sma_50"], 1.0, 0.0)
df["above_sma200"] = np.where(close > df["sma_200"], 1.0, 0.0)
df["sma20_above50"] = np.where(df["sma_20"] > df["sma_50"], 1.0, 0.0)
df["sma50_above200"] = np.where(df["sma_50"] > df["sma_200"], 1.0, 0.0)
# ── Lagged returns for autoregressive signal ───────────────────────────────
for lag in [1, 2, 3, 4, 5]:
df[f"close_lag_{lag}"] = close.shift(lag)
df[f"ret_lag_{lag}"] = ret1.shift(lag)
# ── Rolling correlation: price vs SMA distance ─────────────────────────────
for w in [20]:
df[f"autocorr_ret_{w}"] = ret1.rolling(w).apply(
lambda x: pd.Series(x).autocorr(lag=1) if len(x) > 1 else 0.0, raw=False
)
# ── Volume-proxy: bar range z-score ───────────────────────────────────────
for w in [20]:
rng_mean = bar_range.rolling(w).mean()
rng_std = bar_range.rolling(w).std().replace(0, np.nan)
df[f"range_zscore_{w}"] = (bar_range - rng_mean) / rng_std
# ── Fill NaN from warm-up ─────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD SMA Trend + Multi-Indicator XGBoost",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 500,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.8,
"colsample_bytree": 0.7,
"colsample_bylevel": 0.8,
"min_child_weight": 5,
"gamma": 0.1,
"reg_alpha": 0.1,
"reg_lambda": 2.0,
"scale_pos_weight": 1,
"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.01,
"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) via XGBoost with deep "
"SMA-based trend features (20/50/200), momentum, volatility, RSI, MACD, "
"Bollinger Bands, Stochastics, CCI, Donchian channels, and bar microstructure. "
"Regularised tree ensemble (L1+L2, subsampling) prevents overfit on 15-min FX data. "
"2:1 reward-to-risk with 0.5% SL / 1.0% TP targets consistent positive expectancy. "
"Session filter [6,18] UTC focuses on liquid London+NY overlap. "
"Trend filter sma_50 suppresses counter-trend noise."
),
"notes": (
"n_estimators=500 with low learning_rate=0.03 gives stable generalisation. "
"max_depth=4 limits tree complexity to avoid overfit on 15-min EURUSD. "
"min_child_weight=5 and gamma=0.1 add conservative splitting constraints. "
"reg_lambda=2.0 strong L2 regularisation for stable leaf weights. "
"colsample_bytree=0.7 adds feature bagging diversity. "
"target_horizon=4 (1 hour ahead) balances signal frequency and predictability. "
"signal_threshold=0.56 filters marginal predictions, improving precision."
),
}
|
||||||||||
|
2.69
|
EUR/USD RSI+MACD Momentum Scalper (XGBoost)
Maximize risk-adjusted return (Sharpe/Calmar). XGBoost with regularisation (alpha, lambda, gamma, min_child_weight) to reduce overfitting on…
|
V
@vol_drifter
|
EURUSD | 15min | 60.9%60.4% | +3.29%+9.74% | 1.191.38 | 3.02%3.02% | 22548 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 03:32:34
# Model : XGBoost
# 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/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):
# ── 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))
# RSI-derived features
df["rsi_overbought"] = np.where(df["rsi_14"] > 70, 1, 0)
df["rsi_oversold"] = np.where(df["rsi_14"] < 30, 1, 0)
df["rsi_centered"] = df["rsi_14"] - 50.0
# RSI momentum (change over last 3 bars)
df["rsi_slope"] = df["rsi_14"].diff(3)
# ── 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()
macd_hist = macd_line - signal_line
df["macd_line"] = macd_line
df["signal_line"] = signal_line
df["macd_hist"] = macd_hist
# MACD-derived features
df["macd_above_signal"] = np.where(macd_line > signal_line, 1, 0)
df["macd_hist_slope"] = macd_hist.diff(2)
df["macd_hist_sign"] = np.where(macd_hist > 0, 1, -1)
# ── Additional price-based features ─────────────────────────────────────
# ATR (14)
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
df["atr_14"] = tr.ewm(com=13, min_periods=14).mean()
df["natr_14"] = df["atr_14"] / close
# Normalised price position within recent range (20-bar)
roll_high = high.rolling(20).max()
roll_low = low.rolling(20).min()
denom = (roll_high - roll_low).replace(0, np.nan)
df["price_position_20"] = (close - roll_low) / denom
# 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
bb_width = (bb_upper - bb_lower) / sma_20.replace(0, np.nan)
df["bb_pct_b"] = (close - bb_lower) / (bb_upper - bb_lower).replace(0, np.nan)
df["bb_width"] = bb_width
# SMA trend features
sma_50 = close.rolling(50).mean()
sma_200 = close.rolling(200).mean()
df["sma_50"] = sma_50
df["price_vs_sma50"] = (close - sma_50) / sma_50.replace(0, np.nan)
df["price_vs_sma200"] = (close - sma_200) / sma_200.replace(0, np.nan)
df["sma_trend"] = np.where(sma_50 > sma_200, 1, -1)
# Momentum / returns
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)
# Candle body / wick features
body = (close - open_).abs()
candle_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng
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)
# Volatility regime: rolling std of returns (20-bar)
df["vol_regime"] = df["ret_1"].rolling(20).std()
# RSI × MACD interaction
df["rsi_macd_interact"] = df["rsi_centered"] * df["macd_hist"]
# Volume of MACD histogram change (acceleration)
df["macd_hist_accel"] = df["macd_hist"].diff(1)
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD RSI+MACD Momentum Scalper (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.75,
"min_child_weight": 5,
"gamma": 0.1,
"reg_alpha": 0.05,
"reg_lambda": 1.5,
"objective": "binary:logistic",
"tree_method": "hist",
"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": [7, 17],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar). "
"XGBoost with regularisation (alpha, lambda, gamma, min_child_weight) "
"to reduce overfitting on 15-min EUR/USD data. "
"Shallow trees (max_depth=4) and column/row subsampling prevent "
"memorisation of noise. Horizon=4 bars (1 hour) balances signal "
"quality vs trade frequency. Session filter [7,17] UTC focuses on "
"liquid London/NY overlap. SL=0.5%, TP=1.0% gives 1:2 R/R. "
"Threshold=0.55 filters marginal predictions while keeping enough trades."
),
"notes": (
"Features: RSI-14 (centred, slope, OB/OS flags), MACD(12,26,9) "
"(line, signal, histogram, slope, acceleration, interaction with RSI), "
"Bollinger Bands (pct_b, width), SMA-50/200 trend offsets, "
"ATR/NATR, 20-bar price position, momentum returns (1/3/8/16 bars), "
"candle anatomy (body ratio, upper/lower wick), volatility regime. "
"All features are look-ahead free and normalised where possible."
),
}
|
||||||||||
|
2.24
|
EUR/USD SMA Trend + Multi-Indicator GBM Scalper
Maximise risk-adjusted return (Sharpe / Calmar). GradientBoostingClassifier chosen for its strong performance on tabular financial data with…
|
S
@still-lynx-704
|
EURUSD | 15min | 39.7%40.0% | +5.29%+6.83% | 1.621.49 | 1.82%1.82% | 7320 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:35:43
# 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/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):
# ── SMA core 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 slope (momentum of the moving average itself) ──────────────────
for period in [20, 50, 200]:
df[f"sma_{period}_slope"] = df[f"sma_{period}"].diff(5) / df[f"sma_{period}"].shift(5)
# ── SMA crossover signals ──────────────────────────────────────────────
df["sma_20_50_cross"] = df["sma_20"] - df["sma_50"]
df["sma_50_200_cross"] = df["sma_50"] - df["sma_200"]
df["sma_20_200_cross"] = df["sma_20"] - df["sma_200"]
# Sign of crossover difference (trend direction)
df["trend_20_50"] = np.where(df["sma_20_50_cross"] > 0, 1, -1)
df["trend_50_200"] = np.where(df["sma_50_200_cross"] > 0, 1, -1)
# ── Price momentum ────────────────────────────────────────────────────
for lag in [1, 4, 8, 16, 32]:
df[f"return_{lag}"] = close.pct_change(lag)
# ── Volatility features ───────────────────────────────────────────────
# 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)
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
# Rolling realised volatility
log_ret = np.log(close / close.shift(1))
for vol_period in [20, 50]:
df[f"realvol_{vol_period}"] = log_ret.rolling(vol_period).std()
# ── 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_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower + 1e-12)
df["bb_position"] = (close - bb_mid) / (bb_std + 1e-12)
# ── RSI (14) ──────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0).rolling(14).mean()
loss = (-delta.clip(upper=0)).rolling(14).mean()
rs = gain / (loss + 1e-12)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI normalised to [-1, 1]
df["rsi_14_norm"] = (df["rsi_14"] - 50) / 50
# ── 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
df["macd_hist_chg"] = df["macd_hist"].diff()
# Normalise MACD by price
df["macd_norm"] = df["macd"] / close
df["macd_hist_norm"] = df["macd_hist"] / close
# ── Stochastic Oscillator (14, 3) ─────────────────────────────────────
low14 = low.rolling(14).min()
high14 = high.rolling(14).max()
stoch_k = 100 * (close - low14) / (high14 - low14 + 1e-12)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_diff"] = stoch_k - stoch_d
# ── Rate-of-change ────────────────────────────────────────────────────
for roc_period in [5, 10, 20]:
df[f"roc_{roc_period}"] = (close - close.shift(roc_period)) / (close.shift(roc_period) + 1e-12)
# ── Candle body and wick features ─────────────────────────────────────
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_direction"] = np.where(close >= open_, 1, -1)
# ── Time-of-day features (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)
# ── Lagged returns as features ────────────────────────────────────────
for lag in [1, 2, 3, 4]:
df[f"close_lag_{lag}"] = close.shift(lag)
df[f"ret_lag_{lag}"] = log_ret.shift(lag)
# ── Volume proxy: range-based ─────────────────────────────────────────
df["range_abs"] = high - low
df["range_norm"] = (high - low) / close
# ── High-Low channel position ─────────────────────────────────────────
for ch_period in [20, 50]:
ch_high = high.rolling(ch_period).max()
ch_low = low.rolling(ch_period).min()
df[f"channel_pos_{ch_period}"] = (close - ch_low) / (ch_high - ch_low + 1e-12)
# ── Fill NaN from warm-up ─────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD SMA Trend + Multi-Indicator GBM Scalper",
"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,
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"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, 18],
"min_atr": None,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return (Sharpe / Calmar). "
"GradientBoostingClassifier chosen for its strong performance on "
"tabular financial data with moderate feature sets. "
"Shallow trees (max_depth=4) with high n_estimators and a low "
"learning_rate reduce overfitting. subsample=0.75 adds stochastic "
"regularisation. Early stopping via n_iter_no_change prevents "
"over-training on the validation split. "
"SL=0.5%, TP=1.0% gives a 1:2 risk-reward ratio, "
"targeting positive expectancy even at sub-60% accuracy. "
"Session filter (06-18 UTC) restricts trading to liquid hours "
"covering London and New York overlap for EUR/USD. "
"SMA-50 trend filter ensures trades align with the medium-term "
"trend, reducing counter-trend noise."
),
"notes": (
"Features include required SMA(20,50,200) distances and crossovers, "
"RSI-14, MACD histogram, Bollinger Band position, Stochastic, ATR, "
"realised volatility, rate-of-change, candle structure ratios, "
"channel position, lagged returns, and cyclical time encoding. "
"Target horizon of 4 bars (1 hour) on 15-min data balances "
"signal frequency with meaningful directional moves."
),
}
|
||||||||||
|
1.76
|
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%40.0% | +11.79%+2.85% | 2.921.28 | 0.83%0.83% | 6710 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||
|
0.63
|
EUR/USD Gradient Boost SMA+RSI+MACD Swing
Maximize risk-adjusted return (Sharpe / Calmar) on EUR/USD 15-min. GradientBoostingClassifier chosen for robustness to noisy FX features and…
|
E
@elastic-moose-350
|
EURUSD | 15min | 47.2%44.4% | +4.52%+3.06% | 1.551.26 | 2.72%2.72% | 7218 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 03:08:55
# Model : Gradient Boosting
# 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-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 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) ────────────────────────────────────────────
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_width"] = (bb_upper - bb_lower) / bb_mid
denom = bb_upper - bb_lower
df["bb_pct"] = np.where(denom != 0, (close - bb_lower) / denom, 0.5)
# ── 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 - (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()
df["macd_line"] = macd_line
df["macd_sig"] = signal_line
df["macd_hist"] = macd_line - signal_line
# ── ATR 14 + NATR ─────────────────────────────────────────────────────
hl = high - low
hc = (high - close.shift(1)).abs()
lc = (low - close.shift(1)).abs()
tr = pd.concat([hl, hc, lc], 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 p in [4, 8, 16, 32]:
df[f"roc_{p}"] = close.pct_change(p)
# ── Candle body and 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["body_dir"] = np.sign(close - open_)
# ── Volume-normalised (uses candle range as proxy if no volume col) ───
# Rolling z-score of close
roll_mean = close.rolling(20).mean()
roll_std = close.rolling(20).std(ddof=0).replace(0, np.nan)
df["close_zscore_20"] = (close - roll_mean) / roll_std
# ── RSI divergence proxy ──────────────────────────────────────────────
df["rsi_delta_4"] = df["rsi_14"].diff(4)
df["price_delta_4"] = close.pct_change(4)
df["rsi_price_div"] = df["rsi_delta_4"] - (df["price_delta_4"] * 100)
# ── MACD histogram slope ──────────────────────────────────────────────
df["macd_hist_slope"] = df["macd_hist"].diff(2)
# ── SMA crossover signals ─────────────────────────────────────────────
df["sma20_vs_50"] = np.where(df["sma_20"] > df["sma_50"], 1.0, -1.0)
df["sma50_vs_200"] = np.where(df["sma_50"] > df["sma_200"], 1.0, -1.0)
# ── Volatility regime (ATR percentile proxy) ──────────────────────────
atr_roll_min = atr.rolling(96).min()
atr_roll_max = atr.rolling(96).max()
atr_range = (atr_roll_max - atr_roll_min).replace(0, np.nan)
df["atr_pctile_96"] = (atr - atr_roll_min) / atr_range
# ── Stochastic oscillator %K, %D ──────────────────────────────────────
low_14 = low.rolling(14).min()
high_14 = high.rolling(14).max()
stoch_k = 100 * (close - low_14) / (high_14 - low_14).replace(0, np.nan)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_diff"] = stoch_k - stoch_d
# ── Rolling high/low breakout distance ────────────────────────────────
df["dist_hi_20"] = (high.rolling(20).max() - close) / close
df["dist_lo_20"] = (close - low.rolling(20).min()) / close
# ── Hour-of-day and day-of-week cyclical features ─────────────────────
hour = pd.Series(df.index.hour, index=df.index, dtype=float)
dow = pd.Series(df.index.dayofweek, index=df.index, dtype=float)
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)
# ── Fill NaN from indicator warm-up ───────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD Gradient Boost SMA+RSI+MACD Swing",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"min_samples_leaf": 20,
"min_samples_split": 30,
"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.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 18],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe / Calmar) on EUR/USD 15-min. "
"GradientBoostingClassifier chosen for robustness to noisy FX features and "
"good probability calibration. Shallow trees (depth 4), high n_estimators with "
"early stopping prevent overfitting. Subsample=0.75 adds stochasticity. "
"SL=0.5%, TP=1.0% gives 1:2 R:R. Session filter 07-18 UTC captures London+NY overlap. "
"min_atr filters out flat/illiquid periods. sma_50 trend filter aligns trades with "
"medium-term momentum. Threshold 0.55 balances precision vs recall."
),
"notes": (
"Features: SMA(20,50,200) with distance ratios, BB(20,2) width+pct, RSI-14, "
"MACD(12,26,9) line/signal/hist + slope, ATR-14 + NATR, ROC(4,8,16,32), "
"candle body/wick ratios, close z-score, RSI-price divergence proxy, "
"stochastic %K/%D, rolling high/low breakout distances, "
"SMA crossover flags, ATR percentile regime, hour/DOW cyclical encodings. "
"on_opposite=reverse means a strong counter-signal immediately flips the position, "
"reducing idle time and capturing reversals within the London-NY session."
),
}
|
||||||||||
|
0.37
|
EMA Cross (9/21) + RSI Confirmation — XGBoost
Maximize risk-adjusted return (Sharpe / Calmar) on EUR/USD 15-min data. XGBoost with moderate depth (4) and heavy regularisation (gamma, alp…
|
S
@still-lynx-704
|
EURUSD | 15min | 43.6%36.4% | +5.16%+1.47% | 1.551.12 | 1.62%1.62% | 9422 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:39:22
# 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/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 ──────────────────────────────────────────────────────
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 cross signal: +1 when ema_9 > ema_21, -1 otherwise
df["ema_cross"] = np.where(ema_9 > ema_21, 1.0, -1.0)
# EMA cross momentum: difference normalised by ema_21
df["ema_spread"] = (ema_9 - ema_21) / ema_21
# Rate of change of EMA spread (1-bar and 3-bar)
df["ema_spread_chg1"] = df["ema_spread"].diff(1)
df["ema_spread_chg3"] = df["ema_spread"].diff(3)
# ── RSI 14 ────────────────────────────────────────────────────────────────
delta = close.diff(1)
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)
rsi_14 = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi_14
# RSI normalised to [-1, 1]
df["rsi_norm"] = (rsi_14 - 50) / 50
# RSI momentum
df["rsi_chg1"] = rsi_14.diff(1)
df["rsi_chg3"] = rsi_14.diff(3)
# RSI zone flags (overbought / oversold)
df["rsi_ob"] = np.where(rsi_14 > 70, 1.0, 0.0)
df["rsi_os"] = np.where(rsi_14 < 30, 1.0, 0.0)
# ── ATR 14 (for normalisation & volatility context) ───────────────────────
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
df["natr_14"] = atr_14 / close # normalised ATR
# ── Price momentum features ───────────────────────────────────────────────
for n in [1, 3, 5, 10, 20]:
df[f"ret_{n}"] = close.pct_change(n)
# ── Volatility: rolling std of returns ───────────────────────────────────
ret1 = close.pct_change(1)
df["vol_5"] = ret1.rolling(5).std()
df["vol_20"] = ret1.rolling(20).std()
# ── Bollinger Band features (20, 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
df["bb_pct"] = (close - bb_lo) / (bb_up - bb_lo).replace(0, np.nan)
df["bb_width"] = (bb_up - bb_lo) / bb_mid
# ── MACD-style: difference of EMA12 and EMA26 ────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd = ema_12 - ema_26
signal = macd.ewm(span=9, adjust=False).mean()
df["macd"] = macd / close
df["macd_signal"] = signal / close
df["macd_hist"] = (macd - signal) / close
# ── High-Low channel position ─────────────────────────────────────────────
hh20 = high.rolling(20).max()
ll20 = low.rolling(20).min()
df["hl_pos_20"] = (close - ll20) / (hh20 - ll20).replace(0, np.nan)
hh5 = high.rolling(5).max()
ll5 = low.rolling(5).min()
df["hl_pos_5"] = (close - ll5) / (hh5 - ll5).replace(0, np.nan)
# ── Bar body & shadow features ────────────────────────────────────────────
body = (close - open_).abs()
range_ = (high - low).replace(0, np.nan)
df["body_ratio"] = body / range_
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / range_
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / range_
df["bull_bar"] = np.where(close > open_, 1.0, 0.0)
# ── Rolling correlation: EMA spread vs RSI (captures confluence) ──────────
df["corr_spread_rsi"] = df["ema_spread"].rolling(10).corr(rsi_14)
# ── Time-of-day features (hour & minute encoded cyclically) ───────────────
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)
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 indicator warm-up ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EMA Cross (9/21) + RSI Confirmation — 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": [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 (4) and heavy regularisation (gamma, alpha, lambda) "
"to avoid overfitting on a 1-year window. "
"EMA cross provides trend direction; RSI filters against overbought/oversold entries. "
"Asymmetric TP/SL (2:1) boosts expectancy. Session filter restricts trading to "
"London + NY overlap (06–20 UTC) where EUR/USD liquidity is highest. "
"min_atr removes low-volatility bars where spreads erode edge."
),
"notes": (
"Features: EMA 9/21 cross & spread, RSI 14, MACD histogram, Bollinger Band %B, "
"ATR-normalised volatility, price momentum (1/3/5/10/20 bars), rolling vol, "
"high-low channel position, candlestick body/shadow ratios, cyclical time encoding. "
"Target horizon = 4 bars (1 hour ahead). Train/test split 70/30 (no leakage). "
"Cooldown = 0 because on_opposite='reverse' keeps the model always positioned "
"when high-confidence signals appear."
),
}
|
||||||||||
|
—
|
EUR/USD RSI Mean-Reversion + BB Filter (XGBoost)
Maximize Sharpe ratio on a single XGBClassifier trained on a 70/30 train/test holdout. Features centre on RSI oversold/overbought zones (RSI…
|
C
@cold-stork-489
|
EURUSD | 15min | 68.5%46.9% | +11.94%-11.29% | 1.680.72 | 5.91%5.91% | 5449 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-07-27 07:19:12
# Model : XGBoost
# Feature Eng. : buy EURUSD when RSI drops below 30 and sell when RSI rises above 70 + Auto-add features: ON
# Signal / Entry : —
# Optimization : —
# Risk Mgmt : —
# 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 = "2026-06-17"
END_DATE = "2026-07-27"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
LEVERAGE = 30.0
LOTS = 1.0
BALANCE = 10000.0
RISK_UNIT = 'pct'
STOP_LOSS = None
TAKE_PROFIT = None
# 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(upper=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 + 1e-10)
rsi_14 = 100.0 - (100.0 / (1.0 + rs))
df["rsi_14"] = rsi_14
# RSI oversold / overbought binary flags (core user logic)
df["rsi_oversold"] = np.where(rsi_14 < 30, 1.0, 0.0) # potential buy zone
df["rsi_overbought"] = np.where(rsi_14 > 70, 1.0, 0.0) # potential sell zone
# RSI distance from 50 (signed momentum measure)
df["rsi_dist_50"] = rsi_14 - 50.0
# RSI slope (1-bar change)
df["rsi_slope"] = rsi_14.diff(1)
# --- RSI (6) — fast RSI for short-term momentum ---
avg_gain6 = gain.ewm(com=5, min_periods=6, adjust=False).mean()
avg_loss6 = loss.ewm(com=5, min_periods=6, adjust=False).mean()
rs6 = avg_gain6 / (avg_loss6 + 1e-10)
rsi_6 = 100.0 - (100.0 / (1.0 + rs6))
df["rsi_6"] = rsi_6
# --- RSI (28) — slow RSI for regime context ---
avg_gain28 = gain.ewm(com=27, min_periods=28, adjust=False).mean()
avg_loss28 = loss.ewm(com=27, min_periods=28, adjust=False).mean()
rs28 = avg_gain28 / (avg_loss28 + 1e-10)
rsi_28 = 100.0 - (100.0 / (1.0 + rs28))
df["rsi_28"] = rsi_28
# RSI divergence proxy: fast minus slow
df["rsi_divergence"] = rsi_6 - rsi_28
# --- Bollinger Bands (20, 2) ---
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_width = (bb_upper - bb_lower) / (bb_mid + 1e-10)
bb_pct_b = (close - bb_lower) / (bb_upper - bb_lower + 1e-10)
df["bb_pct_b"] = bb_pct_b
df["bb_width"] = bb_width
df["bb_mid_dist"] = (close - bb_mid) / (bb_std + 1e-10)
# Price at/near lower band (potential buy) or upper band (potential sell)
df["bb_near_lower"] = np.where(bb_pct_b < 0.1, 1.0, 0.0)
df["bb_near_upper"] = np.where(bb_pct_b > 0.9, 1.0, 0.0)
# Combined RSI + BB oversold/overbought signals
df["rsi_bb_buy"] = np.where((rsi_14 < 35) & (bb_pct_b < 0.2), 1.0, 0.0)
df["rsi_bb_sell"] = np.where((rsi_14 > 65) & (bb_pct_b > 0.8), 1.0, 0.0)
# --- ATR (14) for volatility context ---
tr1 = high - low
tr2 = (high - close.shift(1)).abs()
tr3 = (low - close.shift(1)).abs()
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
atr_14 = tr.ewm(com=13, min_periods=14, adjust=False).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / (close + 1e-10) # normalised ATR
# --- EMA trend context ---
ema_20 = close.ewm(span=20, adjust=False).mean()
ema_50 = close.ewm(span=50, adjust=False).mean()
ema_100 = close.ewm(span=100, adjust=False).mean()
df["ema_20_dist"] = (close - ema_20) / (atr_14 + 1e-10)
df["ema_50_dist"] = (close - ema_50) / (atr_14 + 1e-10)
df["ema_100_dist"] = (close - ema_100) / (atr_14 + 1e-10)
df["ema_20_50_cross"] = np.where(ema_20 > ema_50, 1.0, -1.0)
df["sma_50"] = close.rolling(50).mean() # used as trend_filter anchor
# --- 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()
macd_hist = macd_line - signal_line
df["macd_line"] = macd_line / (atr_14 + 1e-10)
df["macd_signal"] = signal_line / (atr_14 + 1e-10)
df["macd_hist"] = macd_hist / (atr_14 + 1e-10)
df["macd_hist_slope"] = macd_hist.diff(1) / (atr_14 + 1e-10)
# --- Stochastic Oscillator (14, 3) ---
low_14 = low.rolling(14).min()
high_14 = high.rolling(14).max()
stoch_k = 100.0 * (close - low_14) / (high_14 - low_14 + 1e-10)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_oversold"] = np.where(stoch_k < 20, 1.0, 0.0)
df["stoch_overbought"] = np.where(stoch_k > 80, 1.0, 0.0)
# --- Price momentum / returns ---
df["ret_1"] = close.pct_change(1)
df["ret_3"] = close.pct_change(3)
df["ret_6"] = close.pct_change(6)
df["ret_12"] = close.pct_change(12)
df["ret_24"] = close.pct_change(24)
# --- Candlestick body / wick features ---
body = (close - open_)
candle_rng = (high - low + 1e-10)
df["body_ratio"] = body / candle_rng
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["body_atr_norm"] = body / (atr_14 + 1e-10)
# --- Volume proxy: range-based surrogate ---
df["range_norm"] = candle_rng / (atr_14 + 1e-10)
# --- Rolling volatility regime ---
ret_std = close.pct_change(1).rolling(20).std()
df["vol_regime"] = np.where(ret_std > ret_std.rolling(60).median(), 1.0, 0.0)
# --- Mean-reversion z-score (20-bar) ---
roll_mean = close.rolling(20).mean()
roll_std = close.rolling(20).std(ddof=0)
df["zscore_20"] = (close - roll_mean) / (roll_std + 1e-10)
# --- Lag features of key signals (avoid look-ahead) ---
df["rsi_14_lag1"] = rsi_14.shift(1)
df["rsi_14_lag2"] = rsi_14.shift(2)
df["rsi_14_lag4"] = rsi_14.shift(4)
df["macd_hist_lag1"] = df["macd_hist"].shift(1)
df["zscore_20_lag1"] = df["zscore_20"].shift(1)
# Fill NaN from indicator warm-up periods
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD RSI Mean-Reversion + BB Filter (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 3,
"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,
"n_jobs": -1,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.0030,
"take_profit": 0.0060,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 17],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize Sharpe ratio on a single XGBClassifier trained on a 70/30 "
"train/test holdout. Features centre on RSI oversold/overbought zones "
"(RSI<30 buy, RSI>70 sell) reinforced by Bollinger Band %B, MACD histogram, "
"Stochastic K, z-score mean reversion, and candle-body shape. "
"XGBoost hyperparameters use moderate depth (4), a low learning rate (0.03), "
"and L1/L2 regularisation to reduce overfitting and improve out-of-sample "
"Sharpe. SL=0.30%, TP=0.60% gives a 1:2 R/R that supports positive "
"expectancy on mean-reversion trades. Session filter 07-17 UTC keeps "
"execution within liquid London/NY overlap hours."
),
"notes": (
"Round-trip transaction cost of 2e-5 per trade is accounted for by the "
"framework. The strategy trades both directions, reversing on opposite "
"signals with no cooldown to remain responsive in choppy mean-reverting "
"regimes. target_horizon=4 bars (1 hour on 15-min data) aligns the "
"prediction label with a realistic intraday mean-reversion window. "
"No walk-forward or cross-validation is used — this is a single holdout split."
),
}
|
||||||||||
|
—
|
EUR/USD RSI Mean-Reversion + BB Confluence (XGBoost)
Maximize Sharpe ratio via RSI mean-reversion on EURUSD 15-min. A single XGBClassifier is trained on a 70/30 train/test holdout. Core signal:…
|
N
@neural-tiger-347
|
EURUSD | 15min | 60.6%46.6% | +5.89%-4.21% | 1.260.89 | 7.45%7.45% | 7158 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-07-21 01:59:30
# Model : XGBoost
# Feature Eng. : Buy EURUSD 15min when RSI < 30 and sell when RSI > 70, with a stop loss of 15 pips and a take profit of 30 pips. + Auto-add features: ON
# Signal / Entry : —
# Optimization : —
# Risk Mgmt : —
# 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 = "2026-06-11"
END_DATE = "2026-07-21"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
LEVERAGE = 30.0
LOTS = 1.0
BALANCE = 10000.0
RISK_UNIT = 'pct'
STOP_LOSS = None
TAKE_PROFIT = None
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# --- RSI (14) — primary signal driver ---
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=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 + 1e-10)
rsi_14 = 100.0 - (100.0 / (1.0 + rs))
df['rsi_14'] = rsi_14
# Oversold / overbought binary flags (no lookahead — current bar RSI only)
df['rsi_oversold'] = np.where(rsi_14 < 30, 1, 0)
df['rsi_overbought'] = np.where(rsi_14 > 70, 1, 0)
# RSI distance from 50 (signed momentum proxy)
df['rsi_dist_50'] = rsi_14 - 50.0
# RSI short-period (7) for faster reaction
avg_gain7 = gain.ewm(com=6, min_periods=7, adjust=False).mean()
avg_loss7 = loss.ewm(com=6, min_periods=7, adjust=False).mean()
rs7 = avg_gain7 / (avg_loss7 + 1e-10)
rsi_7 = 100.0 - (100.0 / (1.0 + rs7))
df['rsi_7'] = rsi_7
# RSI divergence proxy: RSI7 - RSI14
df['rsi_divergence'] = rsi_7 - rsi_14
# --- Bollinger Bands (20, 2) ---
bb_window = 20
bb_mid = close.rolling(bb_window).mean()
bb_std = close.rolling(bb_window).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 + 1e-10
df['bb_pct_b'] = (close - bb_lower) / bb_range # 0=lower band, 1=upper band
df['bb_width'] = bb_range / (bb_mid + 1e-10) # band width relative to mid
df['bb_mid_dist'] = (close - bb_mid) / (bb_std + 1e-10) # z-score vs band mid
# Price below lower band or above upper band flags
df['price_below_bb'] = np.where(close < bb_lower, 1, 0)
df['price_above_bb'] = np.where(close > bb_upper, 1, 0)
# --- ATR (14) — volatility / pip sizing ---
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(com=13, min_periods=14, adjust=False).mean()
df['atr_14'] = atr_14
df['natr_14'] = atr_14 / (close + 1e-10) # normalized ATR
# --- MACD (12, 26, 9) ---
ema12 = close.ewm(span=12, min_periods=12, adjust=False).mean()
ema26 = close.ewm(span=26, min_periods=26, adjust=False).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, min_periods=9, adjust=False).mean()
df['macd_line'] = macd_line
df['macd_signal'] = macd_signal
df['macd_hist'] = macd_line - macd_signal
df['macd_cross_up'] = np.where((macd_line > macd_signal) & (macd_line.shift(1) <= macd_signal.shift(1)), 1, 0)
df['macd_cross_down'] = np.where((macd_line < macd_signal) & (macd_line.shift(1) >= macd_signal.shift(1)), 1, 0)
# --- Stochastic %K and %D (14, 3) ---
lowest_14 = low.rolling(14).min()
highest_14 = high.rolling(14).max()
stoch_k = 100.0 * (close - lowest_14) / (highest_14 - lowest_14 + 1e-10)
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_oversold'] = np.where(stoch_k < 20, 1, 0)
df['stoch_overbought'] = np.where(stoch_k > 80, 1, 0)
# --- Williams %R (14) ---
df['williams_r'] = -100.0 * (highest_14 - close) / (highest_14 - lowest_14 + 1e-10)
# --- Momentum: rate of change ---
df['roc_4'] = close.pct_change(4) # 1-hour return on 15-min bars
df['roc_8'] = close.pct_change(8) # 2-hour return
df['roc_16'] = close.pct_change(16) # 4-hour return
# --- EMA trend features ---
ema_20 = close.ewm(span=20, min_periods=20, adjust=False).mean()
ema_50 = close.ewm(span=50, min_periods=50, adjust=False).mean()
ema_100 = close.ewm(span=100, min_periods=100, adjust=False).mean()
df['ema_20'] = ema_20
df['ema_50'] = ema_50
df['price_vs_ema20'] = (close - ema_20) / (ema_20 + 1e-10)
df['price_vs_ema50'] = (close - ema_50) / (ema_50 + 1e-10)
df['ema20_vs_ema50'] = (ema_20 - ema_50) / (ema_50 + 1e-10)
df['ema20_vs_ema100'] = (ema_20 - ema_100) / (ema_100 + 1e-10)
# --- Candle body & wick features ---
body = (close - open_).abs()
candle_range = (high - low).abs() + 1e-10
df['body_ratio'] = body / candle_range # body as fraction of 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_candle'] = np.where(close > open_, 1, 0)
# --- Rolling volatility (std of returns) ---
ret = close.pct_change()
df['vol_8'] = ret.rolling(8).std()
df['vol_20'] = ret.rolling(20).std()
# --- Lagged RSI values for sequence context ---
df['rsi_14_lag1'] = rsi_14.shift(1)
df['rsi_14_lag2'] = rsi_14.shift(2)
df['rsi_14_lag4'] = rsi_14.shift(4)
# --- RSI slope (momentum of RSI) ---
df['rsi_slope_4'] = rsi_14 - rsi_14.shift(4)
# --- Combined oversold/overbought confirmation flags ---
# Both RSI and Stochastic agree on oversold → stronger buy signal
df['dual_oversold'] = np.where((rsi_14 < 35) & (stoch_k < 25), 1, 0)
df['dual_overbought'] = np.where((rsi_14 > 65) & (stoch_k > 75), 1, 0)
# Fill any NaN introduced by rolling/ewm warm-up
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD RSI Mean-Reversion + BB Confluence (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.15,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"scale_pos_weight": 1.0,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.0015, # 15 pips on EURUSD (1 pip ≈ 0.0001)
"take_profit": 0.0030, # 30 pips on EURUSD → 2:1 reward/risk
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 17], # London + early NY session UTC
"min_atr": None,
"trend_filter": None,
"target_horizon": 4, # predict direction 1 hour ahead (4 × 15-min bars)
"objective": (
"Maximize Sharpe ratio via RSI mean-reversion on EURUSD 15-min. "
"A single XGBClassifier is trained on a 70/30 train/test holdout. "
"Core signal: RSI < 30 (buy) / RSI > 70 (sell), confirmed by Bollinger Band %B, "
"Stochastic, MACD histogram, and candle structure features. "
"SL=15 pips / TP=30 pips gives 2:1 reward-to-risk aligned with mean-reversion style. "
"XGBoost depth-4 trees with regularisation (alpha, lambda, gamma) reduce overfitting "
"on the short date range. Session filter [7,17] UTC keeps execution in liquid hours."
),
"notes": (
"Feature set centres on RSI at multiple periods (7, 14) and their lags, "
"Bollinger Band z-score and %B, Stochastic %K/%D, Williams %R, MACD histogram, "
"candle body/wick ratios, and rolling volatility. Dual oversold/overbought flags "
"combine RSI + Stochastic for higher-confidence entries. "
"No talib/ta library used — all indicators computed with pandas/numpy. "
"Round-trip transaction cost ~2e-5 per trade is handled by the framework's backtest engine."
),
}
|
||||||||||
|
—
|
EUR/USD XGBoost Multi-Feature Sharpe Maximiser
Maximise Sharpe ratio on 15-min EUR/USD. XGBoost with moderate depth (4) and heavy regularisation (reg_lambda=1.5, min_child_weight=5, gamma…
|
A
@alpha-viper-151
|
EURUSD | 15min | 54.4%41.4% | +0.53%-5.67% | 1.270.88 | 0.63%0.63% | 6887 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-24 01:27:47
# Model : XGBoost
# Feature Eng. : deploy a 15min EURUSD model + Auto-add features: ON
# Signal / Entry : —
# Optimization : —
# Risk Mgmt : —
# 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 = "2026-04-14"
END_DATE = "2026-05-12"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Price returns ──────────────────────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_2"] = close.pct_change(2)
df["ret_4"] = close.pct_change(4)
df["ret_8"] = close.pct_change(8)
df["ret_16"] = close.pct_change(16)
df["ret_32"] = close.pct_change(32)
# ── Candle body / wick features ────────────────────────────────────────
hl = (high - low).replace(0, np.nan)
body = (close - open_).abs()
df["body_ratio"] = body / hl
df["upper_wick"] = (high - np.maximum(close, open_)) / hl
df["lower_wick"] = (np.minimum(close, open_) - low) / hl
df["candle_dir"] = np.sign(close - open_)
# ── RSI (14) ───────────────────────────────────────────────────────────
def _rsi(src, n=14):
delta = src.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_g = gain.ewm(com=n - 1, min_periods=n).mean()
avg_l = loss.ewm(com=n - 1, min_periods=n).mean()
rs = avg_g / avg_l.replace(0, np.nan)
return 100 - (100 / (1 + rs))
df["rsi_14"] = _rsi(close, 14)
df["rsi_7"] = _rsi(close, 7)
df["rsi_21"] = _rsi(close, 21)
# RSI normalised distance from 50
df["rsi_14_dev"] = (df["rsi_14"] - 50) / 50
# ── EMA crossovers ─────────────────────────────────────────────────────
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()
ema200 = close.ewm(span=200, adjust=False).mean()
df["ema8"] = ema8
df["ema21"] = ema21
df["ema50"] = ema50
df["ema8_21_xo"] = (ema8 - ema21) / close
df["ema21_50_xo"] = (ema21 - ema50) / close
df["ema50_200_xo"] = (ema50 - ema200) / close
# Price distance from EMAs (normalised)
df["dist_ema8"] = (close - ema8) / close
df["dist_ema21"] = (close - ema21) / close
df["dist_ema50"] = (close - ema50) / close
df["dist_ema200"] = (close - ema200) / close
# ── MACD ───────────────────────────────────────────────────────────────
macd_line = close.ewm(span=12, adjust=False).mean() - close.ewm(span=26, adjust=False).mean()
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line / close
df["macd_signal"] = macd_signal / close
df["macd_hist"] = (macd_line - macd_signal) / close
df["macd_hist_chg"] = df["macd_hist"].diff()
# ── Bollinger Bands (20, 2) ────────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
bb_upper = bb_mid + 2 * bb_std
bb_lower = bb_mid - 2 * bb_std
bb_width = (bb_upper - bb_lower) / bb_mid.replace(0, np.nan)
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower).replace(0, np.nan)
df["bb_width"] = bb_width
df["bb_width_chg"] = bb_width.diff()
# ── 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(com=13, min_periods=14).mean()
natr = atr14 / close
df["atr14"] = atr14
df["natr14"] = natr
df["natr14_chg"] = natr.diff()
# ── Momentum / Rate of Change ──────────────────────────────────────────
df["mom_4"] = close - close.shift(4)
df["mom_8"] = close - close.shift(8)
df["roc_10"] = (close / close.shift(10).replace(0, np.nan)) - 1
df["roc_20"] = (close / close.shift(20).replace(0, np.nan)) - 1
# ── Stochastic Oscillator (14,3) ───────────────────────────────────────
low14 = low.rolling(14).min()
high14 = high.rolling(14).max()
stoch_k = (close - low14) / (high14 - low14).replace(0, np.nan) * 100
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_kd"] = stoch_k - stoch_d
# ── Volume (if available, else dummy) ──────────────────────────────────
if "volume" in df.columns:
vol = df["volume"].replace(0, np.nan)
vol_ma20 = vol.rolling(20).mean()
df["vol_ratio"] = vol / vol_ma20
df["vol_chg"] = vol.pct_change()
else:
df["vol_ratio"] = 1.0
df["vol_chg"] = 0.0
# ── Rolling volatility ─────────────────────────────────────────────────
df["realvol_8"] = df["ret_1"].rolling(8).std()
df["realvol_20"] = df["ret_1"].rolling(20).std()
df["vol_ratio_short_long"] = df["realvol_8"] / df["realvol_20"].replace(0, np.nan)
# ── Highs/Lows distance (support/resistance proxy) ─────────────────────
df["high_20_dist"] = (high.rolling(20).max() - close) / close
df["low_20_dist"] = (close - low.rolling(20).min()) / close
df["high_5_dist"] = (high.rolling(5).max() - close) / close
df["low_5_dist"] = (close - low.rolling(5).min()) / close
# ── Lagged features ────────────────────────────────────────────────────
for col in ["rsi_14_dev", "macd_hist", "bb_pct", "natr14", "stoch_kd"]:
df[f"{col}_lag1"] = df[col].shift(1)
df[f"{col}_lag2"] = df[col].shift(2)
df[f"{col}_lag4"] = df[col].shift(4)
# ── Session dummies (hour-of-day in UTC) ───────────────────────────────
hour = close.index.hour
df["session_london"] = np.where((hour >= 7) & (hour < 16), 1, 0)
df["session_ny"] = np.where((hour >= 13) & (hour < 21), 1, 0)
df["session_overlap"] = np.where((hour >= 13) & (hour < 16), 1, 0)
df["session_asia"] = np.where((hour >= 0) & (hour < 7), 1, 0)
# Day-of-week
dow = close.index.dayofweek
df["dow_mon"] = np.where(dow == 0, 1, 0)
df["dow_fri"] = np.where(dow == 4, 1, 0)
# ── Interaction features ───────────────────────────────────────────────
df["rsi_macd"] = df["rsi_14_dev"] * df["macd_hist"]
df["rsi_bbpct"] = df["rsi_14_dev"] * df["bb_pct"]
df["macd_vol"] = df["macd_hist"] * df["vol_ratio"]
df["natr_bbwid"] = df["natr14"] * df["bb_width"]
# ── SMA50 for trend filter ─────────────────────────────────────────────
df["sma_50"] = close.rolling(50).mean()
# ── Fill NaN from indicator warm-up ───────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD XGBoost Multi-Feature Sharpe Maximiser",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.75,
"colsample_bytree": 0.70,
"colsample_bylevel": 0.80,
"min_child_weight": 5,
"gamma": 0.1,
"reg_alpha": 0.05,
"reg_lambda": 1.5,
"scale_pos_weight": 1.0,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.55,
"direction": "both",
"max_positions": 1,
"on_opposite": "reverse",
"cooldown": 0,
"stop_loss": 0.0008,
"take_profit": 0.0016,
"session_filter": [7, 21],
"min_atr": 0.00015,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximise Sharpe ratio on 15-min EUR/USD. "
"XGBoost with moderate depth (4) and heavy regularisation "
"(reg_lambda=1.5, min_child_weight=5, gamma=0.1) controls overfitting "
"on the short date window. Low learning_rate (0.03) with 400 trees "
"for stable convergence. SL=0.8 pip / TP=1.6 pip gives 1:2 RR to "
"preserve Sharpe. Session filter [7,21] removes illiquid Asia opens. "
"min_atr filters flat, low-volatility bars that degrade signal quality."
),
"notes": (
"Feature set covers trend (EMA crosses, MACD), mean-reversion (RSI, BB%B), "
"volatility (ATR, realvol), momentum (ROC, Stochastic), microstructure "
"(candle body/wick ratios), session dummies, and lagged versions of key "
"signals to give the model temporal context without lookahead. "
"Interaction terms (rsi*macd, rsi*bb_pct) capture combined regime signals. "
"target_horizon=4 bars (1 hour) balances enough price movement to overcome "
"2e-5 round-trip cost while avoiding excessive label noise."
),
}
|
||||||||||
|
—
|
EMA crossover (9/21) + RSI 14 confirmation
Claude-generated EMA 9/21 trend filter with RSI 14 momentum gate on EURUSD 15min. Test holdout: WR 71%, PF 2.36, 77 trades, +1.7% over ~8 da…
|
P
@pivot_kid
|
EURUSD | 15min | 71.4%46.9% | +1.72%-9.45% | 2.360.78 | 0.37%0.37% | 7764 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-07 01:38:25
# Model : XGBoost
# Feature Eng. : EMA crossover trend (9/21) with RSI 14 confirmation on EURUSD 15min + Auto-add features: ON
# Signal / Entry : —
# Optimization : —
# Risk Mgmt : —
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# QUANTIFY ME — STRATEGY MODULE
# EMA Crossover + RSI Confirmation (XGBoost, Sharpe Optimization)
# ============================================================
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2026-03-28"
END_DATE = "2026-04-25"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# ============================================================
# SECTION 1 — FEATURE ENGINEERING
# ============================================================
def feature_engineering(df, close, open_, high, low):
"""
Add EMA crossover trend features + RSI confirmation.
Features:
- EMA 9 and EMA 21 for trend direction
- RSI 14 for momentum confirmation
- EMA crossover signal
- Price deviation from EMA 21
- High/Low proximity ratios
- Volume-based volatility (NATR)
"""
# EMA 9 and EMA 21 for trend
df['ema_9'] = close.ewm(span=9, adjust=False).mean()
df['ema_21'] = close.ewm(span=21, adjust=False).mean()
df['ema_crossover'] = np.where(df['ema_9'] > df['ema_21'], 1, -1)
# EMA crossover signal (1 when 9 crosses above 21, -1 when crosses below)
df['ema_cross_signal'] = df['ema_crossover'].diff().fillna(0)
df['ema_cross_signal'] = np.where(df['ema_cross_signal'] != 0, df['ema_cross_signal'], 0)
# Price deviation from EMA 21 (normalized)
df['price_ema_deviation'] = (close - df['ema_21']) / df['ema_21']
# RSI 14 for momentum confirmation
delta = close.diff()
gain = np.where(delta > 0, delta, 0)
loss = np.where(delta < 0, -delta, 0)
avg_gain = pd.Series(gain, index=close.index).ewm(span=14, adjust=False).mean()
avg_loss = pd.Series(loss, index=close.index).ewm(span=14, adjust=False).mean()
rs = avg_gain / (avg_loss + 1e-10)
df['rsi_14'] = 100 - (100 / (1 + rs))
# RSI signal: overbought/oversold
df['rsi_overbought'] = np.where(df['rsi_14'] > 70, 1, 0)
df['rsi_oversold'] = np.where(df['rsi_14'] < 30, 1, 0)
# High/Low proximity (distance from recent extremes)
df['high_20'] = high.rolling(window=20).max()
df['low_20'] = low.rolling(window=20).min()
df['price_position'] = (close - df['low_20']) / (df['high_20'] - df['low_20'] + 1e-10)
# NATR (Normalized ATR) for volatility
atr_period = 14
tr1 = high - low
tr2 = np.abs(high - close.shift(1))
tr3 = np.abs(low - close.shift(1))
tr = np.maximum(tr1, np.maximum(tr2, tr3))
atr = pd.Series(tr, index=close.index).rolling(window=atr_period).mean()
df['natr'] = (atr / close) * 100
# EMA momentum (rate of change in EMA)
df['ema_9_roc'] = df['ema_9'].pct_change(periods=3)
df['ema_21_roc'] = df['ema_21'].pct_change(periods=3)
# Close relative to open (intrabar direction)
df['close_above_open'] = np.where(close > open_, 1, 0)
# Volume-based features (if available; otherwise skip)
if 'volume' in df.columns:
df['volume_ma'] = df['volume'].rolling(window=20).mean()
df['volume_ratio'] = df['volume'] / (df['volume_ma'] + 1e-10)
else:
df['volume_ratio'] = 1.0
# Fill NaN from indicator warm-up
df = df.bfill().ffill()
return df
# ============================================================
# SECTION 2 — STRATEGY CONFIG
# ============================================================
def strategy_config():
"""
XGBoost strategy optimized for Sharpe ratio on EMA/RSI signals.
Hyperparameters tuned for:
- Fast learning (learning_rate=0.08)
- Shallow trees (max_depth=4) to avoid overfitting on 15min data
- Moderate boosting (n_estimators=250) for good generalization
- Regularization (subsample=0.85, colsample_bytree=0.8)
- Balanced class weights via scale_pos_weight
Signal threshold 0.55 chosen to be moderately selective while maintaining
good trade frequency on the EMA crossover setup.
"""
return {
# Model specification
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 250,
"max_depth": 4,
"learning_rate": 0.08,
"subsample": 0.85,
"colsample_bytree": 0.8,
"min_child_weight": 1,
"gamma": 0.5,
"reg_alpha": 0.1,
"reg_lambda": 1.0,
"random_state": 42,
"verbosity": 0,
},
# Entry signal
"signal_threshold": 0.55,
# Position management
"direction": "both",
"max_positions": 1,
"on_opposite": "reverse",
"cooldown": 0,
# Risk management
"stop_loss": 0.008,
"take_profit": 0.015,
# Filters
"session_filter": None,
"min_atr": None,
"trend_filter": None,
# Target
"target_horizon": 4,
# Metadata
"title": "EMA Crossover + RSI Confirmation (XGBoost)",
"objective": "Maximize Sharpe ratio with EMA 9/21 trend + RSI 14 momentum confirmation",
"notes": (
"Strategy uses fast EMA (9) crossover above/below slow EMA (21) "
"as primary trend signal, confirmed by RSI 14 momentum. "
"XGBoost learns non-linear interactions between these features. "
"Moderate SL/TP (0.8%/1.5%) and bidirectional trading for scalping efficiency. "
"Optimized for EURUSD 15min with 70/30 train/test split."
),
}
|
||||||||||
|
—
|
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%39.3% | +9.05%-8.18% | 2.640.74 | 1.05%1.05% | 6628 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||
|
—
|
EUR/USD SMA Trend + Multi-Indicator XGBoost
Maximize risk-adjusted return (Sharpe/Calmar) on EUR/USD 15-min data. SMA triple-stack (20/50/200) provides trend context; supplementary mom…
|
D
@delta_one
|
EURUSD | 15min | 45.5%41.2% | +4.67%-8.91% | 1.670.65 | 1.74%1.74% | 4417 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:56:13
# 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/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):
# ── SMA core 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 slope (momentum of the moving average itself) ─────────────────
for period in [20, 50, 200]:
df[f"sma_{period}_slope"] = df[f"sma_{period}"].diff(5) / df[f"sma_{period}"].shift(5)
# ── SMA crossover signals ─────────────────────────────────────────────
df["sma_20_50_cross"] = df["sma_20"] - df["sma_50"]
df["sma_50_200_cross"] = df["sma_50"] - df["sma_200"]
df["sma_20_200_cross"] = df["sma_20"] - df["sma_200"]
# ── Price vs SMA alignment score (how many SMAs price is above) ───────
above_20 = np.where(close > df["sma_20"], 1, -1)
above_50 = np.where(close > df["sma_50"], 1, -1)
above_200 = np.where(close > df["sma_200"], 1, -1)
df["sma_alignment"] = (above_20 + above_50 + above_200).astype(float)
# ── Returns at multiple horizons ──────────────────────────────────────
for lag in [1, 2, 4, 8, 16]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Log returns ───────────────────────────────────────────────────────
df["log_ret_1"] = np.log(close / close.shift(1))
df["log_ret_4"] = np.log(close / close.shift(4))
# ── ATR (14-period) ───────────────────────────────────────────────────
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.rolling(14).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / close
# ── ATR ratio (current TR vs average — volatility burst) ──────────────
df["atr_ratio"] = true_range / atr_14
# ── Bollinger Bands (20, 2) ───────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).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
df["bb_pct_b"] = (close - bb_lower) / (bb_upper - bb_lower + 1e-12)
df["bb_width"] = bb_width
df["bb_zscore"] = (close - bb_mid) / (bb_std + 1e-12)
# ── RSI (14) ──────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0).rolling(14).mean()
loss = (-delta.clip(upper=0)).rolling(14).mean()
rs = gain / (loss + 1e-12)
df["rsi_14"] = 100 - (100 / (1 + rs))
# ── RSI momentum ─────────────────────────────────────────────────────
df["rsi_14_diff"] = df["rsi_14"].diff(2)
# ── 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
df["macd_hist_diff"] = df["macd_hist"].diff(1)
# ── Stochastic %K / %D (14, 3) ───────────────────────────────────────
low_14 = low.rolling(14).min()
high_14 = high.rolling(14).max()
stoch_k = 100 * (close - low_14) / (high_14 - low_14 + 1e-12)
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
# ── Williams %R (14) ─────────────────────────────────────────────────
df["williams_r"] = -100 * (high_14 - close) / (high_14 - low_14 + 1e-12)
# ── CCI (20) ─────────────────────────────────────────────────────────
typical_price = (high + low + close) / 3
tp_sma = typical_price.rolling(20).mean()
tp_mad = typical_price.rolling(20).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
df["cci_20"] = (typical_price - tp_sma) / (0.015 * tp_mad + 1e-12)
# ── Rate of Change (10) ───────────────────────────────────────────────
df["roc_10"] = (close - close.shift(10)) / (close.shift(10) + 1e-12) * 100
# ── Volume / candle-body features (no volume data assumed) ────────────
df["body_size"] = (close - open_).abs() / (high - low + 1e-12)
df["upper_wick"] = (high - np.maximum(close, open_)) / (high - low + 1e-12)
df["lower_wick"] = (np.minimum(close, open_) - low) / (high - low + 1e-12)
df["is_bullish"] = np.where(close > open_, 1.0, 0.0)
# ── Rolling volatility (realised) ────────────────────────────────────
df["vol_8"] = df["log_ret_1"].rolling(8).std()
df["vol_20"] = df["log_ret_1"].rolling(20).std()
df["vol_ratio"] = df["vol_8"] / (df["vol_20"] + 1e-12)
# ── High-low range relative to SMA ───────────────────────────────────
df["hl_range_sma20"] = (high - low) / (df["sma_20"] + 1e-12)
# ── Price position within recent n-bar range ──────────────────────────
for n in [8, 20]:
roll_low = low.rolling(n).min()
roll_high = high.rolling(n).max()
df[f"price_pos_{n}"] = (close - roll_low) / (roll_high - roll_low + 1e-12)
# ── Lagged returns as autoregressive features ─────────────────────────
for lag in [1, 2, 3, 4, 8]:
df[f"lag_ret_{lag}"] = df["ret_1"].shift(lag)
# ── Distance from 20-bar high/low ────────────────────────────────────
df["dist_high_20"] = (high.rolling(20).max() - close) / (close + 1e-12)
df["dist_low_20"] = (close - low.rolling(20).min()) / (close + 1e-12)
# ── Fill NaN from warm-up ─────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD SMA Trend + Multi-Indicator XGBoost",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 500,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.8,
"colsample_bytree": 0.7,
"min_child_weight": 3,
"gamma": 0.1,
"reg_alpha": 0.05,
"reg_lambda": 1.0,
"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": [6, 18],
"min_atr": None,
"trend_filter": "sma_200",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on EUR/USD 15-min data. "
"SMA triple-stack (20/50/200) provides trend context; supplementary "
"momentum (RSI, MACD, Stochastic), volatility (ATR, BB), and "
"mean-reversion (CCI, Williams %R) features give the XGBoost model a "
"rich multi-regime signal set. XGBoost chosen for its ability to rank "
"feature importance and handle non-linear interactions. Shallow trees "
"(max_depth=4) with high n_estimators and low learning_rate reduce "
"overfitting. 2:1 reward:risk (SL 0.5% / TP 1.0%) ensures positive "
"expectancy even at moderate win rates. Session filter 06-18 UTC "
"concentrates trades in liquid London/NY overlap."
),
"notes": (
"trend_filter=sma_200 aligns trades with the dominant trend — longs "
"only above the 200-SMA, shorts only below — acting as a regime gate "
"to suppress counter-trend noise. signal_threshold=0.55 slightly above "
"0.50 to reduce false positives without starving signal count. "
"colsample_bytree=0.7 introduces randomisation across features to "
"decorrelate trees and improve generalisation on forex data."
),
}
|
||||||||||