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 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%60.5% | +2.45%+5.24% | 1.151.39 | 1.65%1.65% | 30938 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||
|
🥈
|
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%40.0% | +3.05%+4.63% | 1.461.44 | 1.99%1.99% | 5720 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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/CAD BB + ATR Gradient Boosting Mean-Rev
Maximize risk-adjusted return (Sharpe/Calmar) on USD/CAD 15-min data. GradientBoostingClassifier chosen for strong generalisation on noisy F…
|
S
@silver-bull-130
|
USDCAD | 15min | 62.6%48.3% | +2.56%-3.02% | 1.150.90 | 1.75%1.75% | 35660 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:50:17
# 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 : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# 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)
# ── ATR (14) & Normalised ATR ────────────────────────────────────────────
atr_period = 14
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs()
], axis=1).max(axis=1)
atr = tr.ewm(span=atr_period, adjust=False).mean()
natr = atr / close
df["atr"] = atr
df["natr"] = natr
# ── 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, adjust=False).mean()
avg_loss = loss.ewm(span=rsi_period, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi"] = 100 - (100 / (1 + rs))
# ── MACD (12, 26, 9) ─────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
macd_signal = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line
df["macd_sig"] = macd_signal
df["macd_hist"]= macd_line - macd_signal
# ── SMA filters (50, 200) ────────────────────────────────────────────────
df["sma_20"] = close.rolling(20).mean()
df["sma_50"] = close.rolling(50).mean()
df["sma_200"] = close.rolling(200).mean()
# Price relative to moving averages
df["close_vs_sma20"] = (close - df["sma_20"]) / df["sma_20"]
df["close_vs_sma50"] = (close - df["sma_50"]) / df["sma_50"]
df["close_vs_sma200"] = (close - df["sma_200"]) / df["sma_200"]
# ── Price momentum / returns ─────────────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_4"] = close.pct_change(4)
df["ret_8"] = close.pct_change(8)
df["ret_16"] = close.pct_change(16)
df["ret_32"] = close.pct_change(32)
# ── 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["close_dir"] = np.sign(close - open_)
# ── Volatility regime ────────────────────────────────────────────────────
df["vol_ratio"] = natr / natr.rolling(50).mean() # ATR vs its own average
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).quantile(0.25), 1.0, 0.0)
# ── 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
# ── Rate-of-change ───────────────────────────────────────────────────────
df["roc_10"] = (close - close.shift(10)) / close.shift(10)
# ── Rolling z-score of close (20-bar) ────────────────────────────────────
roll_mean = close.rolling(20).mean()
roll_std = close.rolling(20).std(ddof=0).replace(0, np.nan)
df["zscore_20"] = (close - roll_mean) / roll_std
# ── Volume-related (if volume column exists) ─────────────────────────────
if "volume" in df.columns and df["volume"].sum() > 0:
vol_ma = df["volume"].rolling(20).mean().replace(0, np.nan)
df["vol_ratio_20"] = df["volume"] / vol_ma
# ── Fill NaNs from warm-up ───────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "USD/CAD BB + ATR Gradient Boosting Mean-Rev",
"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": [7, 20],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on USD/CAD 15-min data. "
"GradientBoostingClassifier chosen for strong generalisation on noisy FX "
"price data; moderate depth (4) and learning rate (0.04) with early stopping "
"prevent overfitting. Features: Bollinger Bands (mean-reversion signal via "
"bb_pct and bb_width), ATR/NATR (volatility filter), RSI, MACD, Stochastic, "
"z-score, momentum returns, and candle-body ratios. 2:1 R:R (SL 0.5%, TP 1.0%) "
"with session filter (07-20 UTC) to avoid illiquid overnight hours."
),
"notes": (
"session_filter [7,20] captures London + NY overlap on USD/CAD. "
"min_atr 0.0002 avoids flat/choppy markets. on_opposite=reverse ensures "
"the model flips direction quickly when sentiment changes. "
"target_horizon=4 bars (1 hour) aligns with typical intraday FX moves."
),
}
|
||||||||||
|
—
|
USD/CAD BB Mean-Reversion + ATR XGBoost
Maximise risk-adjusted return (Sharpe/Calmar) on USD/CAD 15-min using Bollinger Band mean-reversion signals augmented by ATR, RSI, MACD, and…
|
C
@candle_owl
|
USDCAD | 15min | 59.1%43.1% | +4.84%-14.90% | 1.310.55 | 1.34%1.34% | 36265 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:36:58
# Model : XGBoost
# Feature Eng. : BB (20,2.0), ATR 14 + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/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_sigma = close.rolling(bb_period).std(ddof=0)
bb_upper = bb_mid + bb_std * bb_sigma
bb_lower = bb_mid - bb_std * bb_sigma
df["bb_mid"] = bb_mid
df["bb_upper"] = bb_upper
df["bb_lower"] = bb_lower
df["bb_width"] = (bb_upper - bb_lower) / bb_mid
df["bb_pct"] = (close - bb_lower) / (bb_upper - bb_lower)
# ── ATR (14) & Normalised ATR ────────────────────────────────────────────
atr_period = 14
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr = tr.ewm(alpha=1.0 / atr_period, min_periods=atr_period, adjust=False).mean()
natr = atr / close
df["atr"] = atr
df["natr"] = natr
# ── Price momentum / returns ─────────────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_4"] = close.pct_change(4)
df["ret_8"] = close.pct_change(8)
df["ret_16"] = close.pct_change(16)
# ── Distance from Bollinger mid / bands ──────────────────────────────────
df["close_minus_mid"] = (close - bb_mid) / bb_mid
df["close_minus_upper"] = (close - bb_upper) / bb_mid
df["close_minus_lower"] = (close - bb_lower) / bb_mid
# ── BB squeeze flag: width below rolling median ───────────────────────────
bb_width_med = df["bb_width"].rolling(50).median()
df["bb_squeeze"] = np.where(df["bb_width"] < bb_width_med, 1.0, 0.0)
# ── BB mean-reversion z-score ────────────────────────────────────────────
df["bb_z"] = (close - bb_mid) / (bb_sigma + 1e-12)
# ── Candle body / wick features ──────────────────────────────────────────
body = (close - open_).abs()
candle_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_rng
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_rng
df["bull_candle"] = np.where(close > open_, 1.0, 0.0)
# ── RSI (14) built from scratch ──────────────────────────────────────────
rsi_period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_g = gain.ewm(alpha=1.0 / rsi_period, min_periods=rsi_period, adjust=False).mean()
avg_l = loss.ewm(alpha=1.0 / rsi_period, min_periods=rsi_period, adjust=False).mean()
rs = avg_g / (avg_l + 1e-12)
rsi = 100.0 - (100.0 / (1.0 + rs))
df["rsi_14"] = rsi
# RSI deviation from neutral 50
df["rsi_dev"] = (rsi - 50.0) / 50.0
# ── MACD (12, 26, 9) ─────────────────────────────────────────────────────
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema12 - ema26
macd_sig = macd_line.ewm(span=9, adjust=False).mean()
df["macd"] = macd_line / close
df["macd_hist"] = (macd_line - macd_sig) / close
# ── Rolling volatility (realised over 20 bars) ───────────────────────────
df["vol_20"] = df["ret_1"].rolling(20).std()
# ── ATR z-score vs 50-bar rolling mean ───────────────────────────────────
atr_mean = atr.rolling(50).mean()
atr_std = atr.rolling(50).std(ddof=0)
df["atr_z"] = (atr - atr_mean) / (atr_std + 1e-12)
# ── Volume-of-BB-touches over last 10 bars ───────────────────────────────
near_upper = (close >= bb_upper * 0.998).astype(float)
near_lower = (close <= bb_lower * 1.002).astype(float)
df["touch_upper_10"] = near_upper.rolling(10).sum()
df["touch_lower_10"] = near_lower.rolling(10).sum()
# ── SMA 50 (trend filter helper) ─────────────────────────────────────────
df["sma_50"] = close.rolling(50).mean()
df["close_vs_sma"] = (close - df["sma_50"]) / df["sma_50"]
# ── EMA cross (9 / 21) ───────────────────────────────────────────────────
ema9 = close.ewm(span=9, adjust=False).mean()
ema21 = close.ewm(span=21, adjust=False).mean()
df["ema_cross"] = (ema9 - ema21) / close
# ── Bar-of-day / session ─────────────────────────────────────────────────
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
# ── Lag features on bb_pct and rsi ───────────────────────────────────────
for lag in [1, 2, 4]:
df[f"bb_pct_lag{lag}"] = df["bb_pct"].shift(lag)
df[f"rsi_14_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
# ── Fill NaN from warm-up ─────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "USD/CAD BB Mean-Reversion + ATR XGBoost",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 3,
"gamma": 0.15,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"random_state": 42,
"n_jobs": -1,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [7, 20],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return (Sharpe/Calmar) on USD/CAD 15-min "
"using Bollinger Band mean-reversion signals augmented by ATR, RSI, "
"MACD, and EMA-cross features fed into a regularised XGBoost classifier. "
"SL=0.5% / TP=1.0% gives a 1:2 RR floor. Conservative depth (4) and "
"strong L1/L2 regularisation prevent overfitting on a single year of data."
),
"notes": (
"BB squeeze flag and bb_z capture regime; atr_z filters noisy bars. "
"Session filter 07-20 UTC covers London + NY overlap for tighter spreads. "
"min_atr=0.0002 avoids dead-market whipsaws. Lag features on bb_pct and "
"rsi_14 give the model short-term momentum context without look-ahead."
),
}
|
||||||||||
|
—
|
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%47.3% | +6.05%-6.26% | 1.310.78 | 2.07%2.07% | 47474 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||