Browse AI-generated trading strategies shared by the community. Fork, learn, and build on each other's work.
| Score▼ | Strategy | Author | Win Rate▼ | Return▼ | PF▼ | MDD▼ | Trades▼ | Actions | ||
|---|---|---|---|---|---|---|---|---|---|---|
|
🥇
|
GBP/USD Gradient Boosting Trend + Mean-Reversion
Maximize risk-adjusted return (Sharpe/Calmar) on GBP/USD 15-min. GradientBoostingClassifier with 400 shallow trees (depth 4) and a conservat…
|
C
@candid-owl-125
|
GBPUSD | 15min | 53.5%48.9% | +3.70%+7.67% | 1.161.28 | 2.46%2.46% | 31245 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:41:21
# 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/GBPUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA 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) ─────────────────────────────────────────────
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"] = np.where(bb_range != 0, (close - bb_lower) / bb_range, 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, adjust=False).mean()
avg_loss = loss.ewm(com=13, min_periods=14, adjust=False).mean()
rs = np.where(avg_loss != 0, avg_gain / avg_loss, 100.0)
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 + 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(com=13, min_periods=14, adjust=False).mean()
df["atr_14"] = atr
df["natr"] = np.where(close != 0, atr / close, 0.0)
# ── Price momentum (returns over multiple horizons) ────────────────────
for lag in [1, 4, 8, 16]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── 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"] = np.where(
candle_range.notna(),
(high - close.combine(open_, max)) / candle_range,
0.0
)
df["lower_wick"] = np.where(
candle_range.notna(),
(close.combine(open_, min) - low) / candle_range,
0.0
)
df["candle_dir"] = np.where(close >= open_, 1.0, -1.0)
# ── Volume proxy: normalised candle range ──────────────────────────────
rolling_range = candle_range.rolling(20).mean()
df["norm_range"] = np.where(
rolling_range != 0,
(high - low) / rolling_range,
1.0
)
# ── RSI derived features ───────────────────────────────────────────────
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"] = df["rsi_14"] - 50.0
df["rsi_slope"] = df["rsi_14"].diff(3)
# ── MACD histogram slope ───────────────────────────────────────────────
df["macd_hist_slope"] = df["macd_hist"].diff(2)
df["macd_cross"] = np.where(
(df["macd_hist"] > 0) & (df["macd_hist"].shift(1) <= 0), 1.0,
np.where(
(df["macd_hist"] < 0) & (df["macd_hist"].shift(1) >= 0), -1.0,
0.0
)
)
# ── Bollinger squeeze (low volatility precursor) ───────────────────────
bb_width_ma = df["bb_width"].rolling(20).mean()
df["bb_squeeze"] = np.where(df["bb_width"] < bb_width_ma, 1.0, 0.0)
# ── SMA slope features ─────────────────────────────────────────────────
df["sma_20_slope"] = df["sma_20"].pct_change(4)
df["sma_50_slope"] = df["sma_50"].pct_change(8)
# ── Cross-SMA alignment (trend structure) ──────────────────────────────
df["sma20_above_50"] = np.where(df["sma_20"] > df["sma_50"], 1.0, 0.0)
df["sma50_above_200"] = np.where(df["sma_50"] > df["sma_200"], 1.0, 0.0)
df["close_above_200"] = np.where(close > df["sma_200"], 1.0, 0.0)
# ── Lagged close returns as additional features ────────────────────────
for lag in [1, 2, 3]:
df[f"close_lag_{lag}"] = close.shift(lag)
# ── Rolling volatility (std of returns) ────────────────────────────────
df["vol_10"] = close.pct_change().rolling(10).std()
df["vol_20"] = close.pct_change().rolling(20).std()
df["vol_ratio"] = np.where(
df["vol_20"] != 0,
df["vol_10"] / df["vol_20"],
1.0
)
# ── Hour-of-day (London/NY session proxy) ──────────────────────────────
if hasattr(df.index, "hour"):
df["hour_sin"] = np.sin(2 * np.pi * df.index.hour / 24.0)
df["hour_cos"] = np.cos(2 * np.pi * df.index.hour / 24.0)
else:
df["hour_sin"] = 0.0
df["hour_cos"] = 1.0
# ── Fill NaN from warm-up periods ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD Gradient Boosting Trend + Mean-Reversion",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.8,
"min_samples_leaf": 20,
"max_features": "sqrt",
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 20],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on GBP/USD 15-min. "
"GradientBoostingClassifier with 400 shallow trees (depth 4) and a "
"conservative learning rate of 0.04 avoids overfitting while capturing "
"non-linear interactions between trend (SMA alignment, slope), momentum "
"(MACD histogram, RSI), and volatility (ATR, BB squeeze) features. "
"SL=0.5% / TP=1.0% gives 1:2 R/R. Session filter 06-20 UTC covers "
"London open through NY close where GBP/USD liquidity is highest. "
"min_atr filter avoids flat/illiquid bars."
),
"notes": (
"Features include multi-period SMA distances, Bollinger Band pct/width, "
"RSI with overbought/oversold flags, MACD histogram slope and crossover, "
"ATR-normalised volatility, candle body/wick ratios, rolling vol ratio, "
"and hour-of-day cyclical encoding. target_horizon=4 (1-hour forward) "
"balances signal frequency against predictability at 15-min resolution."
),
}
|
||||||||||
|
🥈
|
GBP/USD BB Squeeze Breakout (GradientBoosting)
Maximize risk-adjusted return (Sharpe / Calmar). GradientBoostingClassifier chosen for its strong performance on tabular financial data with…
|
E
@elastic-moose-350
|
GBPUSD | 15min | 53.4%65.3% | +1.03%+1.23% | 1.041.07 | 5.20%5.20% | 34849 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:53:28
# Model : Gradient Boosting
# Feature Eng. : BB (20,2.0), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# Bollinger Bands Squeeze Breakout — GBP/USD 15-min
# ============================================================
# 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):
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_period = 20
bb_std = 2.0
bb_mid = close.rolling(bb_period).mean()
bb_sigma = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_sigma
bb_lower = bb_mid - bb_std * bb_sigma
bb_width = (bb_upper - bb_lower) / bb_mid
bb_pct = (close - bb_lower) / (bb_upper - bb_lower)
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = bb_width
df["bb_pct"] = bb_pct
# ── ATR (14) & NATR ─────────────────────────────────────────────────────
atr_period = 14
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(span=atr_period, min_periods=atr_period, adjust=False).mean()
natr = atr / close
df["atr"] = atr
df["natr"] = natr
# ── Squeeze detection ────────────────────────────────────────────────────
# Squeeze = BB width is in the bottom quartile over a 50-bar lookback
bb_width_min = bb_width.rolling(50).min()
bb_width_max = bb_width.rolling(50).max()
bb_width_norm = (bb_width - bb_width_min) / (bb_width_max - bb_width_min + 1e-12)
df["bb_width_norm"] = bb_width_norm
df["squeeze"] = np.where(bb_width_norm < 0.25, 1.0, 0.0)
# Squeeze released: was in squeeze 1 bar ago, now width is expanding
bb_width_chg = bb_width.diff()
df["squeeze_release"] = np.where(
(df["squeeze"].shift(1) == 1.0) & (bb_width_chg > 0), 1.0, 0.0
)
# ── BB width momentum ────────────────────────────────────────────────────
df["bb_width_chg"] = bb_width_chg
df["bb_width_chg_2"] = bb_width.diff(2)
df["bb_width_chg_5"] = bb_width.diff(5)
# ── Price position relative to bands ─────────────────────────────────────
df["close_vs_mid"] = close - bb_mid
df["close_vs_upper"] = close - bb_upper
df["close_vs_lower"] = close - bb_lower
# ── Momentum & returns ───────────────────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_3"] = close.pct_change(3)
df["ret_5"] = close.pct_change(5)
df["ret_10"] = close.pct_change(10)
df["ret_20"] = close.pct_change(20)
# ── RSI (14) ─────────────────────────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(span=rsi_period, min_periods=rsi_period, adjust=False).mean()
avg_loss = loss.ewm(span=rsi_period, min_periods=rsi_period, adjust=False).mean()
rs = avg_gain / (avg_loss + 1e-12)
rsi = 100.0 - 100.0 / (1.0 + rs)
df["rsi"] = rsi
# RSI divergence proxy: price makes new low/high but RSI does not
df["rsi_5_min"] = rsi.rolling(5).min()
df["close_5_min"] = close.rolling(5).min()
df["rsi_5_max"] = rsi.rolling(5).max()
df["close_5_max"] = close.rolling(5).max()
# ── 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
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
macd_hist = macd_line - macd_signal
df["macd_line"] = macd_line
df["macd_signal"] = macd_signal
df["macd_hist"] = macd_hist
df["macd_hist_chg"] = macd_hist.diff()
# ── Volume-like proxy: bar range ─────────────────────────────────────────
bar_range = high - low
df["bar_range"] = bar_range
df["bar_range_norm"] = bar_range / (atr + 1e-12)
# ── Candle body direction & size ─────────────────────────────────────────
body = close - open_
df["body"] = body
df["body_norm"] = body / (atr + 1e-12)
df["body_dir"] = np.where(body > 0, 1.0, np.where(body < 0, -1.0, 0.0))
# ── Upper / lower wick ───────────────────────────────────────────────────
df["upper_wick"] = high - pd.concat([close, open_], axis=1).max(axis=1)
df["lower_wick"] = pd.concat([close, open_], axis=1).min(axis=1) - low
# ── SMA trend context ─────────────────────────────────────────────────────
sma_50 = close.rolling(50).mean()
sma_200 = close.rolling(200).mean()
df["sma_50"] = sma_50
df["sma_200"] = sma_200
df["close_vs_sma50"] = (close - sma_50) / (sma_50 + 1e-12)
df["sma50_vs_sma200"] = (sma_50 - sma_200) / (sma_200 + 1e-12)
# ── Volatility regime ────────────────────────────────────────────────────
natr_ma = natr.rolling(50).mean()
df["natr_ratio"] = natr / (natr_ma + 1e-12) # >1 = above-avg vol
# ── Mean-reversion distance ───────────────────────────────────────────────
df["z_score_20"] = (close - bb_mid) / (bb_sigma + 1e-12)
# ── Rolling realized vol ─────────────────────────────────────────────────
df["rvol_10"] = df["ret_1"].rolling(10).std()
df["rvol_20"] = df["ret_1"].rolling(20).std()
# ── ATR-normalised returns ────────────────────────────────────────────────
df["ret_1_natr"] = df["ret_1"] / (natr + 1e-12)
# ── Lagged features ───────────────────────────────────────────────────────
for lag in [1, 2, 3, 5]:
df[f"bb_pct_lag{lag}"] = bb_pct.shift(lag)
df[f"bb_width_lag{lag}"] = bb_width.shift(lag)
df[f"rsi_lag{lag}"] = rsi.shift(lag)
df[f"macd_hist_lag{lag}"] = macd_hist.shift(lag)
# ── Hour-of-day (cyclical) ────────────────────────────────────────────────
hour = pd.Series(df.index.hour, index=df.index).astype(float)
df["hour_sin"] = np.sin(2 * np.pi * hour / 24.0)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24.0)
# ── Day-of-week (cyclical) ────────────────────────────────────────────────
dow = pd.Series(df.index.dayofweek, index=df.index).astype(float)
df["dow_sin"] = np.sin(2 * np.pi * dow / 5.0)
df["dow_cos"] = np.cos(2 * np.pi * dow / 5.0)
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD BB Squeeze Breakout (GradientBoosting)",
"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": 40,
"max_features": "sqrt",
"n_iter_no_change": 30,
"validation_fraction": 0.1,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 20],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe / Calmar). "
"GradientBoostingClassifier chosen for its strong performance on "
"tabular financial data with noisy labels. Shallow trees (max_depth=4) "
"with shrinkage (lr=0.04) and subsample=0.75 reduce overfitting. "
"Early stopping (n_iter_no_change=30) prevents over-training. "
"SL=0.5%, TP=1.0% gives a 1:2 risk/reward ratio. "
"Session filter 06-20 UTC captures London + New York overlap for GBP/USD."
),
"notes": (
"Core signal: BB squeeze (narrow band width) followed by expansion "
"breakout, confirmed by MACD histogram direction and RSI. "
"ATR filter ensures minimum volatility for entries. "
"Lagged BB features capture the squeeze build-up dynamic. "
"Z-score and normalized returns give the model mean-reversion context. "
"Cyclical time features allow the model to learn intraday seasonality."
),
}
|
||||||||||
|
🥉
|
GBP/USD SMA Trend + Multi-Indicator XGBoost Classifier
Maximize risk-adjusted return on GBP/USD 15-min bars. Strategy combines required SMA (20/50/200) distance and cross features with ADX trend …
|
E
@elastic-moose-350
|
GBPUSD | 15min | 43.4%41.7% | +7.34%-10.40% | 1.730.43 | 2.20%2.20% | 7612 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:27:43
# Model : XGBoost
# Feature Eng. : SMA (20,50,200) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/GBPUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── Required SMAs and distance metrics ──────────────────────────────────
for p in [20, 50, 200]:
sma = close.rolling(p).mean()
df[f"sma_{p}"] = sma
df[f"dm_sma_{p}"] = (close - sma) / sma
# ── SMA slope (momentum of the moving average itself) ───────────────────
for p in [20, 50]:
sma = close.rolling(p).mean()
df[f"sma_{p}_slope"] = sma.diff(5) / sma.shift(5)
# ── SMA cross signals ────────────────────────────────────────────────────
sma20 = close.rolling(20).mean()
sma50 = close.rolling(50).mean()
sma200 = close.rolling(200).mean()
df["sma20_50_cross"] = (sma20 - sma50) / sma50
df["sma50_200_cross"] = (sma50 - sma200) / sma200
df["sma20_200_cross"] = (sma20 - sma200) / sma200
# ── Price momentum over multiple horizons ────────────────────────────────
for lag in [1, 3, 6, 12, 24, 48]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Volatility: rolling standard deviation of returns ───────────────────
ret1 = close.pct_change(1)
for w in [10, 20, 40]:
df[f"vol_{w}"] = ret1.rolling(w).std()
# ── ATR (Average True Range, normalised) ─────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
for w in [14, 28]:
atr = tr.ewm(span=w, adjust=False).mean()
df[f"natr_{w}"] = atr / close
# ── Bollinger Bands (20-period, 2σ) ──────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std()
bb_up = bb_mid + 2 * bb_std
bb_lo = bb_mid - 2 * bb_std
bb_width = (bb_up - bb_lo) / bb_mid
df["bb_pct_b"] = (close - bb_lo) / (bb_up - bb_lo + 1e-12)
df["bb_width"] = bb_width
df["bb_squeeze"]= np.where(bb_width < bb_width.rolling(50).mean(), 1.0, 0.0)
# ── Keltner Channel (for squeeze confirmation) ───────────────────────────
kc_mid = close.ewm(span=20, adjust=False).mean()
kc_atr = tr.ewm(span=20, adjust=False).mean()
kc_up = kc_mid + 1.5 * kc_atr
kc_lo = kc_mid - 1.5 * kc_atr
df["kc_pct"] = (close - kc_lo) / (kc_up - kc_lo + 1e-12)
# ── RSI (Wilder) ─────────────────────────────────────────────────────────
def wilder_rsi(src, period):
delta = src.diff(1)
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_g = gain.ewm(alpha=1/period, adjust=False).mean()
avg_l = loss.ewm(alpha=1/period, adjust=False).mean()
rs = avg_g / (avg_l + 1e-12)
return 100 - 100 / (1 + rs)
rsi14 = wilder_rsi(close, 14)
rsi6 = wilder_rsi(close, 6)
rsi28 = wilder_rsi(close, 28)
df["rsi14"] = rsi14 / 100.0
df["rsi6"] = rsi6 / 100.0
df["rsi28"] = rsi28 / 100.0
df["rsi14_slope"] = rsi14.diff(3) / 100.0
# RSI divergence proxy: price new high/low but RSI doesn't confirm
price_high_12 = close.rolling(12).max()
price_low_12 = close.rolling(12).min()
rsi_high_12 = rsi14.rolling(12).max()
rsi_low_12 = rsi14.rolling(12).min()
df["rsi_bear_div"] = np.where(
(close >= price_high_12 * 0.999) & (rsi14 < rsi_high_12 * 0.97), 1.0, 0.0)
df["rsi_bull_div"] = np.where(
(close <= price_low_12 * 1.001) & (rsi14 > rsi_low_12 * 1.03), 1.0, 0.0)
# ── MACD ─────────────────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd = ema12 - ema26
signal = macd.ewm(span=9, adjust=False).mean()
hist = macd - signal
df["macd_norm"] = macd / close
df["macd_sig_norm"]= signal / close
df["macd_hist_norm"]= hist / close
df["macd_hist_slope"] = hist.diff(2) / close
# ── Stochastic Oscillator ─────────────────────────────────────────────────
for k_period in [14, 5]:
lo_k = low.rolling(k_period).min()
hi_k = high.rolling(k_period).max()
stoch_k = (close - lo_k) / (hi_k - lo_k + 1e-12) * 100
stoch_d = stoch_k.rolling(3).mean()
df[f"stoch_k_{k_period}"] = stoch_k / 100.0
df[f"stoch_d_{k_period}"] = stoch_d / 100.0
df[f"stoch_kd_{k_period}"] = (stoch_k - stoch_d) / 100.0
# ── Williams %R ───────────────────────────────────────────────────────────
hi14 = high.rolling(14).max()
lo14 = low.rolling(14).min()
df["williams_r"] = (hi14 - close) / (hi14 - lo14 + 1e-12)
# ── CCI (Commodity Channel Index) ────────────────────────────────────────
tp = (high + low + close) / 3.0
tp_sma = tp.rolling(20).mean()
tp_mad = tp.rolling(20).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
df["cci"] = (tp - tp_sma) / (0.015 * tp_mad + 1e-12) / 100.0
# ── Volume-like proxy: candle body and wick ratios ────────────────────────
candle_range = (high - low).replace(0, np.nan)
df["body_ratio"] = (close - open_).abs() / candle_range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_range
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_range
df["bull_candle"] = np.where(close > open_, 1.0, 0.0)
# ── Mean reversion signal: z-score of close vs SMA20 ────────────────────
df["zscore_20"] = (close - sma20) / (close.rolling(20).std() + 1e-12)
df["zscore_50"] = (close - sma50) / (close.rolling(50).std() + 1e-12)
# ── Trend strength: ADX proxy ─────────────────────────────────────────────
plus_dm = (high.diff(1)).clip(lower=0)
minus_dm = (-low.diff(1)).clip(lower=0)
overlap = pd.concat([plus_dm, minus_dm], axis=1).min(axis=1)
plus_dm = plus_dm - overlap
minus_dm = minus_dm - overlap
atr14 = tr.ewm(span=14, adjust=False).mean()
plus_di = 100 * plus_dm.ewm(span=14, adjust=False).mean() / (atr14 + 1e-12)
minus_di = 100 * minus_dm.ewm(span=14, adjust=False).mean() / (atr14 + 1e-12)
dx = (plus_di - minus_di).abs() / (plus_di + minus_di + 1e-12) * 100
adx = dx.ewm(span=14, adjust=False).mean()
df["adx"] = adx / 100.0
df["plus_di"] = plus_di / 100.0
df["minus_di"] = minus_di / 100.0
df["di_diff"] = (plus_di - minus_di) / 100.0
# ── Regime detection: above/below long-term SMA ──────────────────────────
df["bull_regime"] = np.where(close > sma200, 1.0, 0.0)
df["mid_regime"] = np.where(close > sma50, 1.0, 0.0)
# ── Lag features (auto-regressive) ───────────────────────────────────────
for col, lags in [("rsi14", [1, 2, 4]), ("macd_hist_norm", [1, 2]), ("bb_pct_b", [1, 2])]:
for lag in lags:
df[f"{col}_lag{lag}"] = df[col].shift(lag)
# ── Time-of-day features ─────────────────────────────────────────────────
if hasattr(df.index, "hour"):
df["hour_sin"] = np.sin(2 * np.pi * df.index.hour / 24.0)
df["hour_cos"] = np.cos(2 * np.pi * df.index.hour / 24.0)
df["dow_sin"] = np.sin(2 * np.pi * df.index.dayofweek / 5.0)
df["dow_cos"] = np.cos(2 * np.pi * df.index.dayofweek / 5.0)
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD SMA Trend + Multi-Indicator XGBoost Classifier",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 600,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 5,
"gamma": 0.10,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 18],
"min_atr": 0.0003,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return on GBP/USD 15-min bars. "
"Strategy combines required SMA (20/50/200) distance and cross features "
"with ADX trend strength, RSI divergence, Bollinger squeeze, Keltner, "
"MACD histogram slope, Stochastic, CCI, Williams %R, and candle-structure "
"ratios. XGBoost with strong regularisation and subsampling prevents "
"overfitting on the relatively short 1-year window. "
"Session filter 06-18 UTC keeps execution in liquid London/NY hours; "
"0.5% SL and 1.0% TP yield 1:2 R:R; sma_50 trend filter aligns trades "
"with intermediate momentum to improve win rate and Sharpe."
),
"notes": (
"Differs from prior RSI/MACD/BB/Stoch attempts by: (1) foregrounding "
"SMA cross and distance features as primary trend signals; (2) adding "
"ADX-based regime and DI differential; (3) including RSI divergence "
"proxy flags; (4) z-score mean-reversion features; (5) candle body/wick "
"structure ratios as micro-structure proxies; (6) time-of-day cyclical "
"encoding; (7) heavier regularisation (gamma, alpha, lambda) and higher "
"min_child_weight to reduce variance on the thin dataset."
),
}
|
||||||||||
|
—
|
GBP/USD SMA Trend Gradient Boosting Risk-Adj
Maximize risk-adjusted return (Sharpe/Calmar) on GBP/USD 15-min data. GradientBoostingClassifier chosen for its strong bias-variance tradeof…
|
R
@ratio_witch
|
GBPUSD | 15min | 43.1%33.3% | +6.85%-18.94% | 1.710.10 | 2.69%2.69% | 7212 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:47:56
# Model : Gradient Boosting
# Feature Eng. : SMA (20,50,200) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/GBPUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── SMA features (required) ──────────────────────────────────────────────
for period in [20, 50, 200]:
sma = close.rolling(period).mean()
df[f"sma_{period}"] = sma
df[f"dm_sma_{period}"] = (close - sma) / sma
# ── SMA crossover signals ────────────────────────────────────────────────
sma_20 = close.rolling(20).mean()
sma_50 = close.rolling(50).mean()
sma_200 = close.rolling(200).mean()
df["sma_20_50_cross"] = np.where(sma_20 > sma_50, 1.0, -1.0)
df["sma_20_200_cross"] = np.where(sma_20 > sma_200, 1.0, -1.0)
df["sma_50_200_cross"] = np.where(sma_50 > sma_200, 1.0, -1.0)
# ── Price momentum features ──────────────────────────────────────────────
for lag in [1, 2, 4, 8, 16]:
df[f"ret_{lag}"] = close.pct_change(lag)
# ── Volatility: rolling std of returns ──────────────────────────────────
ret_1 = close.pct_change(1)
for window in [8, 20, 50]:
df[f"vol_{window}"] = ret_1.rolling(window).std()
# ── ATR (Average True Range) ─────────────────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
for atr_period in [14, 50]:
atr = tr.rolling(atr_period).mean()
df[f"atr_{atr_period}"] = atr
df[f"natr_{atr_period}"] = atr / close
# ── RSI ──────────────────────────────────────────────────────────────────
for rsi_period in [14, 28]:
delta = close.diff()
gain = delta.clip(lower=0).rolling(rsi_period).mean()
loss = (-delta.clip(upper=0)).rolling(rsi_period).mean()
rs = gain / (loss + 1e-10)
df[f"rsi_{rsi_period}"] = 100 - (100 / (1 + rs))
# ── MACD ─────────────────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_line - signal_line
df["macd_hist_norm"] = (macd_line - signal_line) / (close + 1e-10)
# ── Bollinger Bands ───────────────────────────────────────────────────────
for bb_period in [20, 50]:
bb_mid = close.rolling(bb_period).mean()
bb_std = close.rolling(bb_period).std()
bb_upper = bb_mid + 2.0 * bb_std
bb_lower = bb_mid - 2.0 * bb_std
bb_width = (bb_upper - bb_lower) / (bb_mid + 1e-10)
bb_pos = (close - bb_lower) / (bb_upper - bb_lower + 1e-10)
df[f"bb_width_{bb_period}"] = bb_width
df[f"bb_pos_{bb_period}"] = bb_pos
# ── Stochastic Oscillator ────────────────────────────────────────────────
for stoch_period in [14, 28]:
lowest_low = low.rolling(stoch_period).min()
highest_high = high.rolling(stoch_period).max()
stoch_k = (close - lowest_low) / (highest_high - lowest_low + 1e-10) * 100
stoch_d = stoch_k.rolling(3).mean()
df[f"stoch_k_{stoch_period}"] = stoch_k
df[f"stoch_d_{stoch_period}"] = stoch_d
# ── Rate of Change (ROC) ──────────────────────────────────────────────────
for roc_period in [5, 10, 20]:
df[f"roc_{roc_period}"] = close.pct_change(roc_period)
# ── Candle body and shadow features ──────────────────────────────────────
body = (close - open_).abs()
candle_range = (high - low).abs()
df["body_ratio"] = body / (candle_range + 1e-10)
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / (candle_range + 1e-10)
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / (candle_range + 1e-10)
df["bullish_candle"] = np.where(close > open_, 1.0, -1.0)
# ── Volume-proxy: candle range as volatility proxy ────────────────────────
df["range_norm"] = candle_range / (close + 1e-10)
df["range_ma_ratio"] = candle_range / (candle_range.rolling(20).mean() + 1e-10)
# ── Lag features for return predictors ───────────────────────────────────
for col_lag in ["rsi_14", "macd_hist", "bb_pos_20"]:
for lag in [1, 2, 3]:
df[f"{col_lag}_lag{lag}"] = df[col_lag].shift(lag)
# ── Distance of close from recent high/low ────────────────────────────────
for lookback in [10, 20, 50]:
roll_high = high.rolling(lookback).max()
roll_low = low.rolling(lookback).min()
df[f"dist_high_{lookback}"] = (close - roll_high) / (roll_high + 1e-10)
df[f"dist_low_{lookback}"] = (close - roll_low) / (roll_low + 1e-10)
# ── Trend strength: ADX proxy ─────────────────────────────────────────────
adx_period = 14
tr_adx = tr.copy()
plus_dm = pd.Series(np.where((high.diff() > 0) & (high.diff() > -low.diff()), high.diff(), 0.0), index=close.index)
minus_dm = pd.Series(np.where((-low.diff() > 0) & (-low.diff() > high.diff()), -low.diff(), 0.0), index=close.index)
atr_adx = tr_adx.rolling(adx_period).mean()
plus_di = 100 * plus_dm.rolling(adx_period).mean() / (atr_adx + 1e-10)
minus_di = 100 * minus_dm.rolling(adx_period).mean() / (atr_adx + 1e-10)
dx = (100 * (plus_di - minus_di).abs() / (plus_di + minus_di + 1e-10))
df["adx"] = dx.rolling(adx_period).mean()
df["plus_di"] = plus_di
df["minus_di"] = minus_di
# ── Fill NaN from indicator warm-up ──────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "GBP/USD SMA Trend Gradient Boosting Risk-Adj",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"min_samples_leaf": 20,
"max_features": "sqrt",
"n_iter_no_change": 30,
"validation_fraction": 0.1,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.57,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [6, 18],
"min_atr": 0.0002,
"trend_filter": "sma_50",
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on GBP/USD 15-min data. "
"GradientBoostingClassifier chosen for its strong bias-variance tradeoff "
"on medium-sized tabular datasets without needing GPU. "
"Hyperparameters: moderate depth=4 prevents overfitting, learning_rate=0.04 "
"with 400 estimators balances convergence vs generalisation, subsample=0.75 "
"adds stochasticity to reduce variance, min_samples_leaf=20 enforces statistical "
"significance at each leaf. Early stopping via n_iter_no_change guards against "
"overfit on the training fold. Signal threshold 0.57 filters marginal signals "
"to improve precision. SL=0.5%, TP=1.0% gives 1:2 RR. Session filter 6-18 UTC "
"covers London+NY overlap — highest GBP/USD liquidity and tighter spreads. "
"sma_50 trend filter ensures we only trade in the direction of medium-term trend, "
"reducing whipsaw losses. target_horizon=4 bars (1 hour) gives the model enough "
"time for moves to develop while staying relevant for intraday trading."
),
"notes": (
"Features: SMA 20/50/200 with distance metrics (core requirement), RSI 14/28, "
"MACD, Bollinger Bands 20/50, Stochastic 14/28, ATR 14/50, NATR, ROC, ADX, "
"candle body/shadow ratios, lagged RSI/MACD/BB features, distance from rolling "
"high/low, SMA crossover signals, multi-lag return features. "
"All features are backward-looking only (no lookahead bias). "
"on_opposite=reverse for fast trend-following entries without missing reversals."
),
}
|
||||||||||
|
—
|
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%55.3% | +0.11%-15.15% | 1.010.66 | 3.34%3.34% | 37938 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||