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 | ||
|---|---|---|---|---|---|---|---|---|---|---|
|
—
|
USD/CAD SMA Trend + Momentum XGBoost Scalper
Maximise risk-adjusted return on USD/CAD 15-min bars. XGBoost with deep feature set (multi-period SMA distances and crossovers, RSI, MACD, B…
|
D
@delta-atlas-858
|
USDCAD | 15min | 45.6%42.9% | +3.05%-2.01% | 1.460.78 | 1.99%1.99% | 5714 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:13:41
# 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/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):
# ── SMA features (required) ──────────────────────────────────────────
for p in [20, 50, 200]:
sma = close.rolling(p).mean()
df[f"sma_{p}"] = sma
df[f"dm_sma_{p}"] = (close - sma) / sma
# ── SMA slope (momentum of the moving average itself) ────────────────
for p in [20, 50, 200]:
df[f"sma_{p}_slope"] = df[f"sma_{p}"].diff(5) / df[f"sma_{p}"].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 momentum / rate of change ──────────────────────────────────
for p in [4, 8, 16, 32]:
df[f"roc_{p}"] = close.pct_change(p)
# ── RSI (manual, no external libs) ───────────────────────────────────
def calc_rsi(series, period=14):
delta = series.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi = 100 - (100 / (1 + rs))
return rsi
for p in [9, 14, 21]:
df[f"rsi_{p}"] = calc_rsi(close, p)
df[f"rsi_{p}_norm"] = (df[f"rsi_{p}"] - 50) / 50 # centre around 0
# ── MACD (manual) ────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_signal"] = macd_signal
df["macd_hist"] = macd_line - macd_signal
df["macd_hist_chg"] = df["macd_hist"].diff()
# ── Bollinger Bands ───────────────────────────────────────────────────
for p in [20, 50]:
mid = close.rolling(p).mean()
std = close.rolling(p).std()
df[f"bb_upper_{p}"] = mid + 2 * std
df[f"bb_lower_{p}"] = mid - 2 * std
denom = (df[f"bb_upper_{p}"] - df[f"bb_lower_{p}"]).replace(0, np.nan)
df[f"bb_pct_{p}"] = (close - df[f"bb_lower_{p}"]) / denom
df[f"bb_width_{p}"] = denom / mid
# ── ATR (manual) ─────────────────────────────────────────────────────
def calc_atr(h, l, c, period=14):
prev_c = c.shift(1)
tr = pd.concat([
h - l,
(h - prev_c).abs(),
(l - prev_c).abs()
], axis=1).max(axis=1)
return tr.ewm(alpha=1.0 / period, min_periods=period, adjust=False).mean()
for p in [7, 14]:
atr = calc_atr(high, low, close, p)
df[f"atr_{p}"] = atr
df[f"natr_{p}"] = atr / close # normalised ATR
# ── 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 - np.maximum(close, open_)) / candle_rng
df["lower_wick"] = (np.minimum(close, open_) - low) / candle_rng
df["candle_dir"] = np.sign(close - open_)
# ── Rolling volatility ────────────────────────────────────────────────
log_ret = np.log(close / close.shift(1))
for p in [8, 16, 32]:
df[f"vol_{p}"] = log_ret.rolling(p).std()
# ── Volume (if available) — graceful fallback ─────────────────────────
if "volume" in df.columns and df["volume"].sum() > 0:
vol_ma = df["volume"].rolling(20).mean()
df["vol_ratio"] = df["volume"] / vol_ma.replace(0, np.nan)
else:
df["vol_ratio"] = 1.0
# ── Lagged returns ────────────────────────────────────────────────────
for lag in [1, 2, 3, 4, 8]:
df[f"ret_lag_{lag}"] = log_ret.shift(lag)
# ── Higher-timeframe SMA context (4-bar = 1h proxy) ──────────────────
close_1h = close.rolling(4).mean()
for p in [20, 50]:
sma_1h = close_1h.rolling(p).mean()
df[f"1h_dm_sma_{p}"] = (close_1h - sma_1h) / sma_1h
# ── Fill NaN from indicator warm-up ──────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "USD/CAD SMA Trend + Momentum XGBoost Scalper",
"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.2,
"reg_alpha": 0.1,
"reg_lambda": 1.5,
"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": "sma_50",
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return on USD/CAD 15-min bars. "
"XGBoost with deep feature set (multi-period SMA distances and crossovers, "
"RSI, MACD, Bollinger Bands, ATR, candle structure, lagged returns). "
"Regularised tree ensemble (gamma, L1/L2, min_child_weight) prevents "
"overfitting on the ~1-year window. 2:1 TP:SL ratio locks in positive "
"expectancy; session filter restricts trading to liquid London/NY overlap."
),
"notes": (
"SMA-trio (20/50/200) distances are the primary trend-context features. "
"MACD histogram momentum + RSI multi-period confirm entry timing. "
"ATR normalisation makes volatility features scale-invariant. "
"sma_50 trend filter ensures long trades only above 50-SMA and shorts below, "
"aligning ML signals with dominant trend and improving Sharpe ratio."
),
}
|
||||||||||
|
—
|
USD/CHF Stoch+BB+RSI Mean-Reversion (XGBoost)
Maximize risk-adjusted return (Sharpe/Calmar) on USD/CHF 15-min data. Uses Stochastic (14,3), Bollinger Bands (20,2), and RSI-14 as core fea…
|
R
@rapid-shark-854
|
USDCHF | 15min | 62.5%62.8% | +10.93%-0.48% | 1.181.02 | 4.00%4.00% | 74243 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:52:32
# 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/USDCHF_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_rsi = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=period_rsi - 1, min_periods=period_rsi).mean()
avg_loss = loss.ewm(com=period_rsi - 1, min_periods=period_rsi).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_std_val = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_std_val
bb_lower = bb_mid - bb_std * bb_std_val
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
bb_range = (bb_upper - bb_lower).replace(0, np.nan)
df["bb_pct"] = (close - bb_lower) / bb_range
# ── 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_range = (highest_high - lowest_low).replace(0, np.nan)
df["stoch_k"] = 100 * (close - lowest_low) / stoch_range
df["stoch_d"] = df["stoch_k"].rolling(stoch_d_period).mean()
df["stoch_kd_diff"] = df["stoch_k"] - df["stoch_d"]
# ── Additional derived features ──────────────────────────────────────────
# RSI momentum & zone flags
df["rsi_lag1"] = df["rsi_14"].shift(1)
df["rsi_momentum"] = df["rsi_14"] - df["rsi_lag1"]
df["rsi_oversold"] = np.where(df["rsi_14"] < 30, 1, 0)
df["rsi_overbought"] = np.where(df["rsi_14"] > 70, 1, 0)
# BB squeeze: width below rolling 20-bar median of bb_width
bb_width_median = df["bb_width"].rolling(20).median()
df["bb_squeeze"] = np.where(df["bb_width"] < bb_width_median, 1, 0)
# BB position zone
df["bb_below_lower"] = np.where(close < bb_lower, 1, 0)
df["bb_above_upper"] = np.where(close > bb_upper, 1, 0)
# Stochastic zone flags
df["stoch_oversold"] = np.where(df["stoch_k"] < 20, 1, 0)
df["stoch_overbought"] = np.where(df["stoch_k"] > 80, 1, 0)
# Price momentum (rate of change)
df["roc_4"] = close.pct_change(4)
df["roc_8"] = close.pct_change(8)
df["roc_16"] = close.pct_change(16)
# ATR (14-bar) for 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_14"] = tr.ewm(com=atr_period - 1, min_periods=atr_period).mean()
df["natr_14"] = df["atr_14"] / close
# EMA crossover signals
ema_fast = close.ewm(span=8, min_periods=8).mean()
ema_slow = close.ewm(span=21, min_periods=21).mean()
df["ema_fast"] = ema_fast
df["ema_slow"] = ema_slow
df["ema_cross"] = ema_fast - ema_slow
df["ema_cross_sign"] = np.where(df["ema_cross"] > 0, 1, -1)
# SMA 50 trend context
df["sma_50"] = close.rolling(50).mean()
df["close_vs_sma50"] = (close - df["sma_50"]) / df["sma_50"]
# Candle body and direction
df["candle_body"] = (close - open_).abs()
df["candle_range"] = (high - low).replace(0, np.nan)
df["body_ratio"] = df["candle_body"] / df["candle_range"]
df["candle_dir"] = np.where(close >= open_, 1, -1)
# Volume-proxy: range relative to rolling average range
df["rel_range"] = (high - low) / (high - low).rolling(20).mean()
# Lag features for RSI, stoch_k, bb_pct
for lag in [1, 2, 3]:
df[f"rsi_14_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"stoch_k_lag{lag}"] = df["stoch_k"].shift(lag)
df[f"bb_pct_lag{lag}"] = df["bb_pct"].shift(lag)
df[f"ema_cross_lag{lag}"] = df["ema_cross"].shift(lag)
# Divergence proxy: price making new high but RSI not
price_high_4 = close.rolling(4).max()
rsi_high_4 = df["rsi_14"].rolling(4).max()
df["bearish_div_proxy"] = np.where(
(close >= price_high_4.shift(1)) & (df["rsi_14"] < rsi_high_4.shift(1)), 1, 0
)
price_low_4 = close.rolling(4).min()
rsi_low_4 = df["rsi_14"].rolling(4).min()
df["bullish_div_proxy"] = np.where(
(close <= price_low_4.shift(1)) & (df["rsi_14"] > rsi_low_4.shift(1)), 1, 0
)
# Combined confluence signals
df["long_confluence"] = np.where(
(df["rsi_14"] < 45) & (df["stoch_k"] < 50) & (df["bb_pct"] < 0.5), 1, 0
)
df["short_confluence"] = np.where(
(df["rsi_14"] > 55) & (df["stoch_k"] > 50) & (df["bb_pct"] > 0.5), 1, 0
)
# Fill NaN from warm-up
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "USD/CHF Stoch+BB+RSI Mean-Reversion (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.8,
"colsample_bytree": 0.75,
"min_child_weight": 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.01,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 17],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on USD/CHF 15-min data. "
"Uses Stochastic (14,3), Bollinger Bands (20,2), and RSI-14 as core features "
"with confluence signals, divergence proxies, and EMA crossover context. "
"XGBoost chosen for its strong performance on tabular data with regularization "
"parameters (gamma, alpha, lambda) tuned to reduce overfitting on short date "
"ranges. SL=0.5%/TP=1.0% gives 1:2 R:R ratio. Session filter [7,17] UTC targets "
"London/NY overlap for higher-quality moves. Signal threshold 0.55 filters noise "
"while preserving trade frequency."
),
"notes": (
"Feature set combines mean-reversion indicators (RSI, Stochastic, BB percentile) "
"with trend context (EMA cross, SMA50 distance) and volatility measures (ATR, "
"BB width/squeeze). Lag features (1-3 bars) capture recent indicator momentum. "
"Bullish/bearish divergence proxies add signal quality. Shallow trees (max_depth=4) "
"with high n_estimators and slow learning rate reduce variance. Colsample and "
"subsample add stochastic regularization."
),
}
|
||||||||||
|
—
|
AUD/USD Stochastic BB Mean-Reversion (GBM)
Maximize risk-adjusted return (Sharpe/Calmar) on AUD/USD 15-min. GradientBoostingClassifier with moderate depth and learning rate chosen to …
|
P
@pivot_kid
|
AUDUSD | 15min | 64.8%60.9% | +7.88%-10.04% | 1.200.82 | 4.91%4.91% | 35887 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:24:20
# Model : Gradient Boosting
# Feature Eng. : BB (20,2.0), RSI 14, Stochastic (14,3) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_std_ = close.rolling(bb_period).std(ddof=1)
bb_upper = bb_mid + bb_std * bb_std_
bb_lower = bb_mid - bb_std * bb_std_
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
avg_loss = loss.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi"] = 100.0 - (100.0 / (1.0 + rs))
# ── Stochastic Oscillator (K=14, D=3) ────────────────────────────────────
stoch_k = 14
stoch_d = 3
low_min = low.rolling(stoch_k).min()
high_max = high.rolling(stoch_k).max()
k_raw = 100.0 * (close - low_min) / (high_max - low_min).replace(0, np.nan)
df["stoch_k"] = k_raw
df["stoch_d"] = k_raw.rolling(stoch_d).mean()
# ── ATR (14) ─────────────────────────────────────────────────────────────
atr_period = 14
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
df["atr"] = tr.ewm(com=atr_period - 1, min_periods=atr_period).mean()
df["natr"] = df["atr"] / close
# ── Trend / Momentum features ─────────────────────────────────────────────
df["sma_20"] = close.rolling(20).mean()
df["sma_50"] = close.rolling(50).mean()
df["sma_100"] = close.rolling(100).mean()
df["price_vs_sma20"] = (close - df["sma_20"]) / df["sma_20"]
df["price_vs_sma50"] = (close - df["sma_50"]) / df["sma_50"]
df["price_vs_sma100"] = (close - df["sma_100"]) / df["sma_100"]
df["sma20_vs_sma50"] = (df["sma_20"] - df["sma_50"]) / df["sma_50"]
# ── MACD (12, 26, 9) ─────────────────────────────────────────────────────
ema12 = close.ewm(span=12, min_periods=12).mean()
ema26 = close.ewm(span=26, min_periods=26).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, min_periods=9).mean()
df["macd"] = macd_line
df["macd_signal"] = macd_signal
df["macd_hist"] = macd_line - macd_signal
# ── Rate-of-Change features ───────────────────────────────────────────────
for p in [4, 8, 16]:
df[f"roc_{p}"] = close.pct_change(p)
# ── Volatility regime ────────────────────────────────────────────────────
df["vol_8"] = close.pct_change().rolling(8).std()
df["vol_20"] = close.pct_change().rolling(20).std()
df["vol_ratio"] = df["vol_8"] / df["vol_20"].replace(0, np.nan)
# ── Candle body / shadow features ────────────────────────────────────────
df["body"] = (close - open_).abs()
df["upper_shadow"] = high - pd.concat([close, open_], axis=1).max(axis=1)
df["lower_shadow"] = pd.concat([close, open_], axis=1).min(axis=1) - low
df["body_ratio"] = df["body"] / (high - low).replace(0, np.nan)
# ── RSI-derived features ──────────────────────────────────────────────────
df["rsi_above_50"] = np.where(df["rsi"] > 50, 1, 0)
df["rsi_overbought"] = np.where(df["rsi"] > 70, 1, 0)
df["rsi_oversold"] = np.where(df["rsi"] < 30, 1, 0)
df["rsi_lag1"] = df["rsi"].shift(1)
df["rsi_lag4"] = df["rsi"].shift(4)
# ── Stochastic-derived features ───────────────────────────────────────────
df["stoch_cross_up"] = np.where((df["stoch_k"] > df["stoch_d"]) &
(df["stoch_k"].shift(1) <= df["stoch_d"].shift(1)), 1, 0)
df["stoch_cross_down"] = np.where((df["stoch_k"] < df["stoch_d"]) &
(df["stoch_k"].shift(1) >= df["stoch_d"].shift(1)), 1, 0)
df["stoch_oversold"] = np.where(df["stoch_k"] < 20, 1, 0)
df["stoch_overbought"] = np.where(df["stoch_k"] > 80, 1, 0)
# ── BB-derived features ───────────────────────────────────────────────────
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).quantile(0.20), 1, 0)
df["above_bb_upper"] = np.where(close > bb_upper, 1, 0)
df["below_bb_lower"] = np.where(close < bb_lower, 1, 0)
df["bb_pct_lag1"] = df["bb_pct"].shift(1)
df["bb_pct_lag4"] = df["bb_pct"].shift(4)
# ── Session hour (UTC) ────────────────────────────────────────────────────
df["hour_utc"] = df.index.hour if hasattr(df.index, "hour") else 0
# ── Fill NaN from indicator warm-up ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD Stochastic BB Mean-Reversion (GBM)",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.80,
"min_samples_leaf": 20,
"max_features": "sqrt",
"n_iter_no_change": 30,
"validation_fraction": 0.10,
"tol": 1e-4,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": None,
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on AUD/USD 15-min. "
"GradientBoostingClassifier with moderate depth and learning rate chosen "
"to balance bias-variance. 2:1 reward-to-risk (SL=0.5%, TP=1.0%). "
"Stochastic crossovers, BB mean-reversion, and RSI regime signals "
"form the core feature set; MACD, volatility, and candle features add "
"context. Early stopping (n_iter_no_change=30) prevents overfitting."
),
"notes": (
"Features: Bollinger Bands (20,2) width/pct, RSI(14) with lag/regime flags, "
"Stochastic(14,3) K/D with crossover detection, ATR/NATR volatility, MACD "
"histogram, short/medium SMAs, ROC(4/8/16), volatility ratio, candle body "
"ratios, and UTC session hour. No session or trend filter to allow full "
"mean-reversion opportunities across all sessions."
),
}
|
||||||||||
|
—
|
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%25.8% | +5.16%-14.22% | 1.550.46 | 1.62%1.62% | 9431 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:39:22
# Model : XGBoost
# Feature Eng. : EMA (9,21), RSI 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── EMA 9 and EMA 21 ──────────────────────────────────────────────────────
ema_9 = close.ewm(span=9, adjust=False).mean()
ema_21 = close.ewm(span=21, adjust=False).mean()
df["ema_9"] = ema_9
df["ema_21"] = ema_21
df["dm_ema_9"] = (close - ema_9) / ema_9
df["dm_ema_21"] = (close - ema_21) / ema_21
# EMA cross signal: +1 when ema_9 > ema_21, -1 otherwise
df["ema_cross"] = np.where(ema_9 > ema_21, 1.0, -1.0)
# EMA cross momentum: difference normalised by ema_21
df["ema_spread"] = (ema_9 - ema_21) / ema_21
# Rate of change of EMA spread (1-bar and 3-bar)
df["ema_spread_chg1"] = df["ema_spread"].diff(1)
df["ema_spread_chg3"] = df["ema_spread"].diff(3)
# ── RSI 14 ────────────────────────────────────────────────────────────────
delta = close.diff(1)
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(alpha=1/14, min_periods=14, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/14, min_periods=14, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi_14 = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi_14
# RSI normalised to [-1, 1]
df["rsi_norm"] = (rsi_14 - 50) / 50
# RSI momentum
df["rsi_chg1"] = rsi_14.diff(1)
df["rsi_chg3"] = rsi_14.diff(3)
# RSI zone flags (overbought / oversold)
df["rsi_ob"] = np.where(rsi_14 > 70, 1.0, 0.0)
df["rsi_os"] = np.where(rsi_14 < 30, 1.0, 0.0)
# ── ATR 14 (for normalisation & volatility context) ───────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(span=14, adjust=False).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / close # normalised ATR
# ── Price momentum features ───────────────────────────────────────────────
for n in [1, 3, 5, 10, 20]:
df[f"ret_{n}"] = close.pct_change(n)
# ── Volatility: rolling std of returns ───────────────────────────────────
ret1 = close.pct_change(1)
df["vol_5"] = ret1.rolling(5).std()
df["vol_20"] = ret1.rolling(20).std()
# ── Bollinger Band features (20, 2) ───────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_up = bb_mid + 2 * bb_std
bb_lo = bb_mid - 2 * bb_std
df["bb_pct"] = (close - bb_lo) / (bb_up - bb_lo).replace(0, np.nan)
df["bb_width"] = (bb_up - bb_lo) / bb_mid
# ── MACD-style: difference of EMA12 and EMA26 ────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd = ema_12 - ema_26
signal = macd.ewm(span=9, adjust=False).mean()
df["macd"] = macd / close
df["macd_signal"] = signal / close
df["macd_hist"] = (macd - signal) / close
# ── High-Low channel position ─────────────────────────────────────────────
hh20 = high.rolling(20).max()
ll20 = low.rolling(20).min()
df["hl_pos_20"] = (close - ll20) / (hh20 - ll20).replace(0, np.nan)
hh5 = high.rolling(5).max()
ll5 = low.rolling(5).min()
df["hl_pos_5"] = (close - ll5) / (hh5 - ll5).replace(0, np.nan)
# ── Bar body & shadow features ────────────────────────────────────────────
body = (close - open_).abs()
range_ = (high - low).replace(0, np.nan)
df["body_ratio"] = body / range_
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / range_
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / range_
df["bull_bar"] = np.where(close > open_, 1.0, 0.0)
# ── Rolling correlation: EMA spread vs RSI (captures confluence) ──────────
df["corr_spread_rsi"] = df["ema_spread"].rolling(10).corr(rsi_14)
# ── Time-of-day features (hour & minute encoded cyclically) ───────────────
if hasattr(df.index, "hour"):
hour = df.index.hour
df["hour_sin"] = np.sin(2 * np.pi * hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24)
dow = df.index.dayofweek
df["dow_sin"] = np.sin(2 * np.pi * dow / 5)
df["dow_cos"] = np.cos(2 * np.pi * dow / 5)
# ── Fill NaN from indicator warm-up ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EMA Cross (9/21) + RSI Confirmation — XGBoost",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.15,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 20],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe / Calmar) on EUR/USD 15-min data. "
"XGBoost with moderate depth (4) and heavy regularisation (gamma, alpha, lambda) "
"to avoid overfitting on a 1-year window. "
"EMA cross provides trend direction; RSI filters against overbought/oversold entries. "
"Asymmetric TP/SL (2:1) boosts expectancy. Session filter restricts trading to "
"London + NY overlap (06–20 UTC) where EUR/USD liquidity is highest. "
"min_atr removes low-volatility bars where spreads erode edge."
),
"notes": (
"Features: EMA 9/21 cross & spread, RSI 14, MACD histogram, Bollinger Band %B, "
"ATR-normalised volatility, price momentum (1/3/5/10/20 bars), rolling vol, "
"high-low channel position, candlestick body/shadow ratios, cyclical time encoding. "
"Target horizon = 4 bars (1 hour ahead). Train/test split 70/30 (no leakage). "
"Cooldown = 0 because on_opposite='reverse' keeps the model always positioned "
"when high-confidence signals appear."
),
}
|
||||||||||
|
—
|
EUR/USD Gradient Boost SMA+RSI+MACD Swing
Maximize risk-adjusted return (Sharpe / Calmar) on EUR/USD 15-min. GradientBoostingClassifier chosen for robustness to noisy FX features and…
|
E
@elastic-moose-350
|
EURUSD | 15min | 47.2%38.1% | +4.52%-11.16% | 1.550.52 | 2.72%2.72% | 7221 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 03:08:55
# Model : Gradient Boosting
# Feature Eng. : SMA (20,50,200), BB (20,2.0), RSI 14, MACD (12,26,9), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA 20, 50, 200 + distance from close ─────────────────────────────
for p in [20, 50, 200]:
sma = close.rolling(p).mean()
df[f"sma_{p}"] = sma
df[f"dm_sma_{p}"] = (close - sma) / sma
# ── Bollinger Bands (20, 2) ────────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
bb_upper = bb_mid + 2.0 * bb_std
bb_lower = bb_mid - 2.0 * bb_std
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
denom = bb_upper - bb_lower
df["bb_pct"] = np.where(denom != 0, (close - bb_lower) / denom, 0.5)
# ── RSI 14 ────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=13, min_periods=14).mean()
avg_loss = loss.ewm(com=13, min_periods=14).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# ── MACD (12, 26, 9) ──────────────────────────────────────────────────
ema_fast = close.ewm(span=12, adjust=False).mean()
ema_slow = close.ewm(span=26, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line
df["macd_sig"] = signal_line
df["macd_hist"] = macd_line - signal_line
# ── ATR 14 + NATR ─────────────────────────────────────────────────────
hl = high - low
hc = (high - close.shift(1)).abs()
lc = (low - close.shift(1)).abs()
tr = pd.concat([hl, hc, lc], axis=1).max(axis=1)
atr = tr.ewm(com=13, min_periods=14).mean()
df["atr_14"] = atr
df["natr"] = atr / close
# ── Price momentum / rate-of-change ───────────────────────────────────
for p in [4, 8, 16, 32]:
df[f"roc_{p}"] = close.pct_change(p)
# ── Candle body and wick features ─────────────────────────────────────
body = (close - open_).abs()
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["body_dir"] = np.sign(close - open_)
# ── Volume-normalised (uses candle range as proxy if no volume col) ───
# Rolling z-score of close
roll_mean = close.rolling(20).mean()
roll_std = close.rolling(20).std(ddof=0).replace(0, np.nan)
df["close_zscore_20"] = (close - roll_mean) / roll_std
# ── RSI divergence proxy ──────────────────────────────────────────────
df["rsi_delta_4"] = df["rsi_14"].diff(4)
df["price_delta_4"] = close.pct_change(4)
df["rsi_price_div"] = df["rsi_delta_4"] - (df["price_delta_4"] * 100)
# ── MACD histogram slope ──────────────────────────────────────────────
df["macd_hist_slope"] = df["macd_hist"].diff(2)
# ── SMA crossover signals ─────────────────────────────────────────────
df["sma20_vs_50"] = np.where(df["sma_20"] > df["sma_50"], 1.0, -1.0)
df["sma50_vs_200"] = np.where(df["sma_50"] > df["sma_200"], 1.0, -1.0)
# ── Volatility regime (ATR percentile proxy) ──────────────────────────
atr_roll_min = atr.rolling(96).min()
atr_roll_max = atr.rolling(96).max()
atr_range = (atr_roll_max - atr_roll_min).replace(0, np.nan)
df["atr_pctile_96"] = (atr - atr_roll_min) / atr_range
# ── Stochastic oscillator %K, %D ──────────────────────────────────────
low_14 = low.rolling(14).min()
high_14 = high.rolling(14).max()
stoch_k = 100 * (close - low_14) / (high_14 - low_14).replace(0, np.nan)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_diff"] = stoch_k - stoch_d
# ── Rolling high/low breakout distance ────────────────────────────────
df["dist_hi_20"] = (high.rolling(20).max() - close) / close
df["dist_lo_20"] = (close - low.rolling(20).min()) / close
# ── Hour-of-day and day-of-week cyclical features ─────────────────────
hour = pd.Series(df.index.hour, index=df.index, dtype=float)
dow = pd.Series(df.index.dayofweek, index=df.index, dtype=float)
df["hour_sin"] = np.sin(2 * np.pi * hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24)
df["dow_sin"] = np.sin(2 * np.pi * dow / 5)
df["dow_cos"] = np.cos(2 * np.pi * dow / 5)
# ── Fill NaN from indicator warm-up ───────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD Gradient Boost SMA+RSI+MACD Swing",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"min_samples_leaf": 20,
"min_samples_split": 30,
"max_features": "sqrt",
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"tol": 1e-4,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 18],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe / Calmar) on EUR/USD 15-min. "
"GradientBoostingClassifier chosen for robustness to noisy FX features and "
"good probability calibration. Shallow trees (depth 4), high n_estimators with "
"early stopping prevent overfitting. Subsample=0.75 adds stochasticity. "
"SL=0.5%, TP=1.0% gives 1:2 R:R. Session filter 07-18 UTC captures London+NY overlap. "
"min_atr filters out flat/illiquid periods. sma_50 trend filter aligns trades with "
"medium-term momentum. Threshold 0.55 balances precision vs recall."
),
"notes": (
"Features: SMA(20,50,200) with distance ratios, BB(20,2) width+pct, RSI-14, "
"MACD(12,26,9) line/signal/hist + slope, ATR-14 + NATR, ROC(4,8,16,32), "
"candle body/wick ratios, close z-score, RSI-price divergence proxy, "
"stochastic %K/%D, rolling high/low breakout distances, "
"SMA crossover flags, ATR percentile regime, hour/DOW cyclical encodings. "
"on_opposite=reverse means a strong counter-signal immediately flips the position, "
"reducing idle time and capturing reversals within the London-NY session."
),
}
|
||||||||||
|
—
|
EUR/USD SMA Trend + Multi-Indicator GBM Scalper
Maximise risk-adjusted return (Sharpe / Calmar). GradientBoostingClassifier chosen for its strong performance on tabular financial data with…
|
S
@still-lynx-704
|
EURUSD | 15min | 39.7%40.7% | +5.29%-4.30% | 1.620.82 | 1.82%1.82% | 7327 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:35:43
# Model : Gradient Boosting
# Feature Eng. : SMA (20,50,200) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA core features (required) ──────────────────────────────────────
for period in [20, 50, 200]:
sma = close.rolling(period).mean()
df[f"sma_{period}"] = sma
df[f"dm_sma_{period}"] = (close - sma) / sma
# ── SMA slope (momentum of the moving average itself) ──────────────────
for period in [20, 50, 200]:
df[f"sma_{period}_slope"] = df[f"sma_{period}"].diff(5) / df[f"sma_{period}"].shift(5)
# ── SMA crossover signals ──────────────────────────────────────────────
df["sma_20_50_cross"] = df["sma_20"] - df["sma_50"]
df["sma_50_200_cross"] = df["sma_50"] - df["sma_200"]
df["sma_20_200_cross"] = df["sma_20"] - df["sma_200"]
# Sign of crossover difference (trend direction)
df["trend_20_50"] = np.where(df["sma_20_50_cross"] > 0, 1, -1)
df["trend_50_200"] = np.where(df["sma_50_200_cross"] > 0, 1, -1)
# ── Price momentum ────────────────────────────────────────────────────
for lag in [1, 4, 8, 16, 32]:
df[f"return_{lag}"] = close.pct_change(lag)
# ── Volatility features ───────────────────────────────────────────────
# True Range
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
for atr_period in [14, 50]:
atr = tr.rolling(atr_period).mean()
df[f"atr_{atr_period}"] = atr
df[f"natr_{atr_period}"] = atr / close
# Rolling realised volatility
log_ret = np.log(close / close.shift(1))
for vol_period in [20, 50]:
df[f"realvol_{vol_period}"] = log_ret.rolling(vol_period).std()
# ── Bollinger Bands (20, 2σ) ──────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_upper = bb_mid + 2 * bb_std
bb_lower = bb_mid - 2 * bb_std
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower + 1e-12)
df["bb_position"] = (close - bb_mid) / (bb_std + 1e-12)
# ── RSI (14) ──────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0).rolling(14).mean()
loss = (-delta.clip(upper=0)).rolling(14).mean()
rs = gain / (loss + 1e-12)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI normalised to [-1, 1]
df["rsi_14_norm"] = (df["rsi_14"] - 50) / 50
# ── MACD (12, 26, 9) ─────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
df["macd_hist_chg"] = df["macd_hist"].diff()
# Normalise MACD by price
df["macd_norm"] = df["macd"] / close
df["macd_hist_norm"] = df["macd_hist"] / close
# ── Stochastic Oscillator (14, 3) ─────────────────────────────────────
low14 = low.rolling(14).min()
high14 = high.rolling(14).max()
stoch_k = 100 * (close - low14) / (high14 - low14 + 1e-12)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_diff"] = stoch_k - stoch_d
# ── Rate-of-change ────────────────────────────────────────────────────
for roc_period in [5, 10, 20]:
df[f"roc_{roc_period}"] = (close - close.shift(roc_period)) / (close.shift(roc_period) + 1e-12)
# ── Candle body and wick features ─────────────────────────────────────
body = (close - open_).abs()
candle_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng
df["upper_wick_ratio"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_rng
df["lower_wick_ratio"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_rng
df["candle_direction"] = np.where(close >= open_, 1, -1)
# ── Time-of-day features (hour) ───────────────────────────────────────
hour = df.index.hour
df["hour_sin"] = np.sin(2 * np.pi * hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24)
# ── Lagged returns as features ────────────────────────────────────────
for lag in [1, 2, 3, 4]:
df[f"close_lag_{lag}"] = close.shift(lag)
df[f"ret_lag_{lag}"] = log_ret.shift(lag)
# ── Volume proxy: range-based ─────────────────────────────────────────
df["range_abs"] = high - low
df["range_norm"] = (high - low) / close
# ── High-Low channel position ─────────────────────────────────────────
for ch_period in [20, 50]:
ch_high = high.rolling(ch_period).max()
ch_low = low.rolling(ch_period).min()
df[f"channel_pos_{ch_period}"] = (close - ch_low) / (ch_high - ch_low + 1e-12)
# ── Fill NaN from warm-up ─────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD SMA Trend + Multi-Indicator GBM Scalper",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"max_features": "sqrt",
"min_samples_leaf": 20,
"min_samples_split": 40,
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 18],
"min_atr": None,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return (Sharpe / Calmar). "
"GradientBoostingClassifier chosen for its strong performance on "
"tabular financial data with moderate feature sets. "
"Shallow trees (max_depth=4) with high n_estimators and a low "
"learning_rate reduce overfitting. subsample=0.75 adds stochastic "
"regularisation. Early stopping via n_iter_no_change prevents "
"over-training on the validation split. "
"SL=0.5%, TP=1.0% gives a 1:2 risk-reward ratio, "
"targeting positive expectancy even at sub-60% accuracy. "
"Session filter (06-18 UTC) restricts trading to liquid hours "
"covering London and New York overlap for EUR/USD. "
"SMA-50 trend filter ensures trades align with the medium-term "
"trend, reducing counter-trend noise."
),
"notes": (
"Features include required SMA(20,50,200) distances and crossovers, "
"RSI-14, MACD histogram, Bollinger Band position, Stochastic, ATR, "
"realised volatility, rate-of-change, candle structure ratios, "
"channel position, lagged returns, and cyclical time encoding. "
"Target horizon of 4 bars (1 hour) on 15-min data balances "
"signal frequency with meaningful directional moves."
),
}
|
||||||||||
|
—
|
USD/CAD Momentum-Reversion Hybrid (XGBoost, v2)
Maximise risk-adjusted return (Sharpe/Calmar). Deeper ensemble (600 trees) with aggressive regularisation (reg_alpha=0.5, reg_lambda=2, gamm…
|
P
@pivot_kid
|
USDCAD | 15min | 61.8%57.6% | +6.05%-0.83% | 1.310.98 | 2.07%2.07% | 47485 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:37:19
# 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/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):
# ── SMA & distance features ──────────────────────────────────────────────
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 (rate of change of SMA over 5 bars)
for p in [20, 50]:
sma = df[f"sma_{p}"]
df[f"sma_{p}_slope"] = sma.diff(5) / sma.shift(5)
# SMA cross signals
df["sma_20_50_cross"] = np.where(df["sma_20"] > df["sma_50"], 1.0, -1.0)
df["sma_50_200_cross"] = np.where(df["sma_50"] > df["sma_200"], 1.0, -1.0)
# ── Bollinger Bands ───────────────────────────────────────────────────────
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
df["bb_mid"] = bb_mid
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
bb_range = bb_upper - bb_lower
df["bb_pct"] = np.where(bb_range != 0, (close - bb_lower) / bb_range, 0.5)
# Bollinger Band squeeze: width vs its own 20-bar average
df["bb_squeeze"] = df["bb_width"] / df["bb_width"].rolling(20).mean()
# Price position relative to bands
df["bb_above_upper"] = np.where(close > bb_upper, 1.0, 0.0)
df["bb_below_lower"] = np.where(close < bb_lower, 1.0, 0.0)
# ── RSI ───────────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(alpha=1/14, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/14, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI derived features
df["rsi_norm"] = (df["rsi_14"] - 50) / 50 # centred & scaled
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_slope"] = df["rsi_14"].diff(3)
# RSI divergence proxy: price up but RSI down (5-bar)
price_chg_5 = close.diff(5)
rsi_chg_5 = df["rsi_14"].diff(5)
df["rsi_bear_div"] = np.where((price_chg_5 > 0) & (rsi_chg_5 < 0), 1.0, 0.0)
df["rsi_bull_div"] = np.where((price_chg_5 < 0) & (rsi_chg_5 > 0), 1.0, 0.0)
# ── MACD ──────────────────────────────────────────────────────────────────
ema_fast = close.ewm(span=12, adjust=False).mean()
ema_slow = close.ewm(span=26, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
# MACD normalised by close price
df["macd_line_norm"] = macd_line / close
df["macd_hist_norm"] = df["macd_hist"] / close
# MACD histogram slope and sign change
df["macd_hist_slope"] = df["macd_hist"].diff(2)
df["macd_cross"] = np.where(macd_line > signal_line, 1.0, -1.0)
# ── ATR ───────────────────────────────────────────────────────────────────
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(alpha=1/14, adjust=False).mean()
df["natr"] = df["atr_14"] / close
# ATR regime: current ATR vs 50-bar rolling mean
df["atr_regime"] = df["atr_14"] / df["atr_14"].rolling(50).mean()
# ── Momentum / Price Action features ─────────────────────────────────────
# Returns at multiple horizons
for h in [1, 2, 4, 8, 16]:
df[f"ret_{h}"] = close.pct_change(h)
# Candle body & shadow
body = (close - open_).abs()
total_range = (high - low).replace(0, np.nan)
df["body_ratio"] = body / total_range
df["candle_dir"] = np.where(close >= open_, 1.0, -1.0)
upper_shadow = high - pd.concat([close, open_], axis=1).max(axis=1)
lower_shadow = pd.concat([close, open_], axis=1).min(axis=1) - low
df["upper_shadow_ratio"] = upper_shadow / total_range
df["lower_shadow_ratio"] = lower_shadow / total_range
# Rolling price z-score (mean reversion signal)
for w in [20, 50]:
roll_mean = close.rolling(w).mean()
roll_std = close.rolling(w).std().replace(0, np.nan)
df[f"zscore_{w}"] = (close - roll_mean) / roll_std
# Volume of volatility: rolling std of returns
df["vol_10"] = close.pct_change().rolling(10).std()
df["vol_20"] = close.pct_change().rolling(20).std()
# Efficiency ratio: directional move / path length (20 bars)
direction_move = (close - close.shift(20)).abs()
path_length = close.diff().abs().rolling(20).sum().replace(0, np.nan)
df["efficiency_ratio"] = direction_move / path_length
# ── Interaction / Cross features ─────────────────────────────────────────
# RSI × MACD hist — captures momentum agreement
df["rsi_macd_agree"] = df["rsi_norm"] * df["macd_hist_norm"]
# BB pct × RSI — oversold/overbought near bands
df["bb_rsi_interact"] = df["bb_pct"] * df["rsi_norm"]
# Trend strength: distance from SMA50 scaled by ATR
df["trend_atr_50"] = df["dm_sma_50"] / df["natr"].replace(0, np.nan)
# ── Session / Time features ───────────────────────────────────────────────
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)
# London session flag
df["london_session"] = np.where((hour >= 7) & (hour < 16), 1.0, 0.0)
# NY session flag
df["ny_session"] = np.where((hour >= 13) & (hour < 21), 1.0, 0.0)
if hasattr(df.index, "dayofweek"):
dow = df.index.dayofweek
df["dow_sin"] = np.sin(2 * np.pi * dow / 5)
df["dow_cos"] = np.cos(2 * np.pi * dow / 5)
# ── Fill NaN from warm-up ─────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "USD/CAD Momentum-Reversion Hybrid (XGBoost, v2)",
"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.2,
"reg_alpha": 0.5,
"reg_lambda": 2.0,
"objective": "binary:logistic",
"tree_method": "hist",
"n_jobs": -1,
"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": [7, 21],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return (Sharpe/Calmar). "
"Deeper ensemble (600 trees) with aggressive regularisation "
"(reg_alpha=0.5, reg_lambda=2, gamma=0.2, min_child_weight=5) "
"to prevent overfitting on 15-min USDCAD. "
"Rich feature set adds z-scores, efficiency ratio, session dummies, "
"RSI divergence, candle shape and cross-indicator interactions "
"beyond the prior attempt's plain indicators. "
"0.5% SL / 1.0% TP gives 1:2 R:R; session filter restricts to "
"liquid London+NY overlap hours."
),
"notes": (
"Prior attempt used plain RSI/MACD/BB/ATR/SMA and scored PF=0.98. "
"This version adds: rolling z-scores (20,50), efficiency ratio, "
"candle body/shadow ratios, multi-horizon returns, ATR regime, "
"BB squeeze, RSI divergence proxies, time-of-day sin/cos encoding, "
"and interaction terms (rsi_macd_agree, bb_rsi_interact, trend_atr). "
"Model regularised more heavily to combat the short date range. "
"Signal threshold lifted slightly to 0.54 to reduce marginal trades."
),
}
|
||||||||||
|
—
|
GBP/USD RSI-MACD Momentum + Volatility Regime XGBoost
Maximize risk-adjusted return (Sharpe/Calmar) by combining RSI momentum divergence, MACD histogram dynamics, Bollinger squeeze, Stochastic c…
|
S
@still-lynx-704
|
GBPUSD | 15min | 54.1%52.1% | +0.11%-6.94% | 1.010.87 | 3.34%3.34% | 37948 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:23:23
# 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/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):
# --- RSI 14 ---
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=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 + 1e-12)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI derived features
df["rsi_zscore"] = (df["rsi_14"] - df["rsi_14"].rolling(50).mean()) / (df["rsi_14"].rolling(50).std() + 1e-12)
df["rsi_slope"] = df["rsi_14"].diff(3)
df["rsi_above_50"] = np.where(df["rsi_14"] > 50, 1, 0)
df["rsi_overbought"] = np.where(df["rsi_14"] > 70, 1, 0)
df["rsi_oversold"] = np.where(df["rsi_14"] < 30, 1, 0)
# RSI divergence proxy: price direction vs RSI direction
price_dir_3 = np.sign(close.diff(3))
rsi_dir_3 = np.sign(df["rsi_14"].diff(3))
df["rsi_divergence"] = np.where(price_dir_3 != rsi_dir_3, 1, 0)
# --- MACD (12, 26, 9) ---
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
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_slope"] = macd_hist.diff(2)
df["macd_cross_up"] = np.where((macd_line > signal_line) & (macd_line.shift(1) <= signal_line.shift(1)), 1, 0)
df["macd_cross_dn"] = np.where((macd_line < signal_line) & (macd_line.shift(1) >= signal_line.shift(1)), 1, 0)
df["macd_hist_positive"] = np.where(macd_hist > 0, 1, 0)
df["macd_hist_expanding"] = np.where(macd_hist.abs() > macd_hist.abs().shift(1), 1, 0)
df["macd_normalized"] = macd_line / (close + 1e-12)
# --- ATR 14 ---
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)
atr14 = tr.ewm(com=13, min_periods=14).mean()
df["atr_14"] = atr14
df["natr_14"] = atr14 / (close + 1e-12)
# ATR regime: high vs low volatility
atr_ma = atr14.rolling(50).mean()
df["atr_high_vol"] = np.where(atr14 > atr_ma * 1.2, 1, 0)
df["atr_low_vol"] = np.where(atr14 < atr_ma * 0.8, 1, 0)
# --- Bollinger Bands (20, 2) ---
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_upper = bb_mid + 2 * bb_std
bb_lower = bb_mid - 2 * bb_std
df["bb_pct_b"] = (close - bb_lower) / (bb_upper - bb_lower + 1e-12)
df["bb_width"] = (bb_upper - bb_lower) / (bb_mid + 1e-12)
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).quantile(0.2), 1, 0)
df["bb_upper_touch"] = np.where(close >= bb_upper * 0.999, 1, 0)
df["bb_lower_touch"] = np.where(close <= bb_lower * 1.001, 1, 0)
# --- Keltner Channel (20, 1.5x ATR) ---
kc_mid = close.ewm(span=20, adjust=False).mean()
kc_upper = kc_mid + 1.5 * atr14
kc_lower = kc_mid - 1.5 * atr14
df["kc_pct"] = (close - kc_lower) / (kc_upper - kc_lower + 1e-12)
# Squeeze: BB inside KC
df["kc_bb_squeeze"] = np.where((bb_upper < kc_upper) & (bb_lower > kc_lower), 1, 0)
# --- Volume-like proxy: bar range & body ---
bar_range = high - low
bar_body = (close - open_).abs()
df["range_norm"] = bar_range / (atr14 + 1e-12)
df["body_ratio"] = bar_body / (bar_range + 1e-12)
df["close_position"] = (close - low) / (bar_range + 1e-12)
df["bullish_bar"] = np.where(close > open_, 1, 0)
# --- Momentum & ROC ---
df["roc_5"] = close.pct_change(5)
df["roc_10"] = close.pct_change(10)
df["roc_20"] = close.pct_change(20)
df["momentum_10"] = close - close.shift(10)
df["momentum_20"] = close - close.shift(20)
# --- Moving Average features ---
ema8 = close.ewm(span=8, adjust=False).mean()
ema21 = close.ewm(span=21, adjust=False).mean()
ema50 = close.ewm(span=50, adjust=False).mean()
sma20 = close.rolling(20).mean()
sma50 = close.rolling(50).mean()
sma100 = close.rolling(100).mean()
df["ema8_21_gap"] = (ema8 - ema21) / (close + 1e-12)
df["ema21_50_gap"] = (ema21 - ema50) / (close + 1e-12)
df["price_vs_ema50"] = (close - ema50) / (close + 1e-12)
df["price_vs_sma20"] = (close - sma20) / (close + 1e-12)
df["price_vs_sma100"] = (close - sma100) / (close + 1e-12)
df["ema8_slope"] = ema8.diff(3) / (close + 1e-12)
df["ema21_slope"] = ema21.diff(3) / (close + 1e-12)
df["ema8_above_ema21"] = np.where(ema8 > ema21, 1, 0)
df["ema21_above_ema50"] = np.where(ema21 > ema50, 1, 0)
df["triple_ma_align_bull"] = np.where((ema8 > ema21) & (ema21 > ema50), 1, 0)
df["triple_ma_align_bear"] = np.where((ema8 < ema21) & (ema21 < ema50), 1, 0)
# --- Stochastic %K %D (14, 3) ---
lowest_low_14 = low.rolling(14).min()
highest_high_14 = high.rolling(14).max()
stoch_k = 100 * (close - lowest_low_14) / (highest_high_14 - lowest_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
df["stoch_overbought"] = np.where(stoch_k > 80, 1, 0)
df["stoch_oversold"] = np.where(stoch_k < 20, 1, 0)
df["stoch_cross_up"] = np.where((stoch_k > stoch_d) & (stoch_k.shift(1) <= stoch_d.shift(1)), 1, 0)
df["stoch_cross_dn"] = np.where((stoch_k < stoch_d) & (stoch_k.shift(1) >= stoch_d.shift(1)), 1, 0)
# --- Williams %R (14) ---
df["willr_14"] = -100 * (highest_high_14 - close) / (highest_high_14 - lowest_low_14 + 1e-12)
# --- CCI (20) ---
tp = (high + low + close) / 3
tp_ma = tp.rolling(20).mean()
tp_mad = tp.rolling(20).apply(lambda x: np.mean(np.abs(x - np.mean(x))), raw=True)
df["cci_20"] = (tp - tp_ma) / (0.015 * tp_mad + 1e-12)
df["cci_above_zero"] = np.where(df["cci_20"] > 0, 1, 0)
df["cci_extreme_bull"] = np.where(df["cci_20"] > 100, 1, 0)
df["cci_extreme_bear"] = np.where(df["cci_20"] < -100, 1, 0)
# --- Donchian Channel (20) ---
don_high = high.rolling(20).max()
don_low = low.rolling(20).min()
df["donchian_pct"] = (close - don_low) / (don_high - don_low + 1e-12)
df["donchian_breakout_up"] = np.where(close >= high.rolling(20).max().shift(1), 1, 0)
df["donchian_breakout_dn"] = np.where(close <= low.rolling(20).min().shift(1), 1, 0)
# --- Price pattern features ---
df["higher_high"] = np.where((high > high.shift(1)) & (high.shift(1) > high.shift(2)), 1, 0)
df["lower_low"] = np.where((low < low.shift(1)) & (low.shift(1) < low.shift(2)), 1, 0)
df["inside_bar"] = np.where((high < high.shift(1)) & (low > low.shift(1)), 1, 0)
df["outside_bar"] = np.where((high > high.shift(1)) & (low < low.shift(1)), 1, 0)
# --- Lag features for key indicators ---
for lag in [1, 2, 3, 4]:
df[f"rsi_14_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
df[f"bb_pct_b_lag{lag}"] = df["bb_pct_b"].shift(lag)
# --- Interaction features (avoiding lookahead) ---
df["rsi_macd_bull"] = np.where((df["rsi_14"] > 50) & (df["macd_hist"] > 0), 1, 0)
df["rsi_macd_bear"] = np.where((df["rsi_14"] < 50) & (df["macd_hist"] < 0), 1, 0)
df["rsi_bb_oversold_bounce"] = np.where((df["rsi_14"] < 35) & (df["bb_pct_b"] < 0.2), 1, 0)
df["rsi_bb_overbought_fade"] = np.where((df["rsi_14"] > 65) & (df["bb_pct_b"] > 0.8), 1, 0)
df["triple_bull"] = np.where(
(df["rsi_14"] > 50) & (df["macd_hist"] > 0) & (df["stoch_k"] > 50), 1, 0
)
df["triple_bear"] = np.where(
(df["rsi_14"] < 50) & (df["macd_hist"] < 0) & (df["stoch_k"] < 50), 1, 0
)
# --- Volatility regime ---
realized_vol = close.pct_change().rolling(20).std() * np.sqrt(96)
df["realized_vol_20"] = realized_vol
df["vol_regime_high"] = np.where(realized_vol > realized_vol.rolling(100).median(), 1, 0)
# --- Session-aware time features ---
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["london_session"] = np.where((df.index.hour >= 7) & (df.index.hour < 16), 1, 0)
df["ny_session"] = np.where((df.index.hour >= 13) & (df.index.hour < 21), 1, 0)
df["overlap_session"] = np.where((df.index.hour >= 13) & (df.index.hour < 16), 1, 0)
df["asian_session"] = np.where((df.index.hour >= 0) & (df.index.hour < 7), 1, 0)
df["day_of_week"] = df.index.dayofweek
df["dow_sin"] = np.sin(2 * np.pi * df["day_of_week"] / 5)
df["dow_cos"] = np.cos(2 * np.pi * df["day_of_week"] / 5)
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD RSI-MACD Momentum + Volatility Regime XGBoost",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 500,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.75,
"colsample_bytree": 0.65,
"min_child_weight": 5,
"gamma": 0.15,
"reg_alpha": 0.3,
"reg_lambda": 1.5,
"scale_pos_weight": 1,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 21],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) by combining RSI momentum "
"divergence, MACD histogram dynamics, Bollinger squeeze, Stochastic crossovers, "
"volatility regime, and session-aware time features. XGBoost with moderate depth "
"and strong regularization prevents overfitting on 15-min GBP/USD data. "
"Signal threshold 0.56 filters weak signals, SL/TP at 0.5%/1.0% gives 1:2 RR."
),
"notes": (
"Differentiating from prior attempts (PF=1.08) by: (1) adding Keltner Channel "
"squeeze interaction with Bollinger, (2) CCI and Williams %R as confirmation, "
"Donchian breakout detection, (3) session-aware features (London/NY/overlap), "
"(4) richer MACD/RSI interaction flags, (5) realized volatility regime, "
"(6) stronger XGBoost regularization (alpha=0.3, lambda=1.5, min_child=5) "
"to reduce false signals in choppy GBP/USD conditions."
),
}
|
||||||||||
|
—
|
AUD/USD Stoch+BB+RSI Mean-Reversion XGBoost
Maximize risk-adjusted return (Sharpe / Calmar). XGBoost chosen for its ability to capture non-linear interactions between Stochastic, Bolli…
|
S
@still-lynx-704
|
AUDUSD | 15min | 62.5%62.5% | +10.93%-3.52% | 1.180.94 | 4.00%4.00% | 74288 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:51:31
# Model : XGBoost
# Feature Eng. : BB (20,2.0), RSI 14, Stochastic (14,3) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_std_val = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_std_val
bb_lower = bb_mid - bb_std * bb_std_val
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
avg_loss = loss.ewm(com=rsi_period - 1, min_periods=rsi_period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi"] = 100 - (100 / (1 + rs))
# ── Stochastic Oscillator (K=14, D=3) ────────────────────────────────────
stoch_k_period = 14
stoch_d_period = 3
lowest_low = low.rolling(stoch_k_period).min()
highest_high = high.rolling(stoch_k_period).max()
denom = (highest_high - lowest_low).replace(0, np.nan)
df["stoch_k"] = 100 * (close - lowest_low) / denom
df["stoch_d"] = df["stoch_k"].rolling(stoch_d_period).mean()
df["stoch_kd_diff"] = df["stoch_k"] - df["stoch_d"]
# ── Additional derived features ──────────────────────────────────────────
# RSI overbought / oversold zone flags
df["rsi_ob"] = np.where(df["rsi"] > 70, 1, 0)
df["rsi_os"] = np.where(df["rsi"] < 30, 1, 0)
df["rsi_mid"] = df["rsi"] - 50.0
# Stochastic overbought / oversold zone flags
df["stoch_ob"] = np.where(df["stoch_k"] > 80, 1, 0)
df["stoch_os"] = np.where(df["stoch_k"] < 20, 1, 0)
# BB position regime: price relative to bands
df["price_above_bb_upper"] = np.where(close > bb_upper, 1, 0)
df["price_below_bb_lower"] = np.where(close < bb_lower, 1, 0)
df["price_vs_bb_mid"] = close - bb_mid
# ATR-based volatility (14-bar)
atr_period = 14
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
df["atr14"] = tr.ewm(com=atr_period - 1, min_periods=atr_period).mean()
df["natr14"] = df["atr14"] / close
# SMA trend context
df["sma_20"] = close.rolling(20).mean()
df["sma_50"] = close.rolling(50).mean()
df["sma_200"] = close.rolling(200).mean()
df["price_vs_sma20"] = (close - df["sma_20"]) / df["sma_20"]
df["price_vs_sma50"] = (close - df["sma_50"]) / df["sma_50"]
df["sma20_vs_sma50"] = (df["sma_20"] - df["sma_50"]) / df["sma_50"]
# Momentum: rate of change
df["roc_5"] = close.pct_change(5)
df["roc_10"] = close.pct_change(10)
df["roc_20"] = close.pct_change(20)
# MACD-style (EMA 12 - EMA 26)
ema12 = close.ewm(span=12, min_periods=12).mean()
ema26 = close.ewm(span=26, min_periods=26).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, min_periods=9).mean()
df["macd"] = macd_line
df["macd_signal"] = macd_signal
df["macd_hist"] = macd_line - macd_signal
# Candle body / wick ratios
body = (close - open_).abs()
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["bullish_bar"] = np.where(close > open_, 1, 0)
# Lagged RSI / Stoch features (1 and 2 bars back)
df["rsi_lag1"] = df["rsi"].shift(1)
df["rsi_lag2"] = df["rsi"].shift(2)
df["stoch_k_lag1"] = df["stoch_k"].shift(1)
df["bb_pct_lag1"] = df["bb_pct"].shift(1)
# RSI slope
df["rsi_slope"] = df["rsi"] - df["rsi"].shift(3)
# Stoch K crossing D (momentum signal)
df["stoch_cross_up"] = np.where((df["stoch_k"] > df["stoch_d"]) &
(df["stoch_k"].shift(1) <= df["stoch_d"].shift(1)), 1, 0)
df["stoch_cross_down"] = np.where((df["stoch_k"] < df["stoch_d"]) &
(df["stoch_k"].shift(1) >= df["stoch_d"].shift(1)), 1, 0)
# Volume (if present)
if "volume" in df.columns:
vol_ma = df["volume"].rolling(20).mean()
df["vol_ratio"] = df["volume"] / vol_ma.replace(0, np.nan)
else:
df["vol_ratio"] = 1.0
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD Stoch+BB+RSI Mean-Reversion XGBoost",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.15,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe / Calmar). "
"XGBoost chosen for its ability to capture non-linear interactions "
"between Stochastic, Bollinger Bands, and RSI regimes. "
"Shallow trees (max_depth=4) + high regularisation (reg_lambda=1.5, gamma=0.15) "
"prevent overfitting on 15-min FX data. "
"2:1 TP:SL ratio (1.0% / 0.5%) improves expectancy per trade. "
"Reverse on opposite signal minimises flat time and captures regime flips."
),
"notes": (
"Features include BB width/pct, RSI(14) with overbought/oversold flags, "
"Stochastic K/D crossovers, MACD histogram, ATR volatility, SMA trend context, "
"candle body ratios, lagged indicators, and momentum ROC. "
"signal_threshold=0.55 balances precision vs recall on directional calls. "
"session_filter covers full 24h to capture Asia + London + NY sessions for AUD/USD."
),
}
|
||||||||||
|
—
|
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%52.9% | +19.51%-7.26% | 1.360.84 | 2.51%2.51% | 74585 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||
|
—
|
EUR/USD EMA Cross + ATR Momentum (XGBoost)
Maximize risk-adjusted return (Sharpe) by combining EMA crossover trend regime with ATR-normalised volatility, RSI, MACD, and Bollinger feat…
|
R
@rapid-shark-854
|
EURUSD | 15min | 44.1%40.0% | +4.76%-7.50% | 1.590.71 | 2.55%2.55% | 6825 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:19:50
# Model : XGBoost
# Feature Eng. : EMA (50,200), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── EMA 50 / 200 and distance features ──────────────────────────────
ema_50 = close.ewm(span=50, adjust=False).mean()
ema_200 = close.ewm(span=200, adjust=False).mean()
df["ema_50"] = ema_50
df["ema_200"] = ema_200
df["dm_ema_50"] = (close - ema_50) / ema_50
df["dm_ema_200"] = (close - ema_200) / ema_200
# EMA cross signal: +1 when ema_50 > ema_200, -1 otherwise
df["ema_cross"] = np.where(ema_50 > ema_200, 1.0, -1.0)
# Spread between the two EMAs, normalised by price
df["ema_spread"] = (ema_50 - ema_200) / close
# Rate of change of ema_spread (momentum of the cross)
df["ema_spread_roc"] = df["ema_spread"].diff(4)
# ── ATR 14 & NATR ───────────────────────────────────────────────────
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(span=14, adjust=False).mean()
natr = atr / close
df["atr"] = atr
df["natr"] = natr
# ── RSI 14 ───────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(span=14, adjust=False).mean()
avg_loss = loss.ewm(span=14, adjust=False).mean()
rs = avg_gain / (avg_loss + 1e-12)
rsi = 100.0 - (100.0 / (1.0 + rs))
df["rsi_14"] = rsi
# Normalised RSI centred around 0
df["rsi_norm"] = (rsi - 50.0) / 50.0
# ── MACD (12/26/9) ───────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd = ema_12 - ema_26
signal = macd.ewm(span=9, adjust=False).mean()
df["macd"] = macd / close
df["macd_signal"] = signal / close
df["macd_hist"] = (macd - signal) / close
# ── Bollinger Bands (20, 2σ) ─────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
bb_up = bb_mid + 2.0 * bb_std
bb_lo = bb_mid - 2.0 * bb_std
df["bb_width"] = (bb_up - bb_lo) / (bb_mid + 1e-12)
df["bb_pct"] = (close - bb_lo) / (bb_up - bb_lo + 1e-12)
# ── Price momentum (log-returns at multiple horizons) ────────────────
for lag in [1, 4, 8, 16]:
df[f"logret_{lag}"] = np.log(close / close.shift(lag))
# ── Volume-less volatility proxy: high-low range / ATR ───────────────
df["hl_range_norm"] = (high - low) / (atr + 1e-12)
# ── Candle body and shadow ratios ────────────────────────────────────
body = (close - open_).abs()
range_ = (high - low + 1e-12)
df["body_ratio"] = body / range_
df["upper_shadow_ratio"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / range_
df["lower_shadow_ratio"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / range_
# ── Trend strength: close relative to recent N-bar high/low ──────────
for window in [20, 50]:
roll_hi = high.rolling(window).max()
roll_lo = low.rolling(window).min()
denom = (roll_hi - roll_lo + 1e-12)
df[f"close_rank_{window}"] = (close - roll_lo) / denom
# ── EMA 50 slope (rate of change) ────────────────────────────────────
df["ema_50_slope"] = ema_50.diff(4) / (close + 1e-12)
df["ema_200_slope"] = ema_200.diff(8) / (close + 1e-12)
# ── Hour-of-day and day-of-week as cyclic features ───────────────────
if hasattr(df.index, "hour"):
hour = df.index.hour
dow = df.index.dayofweek
df["hour_sin"] = np.sin(2.0 * np.pi * hour / 24.0)
df["hour_cos"] = np.cos(2.0 * np.pi * hour / 24.0)
df["dow_sin"] = np.sin(2.0 * np.pi * dow / 5.0)
df["dow_cos"] = np.cos(2.0 * np.pi * dow / 5.0)
# ── Fill NaNs from indicator warm-up ────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EUR/USD EMA Cross + ATR Momentum (XGBoost)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.75,
"min_child_weight": 3,
"gamma": 0.1,
"reg_alpha": 0.05,
"reg_lambda": 1.5,
"objective": "binary:logistic",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 20],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe) by combining EMA crossover "
"trend regime with ATR-normalised volatility, RSI, MACD, and Bollinger "
"features. XGBoost hyperparameters tuned for bias-variance balance: "
"moderate depth (4), aggressive shrinkage (lr=0.04), column/row "
"subsampling, and L1/L2 regularisation. SL=0.5% / TP=1.0% targets a "
"2:1 reward-risk ratio. Session filter [6,20] UTC focuses on the "
"liquid London/NY overlap. min_atr filters dead markets."
),
"notes": (
"EMA 50/200 cross provides the macro trend regime; distance features "
"capture how far price has stretched from trend. ATR/NATR quantifies "
"volatility regime. RSI, MACD histogram, and BB %b add mean-reversion "
"and momentum context. Candle body ratios encode micro-structure. "
"Cyclic time features allow the model to learn intraday seasonality. "
"target_horizon=4 bars (1 hour on 15-min data) balances trade frequency "
"against predictability. on_opposite=reverse reduces idle time and "
"captures trend continuation efficiently."
),
}
|
||||||||||
|
—
|
EMA Cross 9/21 + RSI14 Gradient Boost Scalper
Maximize risk-adjusted return (Sharpe/Calmar) using a GradientBoostingClassifier with EMA 9/21 crossover as the primary signal source and RS…
|
P
@pivot_kid
|
EURUSD | 15min | 43.3%23.5% | +11.79%-12.10% | 2.920.42 | 0.83%0.83% | 6717 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:37:57
# Model : Gradient Boosting
# Feature Eng. : EMA (9,21), RSI 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/EURUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# --- EMA 9 and EMA 21 (required) ---
ema_9 = close.ewm(span=9, adjust=False).mean()
ema_21 = close.ewm(span=21, adjust=False).mean()
df["ema_9"] = ema_9
df["ema_21"] = ema_21
df["dm_ema_9"] = (close - ema_9) / ema_9
df["dm_ema_21"] = (close - ema_21) / ema_21
# EMA crossover signal: positive when fast > slow
df["ema_cross"] = ema_9 - ema_21
# Crossover direction change (sign flip)
df["ema_cross_signal"] = np.sign(df["ema_cross"])
df["ema_cross_prev"] = df["ema_cross_signal"].shift(1)
df["ema_cross_flip"] = (df["ema_cross_signal"] != df["ema_cross_prev"]).astype(float)
# --- RSI 14 (required) ---
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=13, adjust=False).mean()
avg_loss = loss.ewm(com=13, adjust=False).mean()
rs = avg_gain / (avg_loss + 1e-10)
rsi_14 = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi_14
# RSI normalised to [-1, 1] range
df["rsi_norm"] = (rsi_14 - 50) / 50
# RSI overbought/oversold flags
df["rsi_ob"] = np.where(rsi_14 > 70, 1.0, 0.0)
df["rsi_os"] = np.where(rsi_14 < 30, 1.0, 0.0)
# --- Additional momentum and volatility features ---
# EMA 50 for trend context
ema_50 = close.ewm(span=50, adjust=False).mean()
df["ema_50"] = ema_50
df["dm_ema_50"] = (close - ema_50) / ema_50
# Price momentum: rate of change over multiple horizons
df["roc_4"] = close.pct_change(4)
df["roc_8"] = close.pct_change(8)
df["roc_16"] = close.pct_change(16)
# ATR (Average True Range) for volatility
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(span=14, adjust=False).mean()
df["atr_14"] = atr_14
# Normalised ATR
df["natr_14"] = atr_14 / close
# Bollinger Bands (20-period, 2 std)
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_upper = bb_mid + 2 * bb_std
bb_lower = bb_mid - 2 * bb_std
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower + 1e-10)
df["bb_width"] = (bb_upper - bb_lower) / (bb_mid + 1e-10)
# BB squeeze: narrow bands signal potential breakout
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).mean(), 1.0, 0.0)
# MACD-like: difference between two EMAs
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
df["macd_line"] = macd_line / (close + 1e-10)
df["macd_signal"] = macd_signal / (close + 1e-10)
df["macd_hist"] = (macd_line - macd_signal) / (close + 1e-10)
df["macd_cross"] = np.sign(macd_line - macd_signal)
# Stochastic oscillator (14-period)
lowest_low = low.rolling(14).min()
highest_high = high.rolling(14).max()
stoch_k = 100 * (close - lowest_low) / (highest_high - lowest_low + 1e-10)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_diff"] = stoch_k - stoch_d
# Volume of price movement (candle body and shadows)
df["body"] = (close - open_).abs() / (atr_14 + 1e-10)
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / (atr_14 + 1e-10)
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / (atr_14 + 1e-10)
df["candle_dir"] = np.sign(close - open_)
# Rolling volatility (realised vol over 20 bars)
log_ret = np.log(close / close.shift(1))
df["realvol_20"] = log_ret.rolling(20).std()
# Close relative to recent high/low channel (20-bar)
roll_high_20 = high.rolling(20).max()
roll_low_20 = low.rolling(20).min()
df["chan_pct_20"] = (close - roll_low_20) / (roll_high_20 - roll_low_20 + 1e-10)
# Lagged RSI and EMA cross for temporal context
df["rsi_14_lag1"] = rsi_14.shift(1)
df["rsi_14_lag2"] = rsi_14.shift(2)
df["ema_cross_lag1"] = df["ema_cross"].shift(1)
df["ema_cross_lag2"] = df["ema_cross"].shift(2)
df["macd_hist_lag1"] = df["macd_hist"].shift(1)
# RSI momentum: change in RSI
df["rsi_delta_1"] = rsi_14.diff(1)
df["rsi_delta_4"] = rsi_14.diff(4)
# EMA9 slope (normalised)
df["ema9_slope"] = ema_9.diff(3) / (ema_9.shift(3) + 1e-10)
df["ema21_slope"] = ema_21.diff(3) / (ema_21.shift(3) + 1e-10)
# Interaction: RSI * EMA cross direction
df["rsi_ema_cross_interact"] = df["rsi_norm"] * df["ema_cross_signal"]
# Fill NaN from indicator warm-up
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "EMA Cross 9/21 + RSI14 Gradient Boost Scalper",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.8,
"min_samples_leaf": 20,
"min_samples_split": 40,
"max_features": "sqrt",
"n_iter_no_change": 30,
"validation_fraction": 0.1,
"tol": 1e-4,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 17],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) using a GradientBoostingClassifier "
"with EMA 9/21 crossover as the primary signal source and RSI 14 as confirmation. "
"Deep feature set includes MACD, Bollinger Bands, Stochastic, ATR normalisation, "
"candle structure, lagged features and RSI/EMA interaction terms. "
"Gradient boosting chosen for its ability to capture non-linear interactions between "
"trend, momentum and volatility features without overfitting when regularised via "
"subsample, max_features, and early stopping. Threshold 0.56 filters marginal signals. "
"Session filter [7,17] focuses on London/NY overlap for highest EUR/USD liquidity. "
"SL 0.5% / TP 1.0% gives 1:2 risk-reward aligned with scalper momentum targets. "
"Reverse on opposite signal to stay in sync with fast EMA crossover momentum."
),
"notes": (
"EMA 9/21 cross captures short-term momentum shifts typical of active EUR/USD sessions. "
"RSI 14 filters entries in extreme overbought/oversold conditions. "
"NATR min_atr filter removes flat/low-vol periods. "
"Trend filter (SMA 50) ensures longs only above and shorts only below the medium-term trend. "
"n_iter_no_change=30 provides early stopping to prevent overfitting on the training split. "
"400 estimators with depth 4 and lr 0.04 balance bias-variance tradeoff for intraday data."
),
}
|
||||||||||
|
—
|
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%57.6% | +10.32%-11.65% | 1.170.81 | 3.96%3.96% | 745125 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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)."
),
}
|
||||||||||
|
—
|
AUD/USD EMA Cross (9/21) + RSI14 XGBoost Scalper
Maximise risk-adjusted return on AUD/USD 15-min bars. XGBoost chosen for its ability to capture non-linear interactions between the EMA-cros…
|
E
@echo-quanta-127
|
AUDUSD | 15min | 63.6%56.1% | +13.00%-18.37% | 1.190.68 | 4.73%4.73% | 1063139 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:40:19
# Model : XGBoost
# Feature Eng. : EMA (9,21), RSI 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/AUDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── EMA 9 and EMA 21 ──────────────────────────────────────────────────
ema_9 = close.ewm(span=9, adjust=False).mean()
ema_21 = close.ewm(span=21, adjust=False).mean()
df["ema_9"] = ema_9
df["ema_21"] = ema_21
df["dm_ema_9"] = (close - ema_9) / ema_9
df["dm_ema_21"] = (close - ema_21) / ema_21
# EMA crossover signal: positive when fast > slow
df["ema_cross"] = ema_9 - ema_21
df["ema_cross_prev"] = df["ema_cross"].shift(1)
# Binary: did a cross just occur?
df["ema_cross_up"] = np.where((df["ema_cross"] > 0) & (df["ema_cross_prev"] <= 0), 1, 0)
df["ema_cross_down"] = np.where((df["ema_cross"] < 0) & (df["ema_cross_prev"] >= 0), 1, 0)
# Trend direction encoded as -1 / 1
df["ema_trend"] = np.where(ema_9 > ema_21, 1, -1)
# ── RSI 14 ────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=13, adjust=False).mean()
avg_loss = loss.ewm(com=13, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi_14 = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi_14
# RSI regime flags
df["rsi_oversold"] = np.where(rsi_14 < 30, 1, 0)
df["rsi_overbought"] = np.where(rsi_14 > 70, 1, 0)
df["rsi_mid"] = rsi_14 - 50 # centred
# RSI momentum (1-bar change in RSI)
df["rsi_delta"] = rsi_14.diff(1)
df["rsi_delta2"] = rsi_14.diff(3)
# ── Additional momentum / volatility features ─────────────────────────
# ATR-like normalised true range
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(span=14, adjust=False).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / close # normalised ATR
# Rate-of-change over various horizons
for n in [4, 8, 16]:
df[f"roc_{n}"] = close.pct_change(n)
# Bollinger Band width and %B (using 20-period SMA)
sma_20 = close.rolling(20).mean()
std_20 = close.rolling(20).std()
bb_upper = sma_20 + 2 * std_20
bb_lower = sma_20 - 2 * std_20
df["bb_width"] = (bb_upper - bb_lower) / sma_20
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower).replace(0, np.nan)
# Candle body and wick features
df["body"] = (close - open_) / close
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / close
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / close
# Volume-normalised momentum proxy: price range relative to ATR
df["range_vs_atr"] = (high - low) / atr_14.replace(0, np.nan)
# Lagged EMA cross signal
df["ema_cross_lag1"] = df["ema_cross"].shift(1)
df["ema_cross_lag2"] = df["ema_cross"].shift(2)
# Combined signal: RSI and EMA cross alignment
df["rsi_ema_bull"] = np.where((rsi_14 > 50) & (ema_9 > ema_21), 1, 0)
df["rsi_ema_bear"] = np.where((rsi_14 < 50) & (ema_9 < ema_21), 1, 0)
# Hour-of-day (cyclical encoding) — no lookahead
hour = df.index.hour
df["hour_sin"] = np.sin(2 * np.pi * hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24)
# Day-of-week (cyclical encoding)
dow = df.index.dayofweek
df["dow_sin"] = np.sin(2 * np.pi * dow / 5)
df["dow_cos"] = np.cos(2 * np.pi * dow / 5)
# Fill NaN from warm-up periods
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "AUD/USD EMA Cross (9/21) + RSI14 XGBoost Scalper",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.8,
"colsample_bytree": 0.75,
"min_child_weight": 3,
"gamma": 0.1,
"reg_alpha": 0.05,
"reg_lambda": 1.5,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.54,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return on AUD/USD 15-min bars. "
"XGBoost chosen for its ability to capture non-linear interactions between "
"the EMA-cross regime, RSI momentum, volatility (NATR/BB width), and time-of-day. "
"Shallow trees (max_depth=4) with strong regularisation (reg_lambda=1.5, gamma=0.1) "
"reduce overfitting on the limited 1-year window. "
"2:1 R:R (SL=0.5%, TP=1.0%) improves Sharpe; reverse on opposite signal captures "
"trend momentum without missing transitions."
),
"notes": (
"Features: EMA-9/21 cross and distances, RSI-14 with regime flags and delta, "
"ATR-14, NATR, Bollinger Band width/%B, 4/8/16-bar ROC, candle anatomy, "
"time cyclical encodings. Threshold 0.54 filters marginal signals to raise precision. "
"No session filter applied — AUD/USD has meaningful moves across Asian and London sessions."
),
}
|
||||||||||
|
—
|
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%48.0% | +2.45%-11.45% | 1.150.58 | 1.65%1.65% | 30950 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||