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 | ||
|---|---|---|---|---|---|---|---|---|---|---|
|
1.36
|
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%59.5% | +3.29%+5.49% | 1.191.26 | 3.02%3.02% | 22542 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||
|
1.30
|
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%45.0% | +9.05%+2.56% | 2.641.18 | 1.05%1.05% | 6620 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||
|
1.23
|
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%40.9% | +5.16%+3.79% | 1.551.29 | 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."
),
}
|
||||||||||
|
1.07
|
AUD/USD Bollinger + ATR Mean-Rev (XGBoost)
Maximize risk-adjusted return (Sharpe). XGBoost with moderate depth and heavy regularisation (gamma, alpha, lambda) prevents overfit on AUD/…
|
E
@elastic-moose-350
|
AUDUSD | 15min | 63.9%67.4% | +6.79%+4.33% | 1.121.14 | 3.10%3.10% | 65689 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:44:26
# Model : XGBoost
# Feature Eng. : BB (20,2.0), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_sigma = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_sigma
bb_lower = bb_mid - bb_std * bb_sigma
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
# guard against zero range
bb_range = bb_upper - bb_lower
df["bb_pct"] = np.where(bb_range != 0, (close - bb_lower) / bb_range, 0.5)
# ── ATR (14) & Normalised ATR ────────────────────────────────────────────
atr_period = 14
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(span=atr_period, min_periods=atr_period, adjust=False).mean()
df["atr"] = atr
df["natr"] = np.where(close != 0, atr / close, 0.0)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(span=rsi_period, min_periods=rsi_period, adjust=False).mean()
avg_loss = loss.ewm(span=rsi_period, min_periods=rsi_period, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi
# ── MACD (12, 26, 9) ─────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
# ── EMA trend features ───────────────────────────────────────────────────
ema_20 = close.ewm(span=20, adjust=False).mean()
ema_50 = close.ewm(span=50, adjust=False).mean()
ema_200 = close.ewm(span=200, adjust=False).mean()
df["ema_20"] = ema_20
df["ema_50"] = ema_50
df["ema_200"] = ema_200
df["close_vs_ema20"] = (close - ema_20) / ema_20
df["close_vs_ema50"] = (close - ema_50) / ema_50
df["ema20_vs_ema50"] = (ema_20 - ema_50) / ema_50
df["ema50_vs_ema200"] = (ema_50 - ema_200) / ema_200
# ── Price momentum / rate-of-change ──────────────────────────────────────
for n in [1, 4, 8, 16]:
df[f"roc_{n}"] = close.pct_change(n)
# ── Rolling volatility ────────────────────────────────────────────────────
df["vol_10"] = close.pct_change().rolling(10).std()
df["vol_20"] = close.pct_change().rolling(20).std()
df["vol_ratio"] = np.where(df["vol_20"] != 0,
df["vol_10"] / df["vol_20"], 1.0)
# ── Stochastic %K / %D (14, 3) ───────────────────────────────────────────
low_14 = low.rolling(14).min()
high_14 = high.rolling(14).max()
stoch_range = high_14 - low_14
stoch_k = np.where(stoch_range != 0,
100 * (close - low_14) / stoch_range, 50.0)
df["stoch_k"] = stoch_k
df["stoch_d"] = pd.Series(stoch_k, index=close.index).rolling(3).mean()
# ── Candle body / wick features ──────────────────────────────────────────
body = (close - open_).abs()
total_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / total_rng
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / total_rng
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / total_rng
df["candle_dir"] = np.sign(close - open_)
# ── BB interaction features ───────────────────────────────────────────────
df["price_above_bb_upper"] = np.where(close > bb_upper, 1, 0)
df["price_below_bb_lower"] = np.where(close < bb_lower, 1, 0)
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).quantile(0.20), 1, 0)
# ── RSI regime bins (replacing pd.cut) ───────────────────────────────────
df["rsi_oversold"] = np.where(rsi < 30, 1, 0)
df["rsi_overbought"] = np.where(rsi > 70, 1, 0)
df["rsi_neutral"] = np.where((rsi >= 30) & (rsi <= 70), 1, 0)
# ── Volume proxy (if volume column exists) ───────────────────────────────
if "volume" in df.columns:
vol_ma = df["volume"].rolling(20).mean()
df["volume_ratio"] = np.where(vol_ma != 0,
df["volume"] / vol_ma, 1.0)
# ── Lagged features (1-bar lag to avoid lookahead) ───────────────────────
for feat in ["bb_pct", "rsi_14", "macd_hist", "natr", "stoch_k"]:
df[f"{feat}_lag1"] = df[feat].shift(1)
df[f"{feat}_lag2"] = df[feat].shift(2)
# ── Fill NaNs from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD Bollinger + ATR Mean-Rev (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 3,
"gamma": 0.10,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe). "
"XGBoost with moderate depth and heavy regularisation "
"(gamma, alpha, lambda) prevents overfit on AUD/USD 15-min data. "
"Bollinger Bands capture mean-reversion; ATR normalises volatility; "
"RSI + MACD confirm momentum; 2:1 TP:SL ratio supports positive expectancy."
),
"notes": (
"Features: BB (20,2) width/pct, ATR-14/NATR, RSI-14, MACD histogram, "
"EMA 20/50/200 spreads, Stochastic %K/%D, candle-body ratios, "
"ROC at multiple horizons, volatility ratio, BB squeeze flag, "
"lagged versions of key features. "
"Threshold 0.56 filters marginal signals, improving precision. "
"target_horizon=4 (1 hour) balances signal frequency vs. noise."
),
}
|
||||||||||
|
0.57
|
AUD/USD XGBoost SMA+RSI+MACD+BB Momentum
Maximize risk-adjusted return (Sharpe/Calmar) on AUD/USD 15-min. XGBoost with depth-4 trees and conservative regularization (reg_lambda=1.5,…
|
D
@delta-atlas-858
|
AUDUSD | 15min | 62.9%70.9% | +10.32%+2.92% | 1.171.10 | 3.96%3.96% | 745103 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:32:18
# Model : XGBoost
# Feature Eng. : SMA (20,50,200), BB (20,2.0), RSI 14, MACD (12,26,9), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# AUDUSD 15-min XGBoost Strategy
# SMA + RSI + MACD + Bollinger Bands + ATR Feature Set
# Optimized for Risk-Adjusted Return
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA 20, 50, 200 + distance from close ──────────────────────────────
for period in [20, 50, 200]:
sma = close.rolling(period).mean()
df[f"sma_{period}"] = sma
df[f"dm_sma_{period}"] = (close - sma) / sma
# ── Bollinger Bands (20, 2.0) ───────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
bb_upper = bb_mid + 2.0 * bb_std
bb_lower = bb_mid - 2.0 * bb_std
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
bb_range = bb_upper - bb_lower
df["bb_pct"] = (close - bb_lower) / bb_range
# ── RSI 14 ──────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=13, min_periods=14).mean()
avg_loss = loss.ewm(com=13, min_periods=14).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100.0 - (100.0 / (1.0 + rs))
# ── MACD (12, 26, 9) ────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
# ── ATR 14 + Normalised ATR ─────────────────────────────────────────────
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(com=13, min_periods=14).mean()
df["atr_14"] = atr
df["natr"] = atr / close
# ── Price momentum / rate-of-change ────────────────────────────────────
for n in [1, 4, 8, 16]:
df[f"roc_{n}"] = close.pct_change(n)
# ── Candle body & wick features ─────────────────────────────────────────
body = (close - open_).abs()
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["candle_dir"] = np.where(close >= open_, 1.0, -1.0)
# ── Volume (if present) ─────────────────────────────────────────────────
if "volume" in df.columns:
vol_ma = df["volume"].rolling(20).mean()
df["vol_ratio"] = df["volume"] / vol_ma.replace(0, np.nan)
# ── Lagged RSI & MACD histogram ─────────────────────────────────────────
for lag in [1, 2, 3]:
df[f"rsi_14_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
# ── RSI overbought / oversold zones ─────────────────────────────────────
df["rsi_ob"] = np.where(df["rsi_14"] > 70, 1.0, 0.0)
df["rsi_os"] = np.where(df["rsi_14"] < 30, 1.0, 0.0)
df["rsi_mid_up"] = np.where((df["rsi_14"] > 50) & (df["rsi_14"] <= 70), 1.0, 0.0)
df["rsi_mid_dn"] = np.where((df["rsi_14"] >= 30) & (df["rsi_14"] < 50), 1.0, 0.0)
# ── MACD cross signals ───────────────────────────────────────────────────
df["macd_cross_up"] = np.where(
(df["macd_line"] > df["macd_signal"]) &
(df["macd_line"].shift(1) <= df["macd_signal"].shift(1)),
1.0, 0.0
)
df["macd_cross_dn"] = np.where(
(df["macd_line"] < df["macd_signal"]) &
(df["macd_line"].shift(1) >= df["macd_signal"].shift(1)),
1.0, 0.0
)
# ── Price position relative to SMA alignment ────────────────────────────
df["trend_aligned_bull"] = np.where(
(close > df["sma_20"]) & (df["sma_20"] > df["sma_50"]) & (df["sma_50"] > df["sma_200"]),
1.0, 0.0
)
df["trend_aligned_bear"] = np.where(
(close < df["sma_20"]) & (df["sma_20"] < df["sma_50"]) & (df["sma_50"] < df["sma_200"]),
1.0, 0.0
)
# ── Bollinger Band squeeze (low volatility) ──────────────────────────────
bb_width_ma = df["bb_width"].rolling(20).mean()
df["bb_squeeze"] = np.where(df["bb_width"] < bb_width_ma, 1.0, 0.0)
# ── Rolling close statistics ─────────────────────────────────────────────
df["close_zscore_20"] = (close - close.rolling(20).mean()) / close.rolling(20).std(ddof=0)
df["close_zscore_50"] = (close - close.rolling(50).mean()) / close.rolling(50).std(ddof=0)
# ── Volatility regime ────────────────────────────────────────────────────
natr_ma = df["natr"].rolling(20).mean()
df["vol_regime_high"] = np.where(df["natr"] > natr_ma, 1.0, 0.0)
# ── Fill NaN from indicator warm-up ─────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD XGBoost SMA+RSI+MACD+BB Momentum",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 500,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.1,
"reg_alpha": 0.1,
"reg_lambda": 1.5,
"scale_pos_weight": 1.0,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on AUD/USD 15-min. "
"XGBoost with depth-4 trees and conservative regularization (reg_lambda=1.5, "
"min_child_weight=5) to reduce overfitting on FX data. "
"2:1 RR (SL=0.5%, TP=1.0%) ensures positive expectancy with ~40%+ win rate. "
"Subsample + colsample add stochastic diversity. 500 estimators with lr=0.04 "
"balances bias-variance. Threshold 0.55 filters marginal signals."
),
"notes": (
"Features: SMA(20/50/200) with distances, Bollinger Bands width+pct, RSI-14 "
"with zone flags, MACD histogram + crosses, ATR-14 + NATR, momentum ROC(1/4/8/16), "
"candle body/wick ratios, trend alignment flags, BB squeeze, z-scores, vol regime. "
"Reverse on opposite signal to capture trend reversals. Session filter disabled "
"to capture AUD/USD Asian + London + NY sessions. Target horizon = 4 bars (1 hour)."
),
}
|
||||||||||
|
0.38
|
NZD/USD EMA Cross (9/21) + RSI Gradient Boost
Maximise risk-adjusted return (Sharpe) on NZD/USD 15-min data. GradientBoostingClassifier chosen for its strong out-of-box performance on ta…
|
C
@candid-owl-125
|
NZDUSD | 15min | 62.5%54.5% | +19.51%+1.66% | 1.361.05 | 2.51%2.51% | 745134 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:54:13
# 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/NZDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── 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
# Rate of change of the crossover (momentum of the cross)
df["ema_cross_roc"] = df["ema_cross"].diff(3)
# ── RSI 14 (required) ────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=13, adjust=False).mean()
avg_loss = loss.ewm(com=13, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI normalised to [-1, 1]
df["rsi_norm"] = (df["rsi_14"] - 50) / 50
# RSI momentum (1-bar diff of RSI)
df["rsi_diff"] = df["rsi_14"].diff(1)
# RSI overbought / oversold flags (np.where, no pd.cut)
df["rsi_ob"] = np.where(df["rsi_14"] > 70, 1, 0)
df["rsi_os"] = np.where(df["rsi_14"] < 30, 1, 0)
# ── ATR 14 ───────────────────────────────────────────────────────────────
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(com=13, adjust=False).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / close # normalised ATR
# ── MACD-style fast/slow difference (12/26 EMA) ─────────────────────────
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
df["macd_signal"] = macd_signal / close
df["macd_hist"] = (macd_line - macd_signal) / close
# ── 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
bb_bw = (bb_up - bb_lo) / bb_mid # bandwidth
bb_pct = (close - bb_lo) / (bb_up - bb_lo) # %B position
df["bb_bandwidth"] = bb_bw
df["bb_pct"] = bb_pct
# ── 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_diff"] = stoch_k - stoch_d
# ── Price momentum (returns over multiple horizons) ───────────────────────
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)
# ── Candle body & shadow ratios ───────────────────────────────────────────
body = (close - open_).abs()
candle_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_rng
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_rng
df["body_direction"] = np.sign(close - open_)
# ── Volume proxy: volatility-based (OHLC spread) ─────────────────────────
df["hl_spread"] = (high - low) / close
# ── Rolling volatility (std of returns) ──────────────────────────────────
df["vol_6"] = df["ret_1"].rolling(6).std()
df["vol_24"] = df["ret_1"].rolling(24).std()
# ── Z-score of close relative to 20-bar rolling mean ─────────────────────
roll_mean_20 = close.rolling(20).mean()
roll_std_20 = close.rolling(20).std()
df["zscore_20"] = (close - roll_mean_20) / roll_std_20.replace(0, np.nan)
# ── Trend strength: R² of close over 20 bars ─────────────────────────────
x = np.arange(20)
x_demeaned = x - x.mean()
ss_x = (x_demeaned ** 2).sum()
def rolling_r2(series, window=20):
arr = series.values
n = len(arr)
out = np.full(n, np.nan)
for i in range(window - 1, n):
y = arr[i - window + 1: i + 1]
if np.any(np.isnan(y)):
continue
y_m = y - y.mean()
slope = np.dot(x_demeaned, y_m) / ss_x
y_hat = slope * x_demeaned + y.mean()
ss_res = ((y - y_hat) ** 2).sum()
ss_tot = ((y - y.mean()) ** 2).sum()
out[i] = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
return out
df["trend_r2_20"] = rolling_r2(close, 20)
# ── SMA 50 (for trend filter reference; also used as feature) ─────────────
sma_50 = close.rolling(50).mean()
df["sma_50"] = sma_50
df["close_vs_sma50"] = (close - sma_50) / sma_50
# ── Higher-timeframe EMA proxy (4-bar resample = 1h equivalent) ──────────
ema_4h = close.ewm(span=4 * 21, adjust=False).mean()
df["dm_ema_4h"] = (close - ema_4h) / ema_4h
# ── Cross confirmation: EMA cross direction × RSI regime ─────────────────
df["cross_x_rsi"] = np.sign(df["ema_cross"]) * df["rsi_norm"]
# ── Fill NaN from indicator warm-up ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD EMA Cross (9/21) + RSI Gradient Boost",
"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": 25,
"tol": 1e-4,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return (Sharpe) on NZD/USD 15-min data. "
"GradientBoostingClassifier chosen for its strong out-of-box performance on "
"tabular financial data, built-in regularisation via subsample/max_features, "
"and early-stopping via n_iter_no_change. Depth-4 trees with 400 estimators "
"and lr=0.04 balance bias-variance. SL=0.5% / TP=1.0% gives 1:2 R:R. "
"4-bar horizon (~1 hour) aligns with EMA-cross momentum persistence. "
"Threshold 0.55 filters marginal signals while preserving trade frequency."
),
"notes": (
"Features: EMA 9/21 cross + distances, RSI 14 with OB/OS flags, MACD histogram, "
"Bollinger %B + bandwidth, Stochastic K/D, multi-horizon returns, candle body "
"ratios, rolling volatility, 20-bar z-score, trend R² and SMA50 distance. "
"No session filter applied — NZD/USD has meaningful liquidity across Asian + "
"London sessions. Reverse on opposite signal for continuous market exposure."
),
}
|
||||||||||
|
0.28
|
USD/CAD Stoch+BB+RSI Mean-Reversion (XGBoost)
Maximize risk-adjusted return (Sharpe/Calmar) by combining Stochastic (14,3), Bollinger Bands (20,2) and RSI(14) mean-reversion signals with…
|
V
@vega-puma-338
|
USDCAD | 15min | 58.2%52.8% | +2.45%+0.81% | 1.151.07 | 1.65%1.65% | 30936 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:58:30
# Model : XGBoost
# Feature Eng. : BB (20,2.0), RSI 14, Stochastic (14,3) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/USDCAD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_std_s = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_std_s
bb_lower = bb_mid - bb_std * bb_std_s
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
avg_loss = loss.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi"] = 100 - (100 / (1 + rs))
# ── Stochastic Oscillator (K=14, D=3) ────────────────────────────────────
stoch_k_period = 14
stoch_d_period = 3
lowest_low = low.rolling(stoch_k_period).min()
highest_high = high.rolling(stoch_k_period).max()
stoch_k_raw = 100 * (close - lowest_low) / (highest_high - lowest_low).replace(0, np.nan)
df["stoch_k"] = stoch_k_raw
df["stoch_d"] = stoch_k_raw.rolling(stoch_d_period).mean()
# ── Derived Stochastic features ──────────────────────────────────────────
df["stoch_kd_diff"] = df["stoch_k"] - df["stoch_d"] # K-D divergence
df["stoch_k_prev"] = df["stoch_k"].shift(1)
df["stoch_d_prev"] = df["stoch_d"].shift(1)
# Bullish crossover: K crosses above D
df["stoch_cross_up"] = np.where(
(df["stoch_k"] > df["stoch_d"]) & (df["stoch_k_prev"] <= df["stoch_d_prev"]), 1.0, 0.0
)
# Bearish crossover: K crosses below D
df["stoch_cross_dn"] = np.where(
(df["stoch_k"] < df["stoch_d"]) & (df["stoch_k_prev"] >= df["stoch_d_prev"]), 1.0, 0.0
)
# ── RSI-derived features ─────────────────────────────────────────────────
df["rsi_prev"] = df["rsi"].shift(1)
df["rsi_slope"] = df["rsi"] - df["rsi_prev"]
df["rsi_ob"] = np.where(df["rsi"] >= 70, 1.0, 0.0) # overbought flag
df["rsi_os"] = np.where(df["rsi"] <= 30, 1.0, 0.0) # oversold flag
# ── BB-derived features ──────────────────────────────────────────────────
df["bb_pct_prev"] = df["bb_pct"].shift(1)
df["bb_pct_slope"] = df["bb_pct"] - df["bb_pct_prev"]
df["price_vs_mid"] = (close - bb_mid) / bb_mid # normalised distance from mid
# Squeeze: narrow bands relative to recent history
df["bb_squeeze"] = np.where(
df["bb_width"] < df["bb_width"].rolling(50).mean(), 1.0, 0.0
)
# ── ATR (14) — volatility context ────────────────────────────────────────
atr_period = 14
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
df["atr"] = tr.ewm(com=atr_period - 1, min_periods=atr_period).mean()
df["natr"] = df["atr"] / close
# ── Momentum / price change features ─────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_4"] = close.pct_change(4)
df["ret_16"] = close.pct_change(16)
# ── Trend context: SMA 50 & 200 ──────────────────────────────────────────
df["sma_50"] = close.rolling(50).mean()
df["sma_200"] = close.rolling(200).mean()
df["price_vs_50"] = (close - df["sma_50"]) / df["sma_50"]
df["price_vs_200"] = (close - df["sma_200"]) / df["sma_200"]
df["trend_up"] = np.where(df["sma_50"] > df["sma_200"], 1.0, 0.0)
# ── Volume proxy: candle body / range ratio ───────────────────────────────
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = (close - open_).abs() / candle_range
df["bull_bar"] = np.where(close > open_, 1.0, 0.0)
# ── MACD-like momentum: EMA12 - EMA26 ────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
df["macd"] = ema12 - ema26
df["macd_signal"] = df["macd"].ewm(span=9, adjust=False).mean()
df["macd_hist"] = df["macd"] - df["macd_signal"]
# ── Rolling volatility (std of returns) ──────────────────────────────────
df["vol_10"] = df["ret_1"].rolling(10).std()
# ── Hour-of-day and day-of-week (cyclical) ────────────────────────────────
if hasattr(df.index, "hour"):
df["hour_sin"] = np.sin(2 * np.pi * df.index.hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * df.index.hour / 24)
df["dow_sin"] = np.sin(2 * np.pi * df.index.dayofweek / 5)
df["dow_cos"] = np.cos(2 * np.pi * df.index.dayofweek / 5)
# ── Combined signal: RSI + Stoch confluence ───────────────────────────────
df["conf_bull"] = np.where((df["rsi"] < 50) & (df["stoch_k"] < 50), 1.0, 0.0)
df["conf_bear"] = np.where((df["rsi"] > 50) & (df["stoch_k"] > 50), 1.0, 0.0)
# ── Fill NaN from warm-up periods ────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "USD/CAD Stoch+BB+RSI Mean-Reversion (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.80,
"colsample_bytree": 0.75,
"min_child_weight": 3,
"gamma": 0.10,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 20],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) by combining "
"Stochastic (14,3), Bollinger Bands (20,2) and RSI(14) mean-reversion "
"signals with XGBoost. Regularisation (reg_alpha, reg_lambda, gamma, "
"min_child_weight) and column/row subsampling control overfitting. "
"A 0.55 confidence threshold filters low-conviction trades. "
"Session filter [7,20] UTC focuses on liquid London+NY overlap hours. "
"SL=0.5% / TP=1.0% gives a 1:2 risk-reward per trade."
),
"notes": (
"target_horizon=4 bars (1 hour on 15-min data) suits intraday mean-reversion. "
"Cyclical time features (hour_sin/cos, dow_sin/cos) capture intraday seasonality. "
"MACD histogram and rolling volatility provide trend/momentum context alongside "
"the core BB/RSI/Stoch mean-reversion suite. "
"reverse on_opposite allows the model to flip positions when conviction is high "
"in the opposing direction without waiting for flat cooldown."
),
}
|
||||||||||
|
—
|
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."
),
}
|
||||||||||
|
—
|
NZD/USD MACD+RSI Gradient Boosting Risk-Adjusted
Maximize risk-adjusted return (Sharpe/Calmar) on NZD/USD 15-min data. GradientBoostingClassifier chosen for strong generalisation with tabul…
|
V
@vol_drifter
|
NZDUSD | 15min | 60.9%59.8% | +18.36%-4.44% | 1.350.92 | 3.80%3.80% | 732127 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:16:23
# Model : Gradient Boosting
# Feature Eng. : RSI 14, MACD (12,26,9) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/NZDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── RSI 14 ──────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(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.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI derived signals
df["rsi_overbought"] = np.where(df["rsi_14"] > 70, 1, 0)
df["rsi_oversold"] = np.where(df["rsi_14"] < 30, 1, 0)
df["rsi_mid_cross"] = np.where(df["rsi_14"] > 50, 1, -1)
# RSI momentum (rate of change of RSI)
df["rsi_roc"] = 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["macd_signal"] = signal_line
df["macd_hist"] = macd_hist
# MACD derived signals
df["macd_cross"] = np.where(macd_line > signal_line, 1, -1)
df["macd_hist_sign"] = np.where(macd_hist > 0, 1, -1)
df["macd_hist_accel"] = macd_hist.diff()
# ── 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, adjust=False).mean()
df["natr_14"] = df["atr_14"] / close
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
sma_20 = close.rolling(20).mean()
std_20 = close.rolling(20).std()
bb_upper = sma_20 + 2 * std_20
bb_lower = sma_20 - 2 * std_20
df["bb_width"] = (bb_upper - bb_lower) / sma_20
df["bb_position"] = (close - bb_lower) / (bb_upper - bb_lower).replace(0, np.nan)
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).mean(), 1, 0)
# ── Trend / SMA filters ──────────────────────────────────────────────────
df["sma_20"] = sma_20
df["sma_50"] = close.rolling(50).mean()
df["sma_200"] = close.rolling(200).mean()
df["price_above_sma50"] = np.where(close > df["sma_50"], 1, -1)
df["price_above_sma200"] = np.where(close > df["sma_200"], 1, -1)
df["sma50_above_sma200"] = np.where(df["sma_50"] > df["sma_200"], 1, -1)
# ── Momentum / ROC ───────────────────────────────────────────────────────
df["roc_4"] = close.pct_change(4)
df["roc_8"] = close.pct_change(8)
df["roc_16"] = close.pct_change(16)
# ── Candlestick / price structure ────────────────────────────────────────
df["body"] = (close - open_) / close
df["upper_wick"] = (high - close.clip(lower=open_)) / close
df["lower_wick"] = (open_.clip(upper=close) - low) / close
df["hl_range"] = (high - low) / close
df["gap"] = (open_ - close.shift(1)) / close.shift(1)
# ── Volume-free spread proxy ─────────────────────────────────────────────
df["spread_ratio"] = df["hl_range"] / df["atr_14"].replace(0, np.nan)
# ── Stochastic RSI proxy ─────────────────────────────────────────────────
rsi_min = df["rsi_14"].rolling(14).min()
rsi_max = df["rsi_14"].rolling(14).max()
df["stoch_rsi"] = (df["rsi_14"] - rsi_min) / (rsi_max - rsi_min).replace(0, np.nan)
# ── Williams %R (14) ─────────────────────────────────────────────────────
highest_high = high.rolling(14).max()
lowest_low = low.rolling(14).min()
df["williams_r"] = -100 * (highest_high - close) / (highest_high - lowest_low).replace(0, np.nan)
# ── Lagged features ──────────────────────────────────────────────────────
for lag in [1, 2, 3, 4]:
df[f"rsi_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
df[f"roc4_lag{lag}"] = df["roc_4"].shift(lag)
# ── Fill NaN from indicator warm-up ─────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD MACD+RSI Gradient Boosting Risk-Adjusted",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.8,
"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.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on NZD/USD 15-min data. "
"GradientBoostingClassifier chosen for strong generalisation with tabular features, "
"low learning rate + early stopping prevents overfitting. "
"SL=0.5%, TP=1.0% gives 1:2 R:R ratio. Threshold=0.55 filters marginal signals."
),
"notes": (
"Core features: RSI-14, MACD(12,26,9) with histogram momentum. "
"Supplementary: ATR, Bollinger Bands, SMA trend, Williams %R, StochRSI, "
"candlestick structure, lagged RSI/MACD/ROC. "
"reverse on opposite signal captures trend continuation. "
"target_horizon=4 bars (1 hour) aligns with typical MACD/RSI signal duration."
),
}
|
||||||||||
|
—
|
NZD/USD MACD+RSI Momentum (XGBoost, Risk-Adj)
Maximise risk-adjusted return (Sharpe/Calmar) on NZD/USD 15-min data. XGBoost chosen for its strong performance on tabular financial data. M…
|
D
@delta_one
|
NZDUSD | 15min | 63.8%53.3% | +18.16%-14.94% | 1.320.68 | 2.46%2.46% | 845135 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:51:04
# 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/NZDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── RSI 14 ──────────────────────────────────────────────────────────────
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_14_norm"] = (df["rsi_14"] - 50) / 50 # centred & scaled
df["rsi_14_ob"] = np.where(df["rsi_14"] > 70, 1, 0) # overbought flag
df["rsi_14_os"] = np.where(df["rsi_14"] < 30, 1, 0) # oversold flag
df["rsi_14_mom"] = df["rsi_14"].diff(3) # 3-bar momentum
# ── 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["macd_signal"] = signal_line
df["macd_hist"] = macd_hist
# MACD derived features
df["macd_hist_mom"] = macd_hist.diff(2) # histogram momentum
df["macd_cross_bull"] = np.where(
(macd_line > signal_line) & (macd_line.shift(1) <= signal_line.shift(1)), 1, 0
)
df["macd_cross_bear"] = np.where(
(macd_line < signal_line) & (macd_line.shift(1) >= signal_line.shift(1)), 1, 0
)
df["macd_zero_cross"] = np.where(
(macd_line > 0) & (macd_line.shift(1) <= 0), 1,
np.where((macd_line < 0) & (macd_line.shift(1) >= 0), -1, 0)
)
# ── Additional price-action features ────────────────────────────────────
# ATR (14) for 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
# Bollinger Bands (20, 2) — mean-reversion context
sma_20 = close.rolling(20).mean()
std_20 = close.rolling(20).std()
bb_up = sma_20 + 2 * std_20
bb_lo = sma_20 - 2 * std_20
df["bb_pct"] = (close - bb_lo) / (bb_up - bb_lo + 1e-12) # 0-1 position
df["bb_width"] = (bb_up - bb_lo) / sma_20 # band width
# SMA filters
df["sma_20"] = sma_20
df["sma_50"] = close.rolling(50).mean()
df["price_vs_sma20"] = (close - sma_20) / (sma_20 + 1e-12)
df["price_vs_sma50"] = (close - df["sma_50"]) / (df["sma_50"] + 1e-12)
# Rate of change
df["roc_5"] = close.pct_change(5)
df["roc_10"] = close.pct_change(10)
df["roc_20"] = close.pct_change(20)
# Candlestick body / shadow ratios
body = (close - open_).abs()
candle_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_rng
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_rng
df["bar_direction"] = np.where(close >= open_, 1, -1)
# Volume-proxy: realised range / ATR ratio
df["range_vs_atr"] = candle_rng / (atr_14 + 1e-12)
# Stochastic %K (14)
lowest_14 = low.rolling(14).min()
highest_14 = high.rolling(14).max()
stoch_k = 100 * (close - lowest_14) / (highest_14 - lowest_14 + 1e-12)
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_k.rolling(3).mean()
df["stoch_diff"] = df["stoch_k"] - df["stoch_d"]
# RSI × MACD interaction
df["rsi_macd_interact"] = df["rsi_14_norm"] * macd_hist
# Lagged features (1 and 2 bars back) for key signals
for col in ["rsi_14_norm", "macd_hist", "bb_pct", "roc_5"]:
df[f"{col}_lag1"] = df[col].shift(1)
df[f"{col}_lag2"] = df[col].shift(2)
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD MACD+RSI Momentum (XGBoost, Risk-Adj)",
"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",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return (Sharpe/Calmar) on NZD/USD 15-min data. "
"XGBoost chosen for its strong performance on tabular financial data. "
"Moderate depth (4) and high regularisation (gamma, alpha, lambda) prevent "
"overfitting on a relatively small forex dataset. Subsample + colsample_bytree "
"add stochastic diversity. SL 0.5% / TP 1.0% gives a 1:2 R:R ratio to support "
"positive expectancy even with a sub-60% win rate. Threshold 0.55 filters marginal "
"signals while keeping trade frequency acceptable. Target horizon of 4 bars (1 hour) "
"aligns with typical MACD/RSI signal resolution on 15-min charts."
),
"notes": (
"Features: RSI-14 (raw, normalised, OB/OS flags, momentum), MACD(12,26,9) "
"(line, signal, histogram, crosses, zero-cross), Bollinger Bands %B & width, "
"ATR/NATR, SMA20/50 price deviations, Stochastic %K/%D, ROC(5/10/20), "
"candlestick body/shadow ratios, RSI×MACD interaction term, and lagged "
"versions (lag1, lag2) of key signals to capture short-term persistence."
),
}
|
||||||||||
|
—
|
GBP/USD SMA Trend + Multi-Indicator XGBoost Classifier
Maximize risk-adjusted return on GBP/USD 15-min bars. Strategy combines required SMA (20/50/200) distance and cross features with ADX trend …
|
E
@elastic-moose-350
|
GBPUSD | 15min | 43.4%33.3% | +7.34%-13.07% | 1.730.27 | 2.20%2.20% | 769 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:27:43
# Model : XGBoost
# Feature Eng. : SMA (20,50,200) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/GBPUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Required SMAs and distance metrics ──────────────────────────────────
for p in [20, 50, 200]:
sma = close.rolling(p).mean()
df[f"sma_{p}"] = sma
df[f"dm_sma_{p}"] = (close - sma) / sma
# ── SMA slope (momentum of the moving average itself) ───────────────────
for p in [20, 50]:
sma = close.rolling(p).mean()
df[f"sma_{p}_slope"] = sma.diff(5) / sma.shift(5)
# ── SMA cross signals ────────────────────────────────────────────────────
sma20 = close.rolling(20).mean()
sma50 = close.rolling(50).mean()
sma200 = close.rolling(200).mean()
df["sma20_50_cross"] = (sma20 - sma50) / sma50
df["sma50_200_cross"] = (sma50 - sma200) / sma200
df["sma20_200_cross"] = (sma20 - sma200) / sma200
# ── Price momentum over multiple horizons ────────────────────────────────
for lag in [1, 3, 6, 12, 24, 48]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Volatility: rolling standard deviation of returns ───────────────────
ret1 = close.pct_change(1)
for w in [10, 20, 40]:
df[f"vol_{w}"] = ret1.rolling(w).std()
# ── ATR (Average True Range, normalised) ─────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
for w in [14, 28]:
atr = tr.ewm(span=w, adjust=False).mean()
df[f"natr_{w}"] = atr / close
# ── Bollinger Bands (20-period, 2σ) ──────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_up = bb_mid + 2 * bb_std
bb_lo = bb_mid - 2 * bb_std
bb_width = (bb_up - bb_lo) / bb_mid
df["bb_pct_b"] = (close - bb_lo) / (bb_up - bb_lo + 1e-12)
df["bb_width"] = bb_width
df["bb_squeeze"]= np.where(bb_width < bb_width.rolling(50).mean(), 1.0, 0.0)
# ── Keltner Channel (for squeeze confirmation) ───────────────────────────
kc_mid = close.ewm(span=20, adjust=False).mean()
kc_atr = tr.ewm(span=20, adjust=False).mean()
kc_up = kc_mid + 1.5 * kc_atr
kc_lo = kc_mid - 1.5 * kc_atr
df["kc_pct"] = (close - kc_lo) / (kc_up - kc_lo + 1e-12)
# ── RSI (Wilder) ─────────────────────────────────────────────────────────
def wilder_rsi(src, period):
delta = src.diff(1)
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_g = gain.ewm(alpha=1/period, adjust=False).mean()
avg_l = loss.ewm(alpha=1/period, adjust=False).mean()
rs = avg_g / (avg_l + 1e-12)
return 100 - 100 / (1 + rs)
rsi14 = wilder_rsi(close, 14)
rsi6 = wilder_rsi(close, 6)
rsi28 = wilder_rsi(close, 28)
df["rsi14"] = rsi14 / 100.0
df["rsi6"] = rsi6 / 100.0
df["rsi28"] = rsi28 / 100.0
df["rsi14_slope"] = rsi14.diff(3) / 100.0
# RSI divergence proxy: price new high/low but RSI doesn't confirm
price_high_12 = close.rolling(12).max()
price_low_12 = close.rolling(12).min()
rsi_high_12 = rsi14.rolling(12).max()
rsi_low_12 = rsi14.rolling(12).min()
df["rsi_bear_div"] = np.where(
(close >= price_high_12 * 0.999) & (rsi14 < rsi_high_12 * 0.97), 1.0, 0.0)
df["rsi_bull_div"] = np.where(
(close <= price_low_12 * 1.001) & (rsi14 > rsi_low_12 * 1.03), 1.0, 0.0)
# ── MACD ─────────────────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd = ema12 - ema26
signal = macd.ewm(span=9, adjust=False).mean()
hist = macd - signal
df["macd_norm"] = macd / close
df["macd_sig_norm"]= signal / close
df["macd_hist_norm"]= hist / close
df["macd_hist_slope"] = hist.diff(2) / close
# ── Stochastic Oscillator ─────────────────────────────────────────────────
for k_period in [14, 5]:
lo_k = low.rolling(k_period).min()
hi_k = high.rolling(k_period).max()
stoch_k = (close - lo_k) / (hi_k - lo_k + 1e-12) * 100
stoch_d = stoch_k.rolling(3).mean()
df[f"stoch_k_{k_period}"] = stoch_k / 100.0
df[f"stoch_d_{k_period}"] = stoch_d / 100.0
df[f"stoch_kd_{k_period}"] = (stoch_k - stoch_d) / 100.0
# ── Williams %R ───────────────────────────────────────────────────────────
hi14 = high.rolling(14).max()
lo14 = low.rolling(14).min()
df["williams_r"] = (hi14 - close) / (hi14 - lo14 + 1e-12)
# ── CCI (Commodity Channel Index) ────────────────────────────────────────
tp = (high + low + close) / 3.0
tp_sma = tp.rolling(20).mean()
tp_mad = tp.rolling(20).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
df["cci"] = (tp - tp_sma) / (0.015 * tp_mad + 1e-12) / 100.0
# ── Volume-like proxy: candle body and wick ratios ────────────────────────
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = (close - open_).abs() / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["bull_candle"] = np.where(close > open_, 1.0, 0.0)
# ── Mean reversion signal: z-score of close vs SMA20 ────────────────────
df["zscore_20"] = (close - sma20) / (close.rolling(20).std() + 1e-12)
df["zscore_50"] = (close - sma50) / (close.rolling(50).std() + 1e-12)
# ── Trend strength: ADX proxy ─────────────────────────────────────────────
plus_dm = (high.diff(1)).clip(lower=0)
minus_dm = (-low.diff(1)).clip(lower=0)
overlap = pd.concat([plus_dm, minus_dm], axis=1).min(axis=1)
plus_dm = plus_dm - overlap
minus_dm = minus_dm - overlap
atr14 = tr.ewm(span=14, adjust=False).mean()
plus_di = 100 * plus_dm.ewm(span=14, adjust=False).mean() / (atr14 + 1e-12)
minus_di = 100 * minus_dm.ewm(span=14, adjust=False).mean() / (atr14 + 1e-12)
dx = (plus_di - minus_di).abs() / (plus_di + minus_di + 1e-12) * 100
adx = dx.ewm(span=14, adjust=False).mean()
df["adx"] = adx / 100.0
df["plus_di"] = plus_di / 100.0
df["minus_di"] = minus_di / 100.0
df["di_diff"] = (plus_di - minus_di) / 100.0
# ── Regime detection: above/below long-term SMA ──────────────────────────
df["bull_regime"] = np.where(close > sma200, 1.0, 0.0)
df["mid_regime"] = np.where(close > sma50, 1.0, 0.0)
# ── Lag features (auto-regressive) ───────────────────────────────────────
for col, lags in [("rsi14", [1, 2, 4]), ("macd_hist_norm", [1, 2]), ("bb_pct_b", [1, 2])]:
for lag in lags:
df[f"{col}_lag{lag}"] = df[col].shift(lag)
# ── Time-of-day features ─────────────────────────────────────────────────
if hasattr(df.index, "hour"):
df["hour_sin"] = np.sin(2 * np.pi * df.index.hour / 24.0)
df["hour_cos"] = np.cos(2 * np.pi * df.index.hour / 24.0)
df["dow_sin"] = np.sin(2 * np.pi * df.index.dayofweek / 5.0)
df["dow_cos"] = np.cos(2 * np.pi * df.index.dayofweek / 5.0)
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD SMA Trend + Multi-Indicator XGBoost Classifier",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 600,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.10,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 18],
"min_atr": 0.0003,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return on GBP/USD 15-min bars. "
"Strategy combines required SMA (20/50/200) distance and cross features "
"with ADX trend strength, RSI divergence, Bollinger squeeze, Keltner, "
"MACD histogram slope, Stochastic, CCI, Williams %R, and candle-structure "
"ratios. XGBoost with strong regularisation and subsampling prevents "
"overfitting on the relatively short 1-year window. "
"Session filter 06-18 UTC keeps execution in liquid London/NY hours; "
"0.5% SL and 1.0% TP yield 1:2 R:R; sma_50 trend filter aligns trades "
"with intermediate momentum to improve win rate and Sharpe."
),
"notes": (
"Differs from prior RSI/MACD/BB/Stoch attempts by: (1) foregrounding "
"SMA cross and distance features as primary trend signals; (2) adding "
"ADX-based regime and DI differential; (3) including RSI divergence "
"proxy flags; (4) z-score mean-reversion features; (5) candle body/wick "
"structure ratios as micro-structure proxies; (6) time-of-day cyclical "
"encoding; (7) heavier regularisation (gamma, alpha, lambda) and higher "
"min_child_weight to reduce variance on the thin dataset."
),
}
|
||||||||||
|
—
|
GBP/USD SMA Trend Gradient Boosting Risk-Adj
Maximize risk-adjusted return (Sharpe/Calmar) on GBP/USD 15-min data. GradientBoostingClassifier chosen for its strong bias-variance tradeof…
|
R
@ratio_witch
|
GBPUSD | 15min | 43.1%38.5% | +6.85%-15.37% | 1.710.21 | 2.69%2.69% | 7213 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:47:56
# Model : Gradient Boosting
# Feature Eng. : SMA (20,50,200) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/GBPUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA features (required) ──────────────────────────────────────────────
for period in [20, 50, 200]:
sma = close.rolling(period).mean()
df[f"sma_{period}"] = sma
df[f"dm_sma_{period}"] = (close - sma) / sma
# ── SMA crossover signals ────────────────────────────────────────────────
sma_20 = close.rolling(20).mean()
sma_50 = close.rolling(50).mean()
sma_200 = close.rolling(200).mean()
df["sma_20_50_cross"] = np.where(sma_20 > sma_50, 1.0, -1.0)
df["sma_20_200_cross"] = np.where(sma_20 > sma_200, 1.0, -1.0)
df["sma_50_200_cross"] = np.where(sma_50 > sma_200, 1.0, -1.0)
# ── Price momentum features ──────────────────────────────────────────────
for lag in [1, 2, 4, 8, 16]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Volatility: rolling std of returns ──────────────────────────────────
ret_1 = close.pct_change(1)
for window in [8, 20, 50]:
df[f"vol_{window}"] = ret_1.rolling(window).std()
# ── ATR (Average True Range) ─────────────────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
for atr_period in [14, 50]:
atr = tr.rolling(atr_period).mean()
df[f"atr_{atr_period}"] = atr
df[f"natr_{atr_period}"] = atr / close
# ── RSI ──────────────────────────────────────────────────────────────────
for rsi_period in [14, 28]:
delta = close.diff()
gain = delta.clip(lower=0).rolling(rsi_period).mean()
loss = (-delta.clip(upper=0)).rolling(rsi_period).mean()
rs = gain / (loss + 1e-10)
df[f"rsi_{rsi_period}"] = 100 - (100 / (1 + rs))
# ── MACD ─────────────────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
df["macd_hist_norm"] = (macd_line - signal_line) / (close + 1e-10)
# ── Bollinger Bands ───────────────────────────────────────────────────────
for bb_period in [20, 50]:
bb_mid = close.rolling(bb_period).mean()
bb_std = close.rolling(bb_period).std()
bb_upper = bb_mid + 2.0 * bb_std
bb_lower = bb_mid - 2.0 * bb_std
bb_width = (bb_upper - bb_lower) / (bb_mid + 1e-10)
bb_pos = (close - bb_lower) / (bb_upper - bb_lower + 1e-10)
df[f"bb_width_{bb_period}"] = bb_width
df[f"bb_pos_{bb_period}"] = bb_pos
# ── Stochastic Oscillator ────────────────────────────────────────────────
for stoch_period in [14, 28]:
lowest_low = low.rolling(stoch_period).min()
highest_high = high.rolling(stoch_period).max()
stoch_k = (close - lowest_low) / (highest_high - lowest_low + 1e-10) * 100
stoch_d = stoch_k.rolling(3).mean()
df[f"stoch_k_{stoch_period}"] = stoch_k
df[f"stoch_d_{stoch_period}"] = stoch_d
# ── Rate of Change (ROC) ──────────────────────────────────────────────────
for roc_period in [5, 10, 20]:
df[f"roc_{roc_period}"] = close.pct_change(roc_period)
# ── Candle body and shadow features ──────────────────────────────────────
body = (close - open_).abs()
candle_range = (high - low).abs()
df["body_ratio"] = body / (candle_range + 1e-10)
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / (candle_range + 1e-10)
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / (candle_range + 1e-10)
df["bullish_candle"] = np.where(close > open_, 1.0, -1.0)
# ── Volume-proxy: candle range as volatility proxy ────────────────────────
df["range_norm"] = candle_range / (close + 1e-10)
df["range_ma_ratio"] = candle_range / (candle_range.rolling(20).mean() + 1e-10)
# ── Lag features for return predictors ───────────────────────────────────
for col_lag in ["rsi_14", "macd_hist", "bb_pos_20"]:
for lag in [1, 2, 3]:
df[f"{col_lag}_lag{lag}"] = df[col_lag].shift(lag)
# ── Distance of close from recent high/low ────────────────────────────────
for lookback in [10, 20, 50]:
roll_high = high.rolling(lookback).max()
roll_low = low.rolling(lookback).min()
df[f"dist_high_{lookback}"] = (close - roll_high) / (roll_high + 1e-10)
df[f"dist_low_{lookback}"] = (close - roll_low) / (roll_low + 1e-10)
# ── Trend strength: ADX proxy ─────────────────────────────────────────────
adx_period = 14
tr_adx = tr.copy()
plus_dm = pd.Series(np.where((high.diff() > 0) & (high.diff() > -low.diff()), high.diff(), 0.0), index=close.index)
minus_dm = pd.Series(np.where((-low.diff() > 0) & (-low.diff() > high.diff()), -low.diff(), 0.0), index=close.index)
atr_adx = tr_adx.rolling(adx_period).mean()
plus_di = 100 * plus_dm.rolling(adx_period).mean() / (atr_adx + 1e-10)
minus_di = 100 * minus_dm.rolling(adx_period).mean() / (atr_adx + 1e-10)
dx = (100 * (plus_di - minus_di).abs() / (plus_di + minus_di + 1e-10))
df["adx"] = dx.rolling(adx_period).mean()
df["plus_di"] = plus_di
df["minus_di"] = minus_di
# ── Fill NaN from indicator warm-up ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD SMA Trend Gradient Boosting Risk-Adj",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"min_samples_leaf": 20,
"max_features": "sqrt",
"n_iter_no_change": 30,
"validation_fraction": 0.1,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.57,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 18],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on GBP/USD 15-min data. "
"GradientBoostingClassifier chosen for its strong bias-variance tradeoff "
"on medium-sized tabular datasets without needing GPU. "
"Hyperparameters: moderate depth=4 prevents overfitting, learning_rate=0.04 "
"with 400 estimators balances convergence vs generalisation, subsample=0.75 "
"adds stochasticity to reduce variance, min_samples_leaf=20 enforces statistical "
"significance at each leaf. Early stopping via n_iter_no_change guards against "
"overfit on the training fold. Signal threshold 0.57 filters marginal signals "
"to improve precision. SL=0.5%, TP=1.0% gives 1:2 RR. Session filter 6-18 UTC "
"covers London+NY overlap — highest GBP/USD liquidity and tighter spreads. "
"sma_50 trend filter ensures we only trade in the direction of medium-term trend, "
"reducing whipsaw losses. target_horizon=4 bars (1 hour) gives the model enough "
"time for moves to develop while staying relevant for intraday trading."
),
"notes": (
"Features: SMA 20/50/200 with distance metrics (core requirement), RSI 14/28, "
"MACD, Bollinger Bands 20/50, Stochastic 14/28, ATR 14/50, NATR, ROC, ADX, "
"candle body/shadow ratios, lagged RSI/MACD/BB features, distance from rolling "
"high/low, SMA crossover signals, multi-lag return features. "
"All features are backward-looking only (no lookahead bias). "
"on_opposite=reverse for fast trend-following entries without missing reversals."
),
}
|
||||||||||
|
—
|
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%21.4% | +4.67%-13.74% | 1.670.53 | 1.74%1.74% | 4414 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||