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 | ||
|---|---|---|---|---|---|---|---|---|---|---|
|
🥇
|
NZD/USD RSI-MACD Gradient Boost Risk-Adjusted
Maximize risk-adjusted return (Sharpe/Calmar) using a deep GradientBoostingClassifier with many slow-learning trees and aggressive regularis…
|
S
@silver-bull-130
|
NZDUSD | 15min | 60.9%0.0% | +18.36%+0.00% | 1.35— | 3.80%3.80% | 7320 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:35:33
# Model : Gradient Boosting
# Feature Eng. : RSI 14, MACD (12,26,9) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/NZDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── RSI 14 ──────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(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)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI derived signals
df["rsi_ob"] = np.where(df["rsi_14"] > 70, 1, 0) # overbought flag
df["rsi_os"] = np.where(df["rsi_14"] < 30, 1, 0) # oversold flag
df["rsi_mid"] = df["rsi_14"] - 50 # centred
df["rsi_slope"] = df["rsi_14"].diff(3) # momentum of RSI
df["rsi_accel"] = df["rsi_slope"].diff(2) # acceleration
# RSI regime: above/below 50
df["rsi_bull"] = np.where(df["rsi_14"] > 50, 1, -1)
# ── 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
df["macd_cross"] = np.where(macd_line > signal_line, 1, -1)
df["macd_hist_sign"] = np.where(macd_hist > 0, 1, -1)
df["macd_hist_chg"] = macd_hist.diff(1) # histogram change
df["macd_hist_accel"]= df["macd_hist_chg"].diff(1) # second derivative
df["macd_zero_cross"]= np.where(macd_line > 0, 1, -1)
# ── ATR 14 ──────────────────────────────────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr14 = tr.ewm(alpha=1/14, min_periods=14, adjust=False).mean()
df["atr_14"] = atr14
df["natr_14"] = atr14 / close # normalised ATR
df["atr_ratio"]= atr14 / atr14.rolling(50).mean() # current vs recent vol
# ── Volatility regime ───────────────────────────────────────────────────
df["vol_high"] = np.where(df["natr_14"] > df["natr_14"].rolling(100).median(), 1, 0)
# ── Price momentum ──────────────────────────────────────────────────────
df["ret_1"] = close.pct_change(1)
df["ret_3"] = close.pct_change(3)
df["ret_8"] = close.pct_change(8)
df["ret_16"] = close.pct_change(16)
# Scaled by ATR so the model sees normalised moves
df["ret_1_atr"] = df["ret_1"] / (atr14 / close).replace(0, np.nan)
df["ret_3_atr"] = df["ret_3"] / (atr14 / close).replace(0, np.nan)
df["ret_8_atr"] = df["ret_8"] / (atr14 / close).replace(0, np.nan)
# ── EMAs & trend structure ───────────────────────────────────────────────
ema8 = close.ewm(span=8, adjust=False).mean()
ema21 = close.ewm(span=21, adjust=False).mean()
ema50 = close.ewm(span=50, adjust=False).mean()
ema100= close.ewm(span=100,adjust=False).mean()
df["ema8_21_spread"] = (ema8 - ema21) / close
df["ema21_50_spread"]= (ema21 - ema50) / close
df["ema50_100_spread"]= (ema50 - ema100) / close
df["price_vs_ema21"] = (close - ema21) / close
df["price_vs_ema50"] = (close - ema50) / close
df["trend_align"] = np.where(
(ema8 > ema21) & (ema21 > ema50), 1,
np.where((ema8 < ema21) & (ema21 < ema50), -1, 0)
)
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
bb_mid = close.rolling(20).mean()
bb_std = close.rolling(20).std(ddof=0)
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
df["bb_pct"] = bb_pct
df["bb_bw"] = bb_bw
df["bb_bw_ratio"] = bb_bw / bb_bw.rolling(50).mean() # squeeze detector
df["bb_upper_touch"] = np.where(close >= bb_up, 1, 0)
df["bb_lower_touch"] = np.where(close <= bb_lo, 1, 0)
# ── Stochastic %K %D (14, 3) ────────────────────────────────────────────
lo14 = low.rolling(14).min()
hi14 = high.rolling(14).max()
stoch_k = 100 * (close - lo14) / (hi14 - lo14).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
df["stoch_ob"] = np.where(stoch_k > 80, 1, 0)
df["stoch_os"] = np.where(stoch_k < 20, 1, 0)
# ── Candle structure ────────────────────────────────────────────────────
body = (close - open_).abs()
candle_rng= (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng # body vs full range
df["upper_wick"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_rng
df["lower_wick"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_rng
df["candle_dir"] = np.where(close > open_, 1, -1)
df["candle_dir_3"] = df["candle_dir"].rolling(3).sum() # short-term bias
# ── Volume-less momentum oscillator (Williams %R 14) ───────────────────
df["williams_r"] = -100 * (hi14 - close) / (hi14 - lo14).replace(0, np.nan)
# ── RSI x MACD composite signal ─────────────────────────────────────────
df["rsi_macd_bull"] = np.where(
(df["rsi_14"] > 50) & (macd_hist > 0), 1,
np.where((df["rsi_14"] < 50) & (macd_hist < 0), -1, 0)
)
# ── Divergence proxy: price vs RSI direction (3-bar) ────────────────────
price_dir3 = np.sign(close.diff(3))
rsi_dir3 = np.sign(df["rsi_14"].diff(3))
df["rsi_div"] = np.where(price_dir3 != rsi_dir3, 1, 0)
# ── Mean-reversion signal: distance from 50-bar mean normalised by ATR ──
sma50 = close.rolling(50).mean()
df["zscore_50"] = (close - sma50) / (close.rolling(50).std(ddof=0).replace(0, np.nan))
df["mean_rev_long"] = np.where(df["zscore_50"] < -1.5, 1, 0)
df["mean_rev_short"] = np.where(df["zscore_50"] > 1.5, 1, 0)
# ── Interaction features ─────────────────────────────────────────────────
df["rsi_bb_pct"] = df["rsi_14"] * df["bb_pct"]
df["macd_hist_rsi_mid"] = df["macd_hist"] * df["rsi_mid"]
df["stoch_rsi"] = df["stoch_k"] * df["rsi_14"] / 1e4 # normalised product
# ── Lag features (avoid lookahead) ──────────────────────────────────────
for lag in [1, 2, 4, 8]:
df[f"rsi_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
df[f"ret_lag{lag}"] = df["ret_1"].shift(lag)
# ── Hour-of-day & day-of-week cyclic encoding ───────────────────────────
if hasattr(df.index, "hour"):
hour = df.index.hour
dow = df.index.dayofweek
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)
# ── Final fill ───────────────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD RSI-MACD Gradient Boost Risk-Adjusted",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 600,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.75,
"max_features": "sqrt",
"min_samples_leaf": 20,
"min_samples_split":40,
"warm_start": False,
},
"signal_threshold": 0.56,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [21, 21],
"min_atr": 0.0002,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) using a deep "
"GradientBoostingClassifier with many slow-learning trees and "
"aggressive regularisation (min_samples_leaf=20, subsample=0.75). "
"Feature set deliberately differs from prior RSI+BB+Stoch attempts "
"by adding: ATR-normalised returns, z-score mean-reversion signals, "
"RSI divergence proxy, Williams %R, candle structure ratios, cyclic "
"time encoding, and interaction/lag features to give the model richer "
"multi-timeframe context. SL=0.5%/TP=1% gives 1:2 RR aligned with "
"maximising Sharpe."
),
"notes": (
"Prior PF=1.35 / ret=+18.36% used standard RSI+MACD+BB+Stoch without "
"ATR normalisation or divergence detection. This version adds z-score "
"mean-reversion context, candle structure, and temporal encoding to "
"reduce false positives. session_filter=[21,21] is intentionally "
"narrow — set to None if you want 24h coverage. min_atr=0.0002 "
"avoids dead-market signals."
),
}
|
||||||||||
|
🥈
|
NZD/USD MACD+RSI Gradient Boosting Risk-Adjusted
Maximize risk-adjusted return (Sharpe/Calmar) on NZD/USD 15-min data. GradientBoostingClassifier chosen for strong generalisation with tabul…
|
V
@vol_drifter
|
NZDUSD | 15min | 60.9%59.7% | +18.36%+7.92% | 1.351.20 | 3.80%3.80% | 732139 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:16:23
# Model : Gradient Boosting
# Feature Eng. : RSI 14, MACD (12,26,9) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/NZDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── RSI 14 ──────────────────────────────────────────────────────────────
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=13, min_periods=14, adjust=False).mean()
avg_loss = loss.ewm(com=13, min_periods=14, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI derived signals
df["rsi_overbought"] = np.where(df["rsi_14"] > 70, 1, 0)
df["rsi_oversold"] = np.where(df["rsi_14"] < 30, 1, 0)
df["rsi_mid_cross"] = np.where(df["rsi_14"] > 50, 1, -1)
# RSI momentum (rate of change of RSI)
df["rsi_roc"] = df["rsi_14"].diff(3)
# ── MACD (12, 26, 9) ────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
macd_hist = macd_line - signal_line
df["macd_line"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_hist
# MACD derived signals
df["macd_cross"] = np.where(macd_line > signal_line, 1, -1)
df["macd_hist_sign"] = np.where(macd_hist > 0, 1, -1)
df["macd_hist_accel"] = macd_hist.diff()
# ── ATR (14) ─────────────────────────────────────────────────────────────
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
df["atr_14"] = tr.ewm(com=13, min_periods=14, adjust=False).mean()
df["natr_14"] = df["atr_14"] / close
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
sma_20 = close.rolling(20).mean()
std_20 = close.rolling(20).std()
bb_upper = sma_20 + 2 * std_20
bb_lower = sma_20 - 2 * std_20
df["bb_width"] = (bb_upper - bb_lower) / sma_20
df["bb_position"] = (close - bb_lower) / (bb_upper - bb_lower).replace(0, np.nan)
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width"].rolling(50).mean(), 1, 0)
# ── Trend / SMA filters ──────────────────────────────────────────────────
df["sma_20"] = sma_20
df["sma_50"] = close.rolling(50).mean()
df["sma_200"] = close.rolling(200).mean()
df["price_above_sma50"] = np.where(close > df["sma_50"], 1, -1)
df["price_above_sma200"] = np.where(close > df["sma_200"], 1, -1)
df["sma50_above_sma200"] = np.where(df["sma_50"] > df["sma_200"], 1, -1)
# ── Momentum / ROC ───────────────────────────────────────────────────────
df["roc_4"] = close.pct_change(4)
df["roc_8"] = close.pct_change(8)
df["roc_16"] = close.pct_change(16)
# ── Candlestick / price structure ────────────────────────────────────────
df["body"] = (close - open_) / close
df["upper_wick"] = (high - close.clip(lower=open_)) / close
df["lower_wick"] = (open_.clip(upper=close) - low) / close
df["hl_range"] = (high - low) / close
df["gap"] = (open_ - close.shift(1)) / close.shift(1)
# ── Volume-free spread proxy ─────────────────────────────────────────────
df["spread_ratio"] = df["hl_range"] / df["atr_14"].replace(0, np.nan)
# ── Stochastic RSI proxy ─────────────────────────────────────────────────
rsi_min = df["rsi_14"].rolling(14).min()
rsi_max = df["rsi_14"].rolling(14).max()
df["stoch_rsi"] = (df["rsi_14"] - rsi_min) / (rsi_max - rsi_min).replace(0, np.nan)
# ── Williams %R (14) ─────────────────────────────────────────────────────
highest_high = high.rolling(14).max()
lowest_low = low.rolling(14).min()
df["williams_r"] = -100 * (highest_high - close) / (highest_high - lowest_low).replace(0, np.nan)
# ── Lagged features ──────────────────────────────────────────────────────
for lag in [1, 2, 3, 4]:
df[f"rsi_lag{lag}"] = df["rsi_14"].shift(lag)
df[f"macd_hist_lag{lag}"] = df["macd_hist"].shift(lag)
df[f"roc4_lag{lag}"] = df["roc_4"].shift(lag)
# ── Fill NaN from indicator warm-up ─────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD MACD+RSI Gradient Boosting Risk-Adjusted",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.8,
"min_samples_leaf": 20,
"max_features": "sqrt",
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"tol": 1e-4,
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximize risk-adjusted return (Sharpe/Calmar) on NZD/USD 15-min data. "
"GradientBoostingClassifier chosen for strong generalisation with tabular features, "
"low learning rate + early stopping prevents overfitting. "
"SL=0.5%, TP=1.0% gives 1:2 R:R ratio. Threshold=0.55 filters marginal signals."
),
"notes": (
"Core features: RSI-14, MACD(12,26,9) with histogram momentum. "
"Supplementary: ATR, Bollinger Bands, SMA trend, Williams %R, StochRSI, "
"candlestick structure, lagged RSI/MACD/ROC. "
"reverse on opposite signal captures trend continuation. "
"target_horizon=4 bars (1 hour) aligns with typical MACD/RSI signal duration."
),
}
|
||||||||||
|
🥉
|
NZD/USD 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%55.4% | +19.51%+5.40% | 1.361.13 | 2.51%2.51% | 745148 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ 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."
),
}
|
||||||||||
|
1.00
|
NZD/USD EMA Cross + ATR Gradient Boosting
Maximize risk-adjusted return (Sharpe / Calmar). GradientBoostingClassifier with moderate depth (4) and low learning rate (0.03) to reduce o…
|
E
@elastic-moose-350
|
NZDUSD | 15min | 63.3%64.5% | +9.88%+4.69% | 1.181.14 | 3.44%3.44% | 712110 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 03:16:00
# Model : Gradient Boosting
# 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/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 50 and EMA 200 ──────────────────────────────────────────────────
ema_50 = close.ewm(span=50, adjust=False).mean()
ema_200 = close.ewm(span=200, adjust=False).mean()
df["ema_50"] = ema_50
df["ema_200"] = ema_200
df["dm_ema_50"] = (close - ema_50) / ema_50
df["dm_ema_200"] = (close - ema_200) / ema_200
# EMA cross signal: ema_50 vs ema_200
df["ema_cross"] = df["ema_50"] - df["ema_200"]
# Cross direction: +1 when ema_50 > ema_200, -1 otherwise
df["ema_cross_sign"] = np.where(df["ema_cross"] > 0, 1.0, -1.0)
# Cross event: 1 when cross just happened (sign flip)
prev_cross = df["ema_cross"].shift(1)
df["ema_cross_event"] = np.where(
(df["ema_cross"] * prev_cross) < 0, 1.0, 0.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 = tr.ewm(span=14, adjust=False).mean()
df["atr"] = atr
df["natr"] = atr / close
# ── 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-10)
rsi = 100 - (100 / (1 + rs))
df["rsi_14"] = rsi
df["rsi_norm"] = (rsi - 50) / 50 # centred and scaled
# ── MACD ────────────────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd = ema_12 - ema_26
signal = macd.ewm(span=9, adjust=False).mean()
df["macd"] = macd
df["macd_signal"] = signal
df["macd_hist"] = macd - signal
df["macd_norm"] = macd / close
df["macd_hist_norm"] = (macd - signal) / close
# ── Bollinger Bands (20, 2) ──────────────────────────────────────────────
sma_20 = close.rolling(20).mean()
std_20 = close.rolling(20).std()
bb_upper = sma_20 + 2 * std_20
bb_lower = sma_20 - 2 * std_20
bb_width = (bb_upper - bb_lower) / (sma_20 + 1e-10)
bb_pos = (close - bb_lower) / (bb_upper - bb_lower + 1e-10)
df["bb_width"] = bb_width
df["bb_pos"] = bb_pos
# ── Momentum & Rate-of-Change ────────────────────────────────────────────
df["mom_4"] = close.pct_change(4)
df["mom_8"] = close.pct_change(8)
df["mom_16"] = close.pct_change(16)
# ── Rolling volatility (realised vol over 20 bars) ──────────────────────
log_ret = np.log(close / close.shift(1))
df["rvol_20"] = log_ret.rolling(20).std()
# ── Stochastic Oscillator (14) ───────────────────────────────────────────
low_14 = low.rolling(14).min()
high_14 = high.rolling(14).max()
stoch_k = 100 * (close - low_14) / (high_14 - low_14 + 1e-10)
stoch_d = stoch_k.rolling(3).mean()
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_d
df["stoch_diff"] = stoch_k - stoch_d
# ── Candle body / range features ────────────────────────────────────────
df["body"] = (close - open_).abs() / (high - low + 1e-10)
df["upper_wick"] = (high - close.clip(lower=open_)) / (high - low + 1e-10)
df["lower_wick"] = (close.clip(upper=open_) - low) / (high - low + 1e-10)
df["bar_dir"] = np.where(close > open_, 1.0, -1.0)
# ── Price position relative to EMAs ─────────────────────────────────────
df["close_vs_ema50_sign"] = np.where(close > ema_50, 1.0, -1.0)
df["close_vs_ema200_sign"] = np.where(close > ema_200, 1.0, -1.0)
# ── Lagged features (1-bar and 2-bar lags on key signals) ───────────────
for col in ["rsi_norm", "macd_hist_norm", "mom_4", "ema_cross", "natr", "bb_pos"]:
df[f"{col}_lag1"] = df[col].shift(1)
df[f"{col}_lag2"] = df[col].shift(2)
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD EMA Cross + ATR Gradient Boosting",
"model_type": "GradientBoostingClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.03,
"subsample": 0.8,
"min_samples_leaf": 20,
"max_features": "sqrt",
"validation_fraction": 0.1,
"n_iter_no_change": 30,
"tol": 1e-4,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.01,
"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). "
"GradientBoostingClassifier with moderate depth (4) and low learning rate (0.03) "
"to reduce overfitting on 15-min NZD/USD. SL=0.5%, TP=1.0% gives 1:2 RR. "
"EMA 50/200 cross is the primary trend feature; ATR normalises volatility context. "
"Supplementary RSI, MACD, Bollinger, Stochastic and candle-body features capture "
"momentum and mean-reversion signals. Early stopping via n_iter_no_change guards "
"against overfit on the training partition."
),
"notes": (
"target_horizon=4 (1 hour) matches typical intraday swing on NZD/USD. "
"reverse on opposite signal keeps the model responsive during trending regimes. "
"No session filter applied — NZD/USD has reasonable liquidity around the clock. "
"min_samples_leaf=20 and subsample=0.8 add regularisation without grid search."
),
}
|
||||||||||
|
—
|
NZD/USD MACD+RSI Momentum (XGBoost, Risk-Adj)
Maximise risk-adjusted return (Sharpe/Calmar) on NZD/USD 15-min data. XGBoost chosen for its strong performance on tabular financial data. M…
|
D
@delta_one
|
NZDUSD | 15min | 63.8%55.2% | +18.16%-7.70% | 1.320.83 | 2.46%2.46% | 845143 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 02:51:04
# Model : XGBoost
# Feature Eng. : RSI 14, MACD (12,26,9) + Auto-add features: ON
# Signal / Entry : Enter when model confidence > threshold; exit on opposite signal or SL/TP
# Optimization : Maximize risk-adjusted return
# Risk Mgmt : Stop loss 0.5%, Take profit 1.0%
# Risk Filter : —
# ══════════════════════════════════════════════════════════════
# ============================================================
# SECTION 0 — IMPORTS & CONSTANTS
import numpy as np
import pandas as pd
DATA_PATH = "/root/Desktop/QuantifyMe/data/ohlc/NZDUSD_15min.parquet"
START_DATE = "2025-04-24"
END_DATE = "2026-04-24"
VALIDATION_DATE = ""
TRAIN_SPLIT = 0.7
# SECTION 1 — FEATURE ENGINEERING
def feature_engineering(df, close, open_, high, low):
# ── RSI 14 ──────────────────────────────────────────────────────────────
period = 14
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=period - 1, min_periods=period).mean()
avg_loss = loss.ewm(com=period - 1, min_periods=period).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
df["rsi_14"] = 100 - (100 / (1 + rs))
# RSI derived features
df["rsi_14_norm"] = (df["rsi_14"] - 50) / 50 # centred & scaled
df["rsi_14_ob"] = np.where(df["rsi_14"] > 70, 1, 0) # overbought flag
df["rsi_14_os"] = np.where(df["rsi_14"] < 30, 1, 0) # oversold flag
df["rsi_14_mom"] = df["rsi_14"].diff(3) # 3-bar momentum
# ── MACD (12, 26, 9) ────────────────────────────────────────────────────
ema_12 = close.ewm(span=12, adjust=False).mean()
ema_26 = close.ewm(span=26, adjust=False).mean()
macd_line = ema_12 - ema_26
signal_line = macd_line.ewm(span=9, adjust=False).mean()
macd_hist = macd_line - signal_line
df["macd_line"] = macd_line
df["macd_signal"] = signal_line
df["macd_hist"] = macd_hist
# MACD derived features
df["macd_hist_mom"] = macd_hist.diff(2) # histogram momentum
df["macd_cross_bull"] = np.where(
(macd_line > signal_line) & (macd_line.shift(1) <= signal_line.shift(1)), 1, 0
)
df["macd_cross_bear"] = np.where(
(macd_line < signal_line) & (macd_line.shift(1) >= signal_line.shift(1)), 1, 0
)
df["macd_zero_cross"] = np.where(
(macd_line > 0) & (macd_line.shift(1) <= 0), 1,
np.where((macd_line < 0) & (macd_line.shift(1) >= 0), -1, 0)
)
# ── Additional price-action features ────────────────────────────────────
# ATR (14) for volatility context
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr_14 = tr.ewm(span=14, adjust=False).mean()
df["atr_14"] = atr_14
df["natr_14"] = atr_14 / close # normalised ATR
# Bollinger Bands (20, 2) — mean-reversion context
sma_20 = close.rolling(20).mean()
std_20 = close.rolling(20).std()
bb_up = sma_20 + 2 * std_20
bb_lo = sma_20 - 2 * std_20
df["bb_pct"] = (close - bb_lo) / (bb_up - bb_lo + 1e-12) # 0-1 position
df["bb_width"] = (bb_up - bb_lo) / sma_20 # band width
# SMA filters
df["sma_20"] = sma_20
df["sma_50"] = close.rolling(50).mean()
df["price_vs_sma20"] = (close - sma_20) / (sma_20 + 1e-12)
df["price_vs_sma50"] = (close - df["sma_50"]) / (df["sma_50"] + 1e-12)
# Rate of change
df["roc_5"] = close.pct_change(5)
df["roc_10"] = close.pct_change(10)
df["roc_20"] = close.pct_change(20)
# Candlestick body / shadow ratios
body = (close - open_).abs()
candle_rng = (high - low).replace(0, np.nan)
df["body_ratio"] = body / candle_rng
df["upper_shadow"] = (high - pd.concat([close, open_], axis=1).max(axis=1)) / candle_rng
df["lower_shadow"] = (pd.concat([close, open_], axis=1).min(axis=1) - low) / candle_rng
df["bar_direction"] = np.where(close >= open_, 1, -1)
# Volume-proxy: realised range / ATR ratio
df["range_vs_atr"] = candle_rng / (atr_14 + 1e-12)
# Stochastic %K (14)
lowest_14 = low.rolling(14).min()
highest_14 = high.rolling(14).max()
stoch_k = 100 * (close - lowest_14) / (highest_14 - lowest_14 + 1e-12)
df["stoch_k"] = stoch_k
df["stoch_d"] = stoch_k.rolling(3).mean()
df["stoch_diff"] = df["stoch_k"] - df["stoch_d"]
# RSI × MACD interaction
df["rsi_macd_interact"] = df["rsi_14_norm"] * macd_hist
# Lagged features (1 and 2 bars back) for key signals
for col in ["rsi_14_norm", "macd_hist", "bb_pct", "roc_5"]:
df[f"{col}_lag1"] = df[col].shift(1)
df[f"{col}_lag2"] = df[col].shift(2)
# ── Fill NaN from warm-up ────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD MACD+RSI Momentum (XGBoost, Risk-Adj)",
"model_type": "XGBClassifier",
"model_params": {
"n_estimators": 400,
"max_depth": 4,
"learning_rate": 0.04,
"subsample": 0.75,
"colsample_bytree": 0.70,
"min_child_weight": 3,
"gamma": 0.15,
"reg_alpha": 0.10,
"reg_lambda": 1.50,
"objective": "binary:logistic",
"tree_method": "hist",
"random_state": 42,
},
"signal_threshold": 0.55,
"direction": "both",
"stop_loss": 0.005,
"take_profit": 0.010,
"cooldown": 0,
"max_positions": 1,
"on_opposite": "reverse",
"session_filter": [0, 23],
"min_atr": None,
"trend_filter": None,
"target_horizon": 4,
"objective": (
"Maximise risk-adjusted return (Sharpe/Calmar) on NZD/USD 15-min data. "
"XGBoost chosen for its strong performance on tabular financial data. "
"Moderate depth (4) and high regularisation (gamma, alpha, lambda) prevent "
"overfitting on a relatively small forex dataset. Subsample + colsample_bytree "
"add stochastic diversity. SL 0.5% / TP 1.0% gives a 1:2 R:R ratio to support "
"positive expectancy even with a sub-60% win rate. Threshold 0.55 filters marginal "
"signals while keeping trade frequency acceptable. Target horizon of 4 bars (1 hour) "
"aligns with typical MACD/RSI signal resolution on 15-min charts."
),
"notes": (
"Features: RSI-14 (raw, normalised, OB/OS flags, momentum), MACD(12,26,9) "
"(line, signal, histogram, crosses, zero-cross), Bollinger Bands %B & width, "
"ATR/NATR, SMA20/50 price deviations, Stochastic %K/%D, ROC(5/10/20), "
"candlestick body/shadow ratios, RSI×MACD interaction term, and lagged "
"versions (lag1, lag2) of key signals to capture short-term persistence."
),
}
|
||||||||||
|
—
|
NZD/USD Stoch+BB+RSI Gradient Boosting Mean-Revert
Maximise risk-adjusted return (Sharpe / Calmar) on NZD/USD 15-min. GradientBoostingClassifier selected for its strong generalisation on stru…
|
C
@candle_owl
|
NZDUSD | 15min | 59.1%57.9% | +3.83%-8.31% | 1.100.75 | 4.90%4.90% | 38157 |
|
# ╔══════════════════════════════════════════════════════════════╗
# ║ STRATEGY REQUEST LOG ║
# ╚══════════════════════════════════════════════════════════════╝
# Generated : 2026-05-06 01:54:56
# 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/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):
# ── 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=0)
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 - (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_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"]
# ── ATR (14) — for normalised volatility / min_atr filter ────────────────
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(com=atr_period - 1, min_periods=atr_period).mean()
df["atr"] = atr
df["natr"] = atr / close # normalised ATR used by min_atr filter
# ── Price momentum / rate-of-change ──────────────────────────────────────
df["roc_4"] = close.pct_change(4) # 1-hour momentum on 15-min bars
df["roc_8"] = close.pct_change(8) # 2-hour momentum
df["roc_16"] = close.pct_change(16) # 4-hour momentum
# ── EMA trend context ─────────────────────────────────────────────────────
df["ema_20"] = close.ewm(span=20, adjust=False).mean()
df["ema_50"] = close.ewm(span=50, adjust=False).mean()
df["ema_100"] = close.ewm(span=100, adjust=False).mean()
df["sma_50"] = close.rolling(50).mean() # used by trend_filter
df["ema_cross_20_50"] = df["ema_20"] - df["ema_50"]
df["ema_cross_50_100"] = df["ema_50"] - df["ema_100"]
df["close_vs_ema20"] = (close - df["ema_20"]) / df["ema_20"]
# ── Candlestick body / wick features ─────────────────────────────────────
df["body"] = (close - open_).abs()
df["candle_dir"] = np.where(close >= open_, 1.0, -1.0)
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
df["body_ratio"] = df["body"] / (high - low).replace(0, np.nan)
# ── Volume-proxy: realised range rolling stats ────────────────────────────
df["hl_range"] = high - low
df["hl_range_ma8"] = df["hl_range"].rolling(8).mean()
df["hl_range_ratio"]= df["hl_range"] / df["hl_range_ma8"]
# ── RSI derived signals ───────────────────────────────────────────────────
df["rsi_overbought"] = np.where(df["rsi"] > 70, 1.0, 0.0)
df["rsi_oversold"] = np.where(df["rsi"] < 30, 1.0, 0.0)
df["rsi_momentum"] = df["rsi"].diff(4)
# ── Stochastic derived signals ────────────────────────────────────────────
df["stoch_overbought"] = np.where(df["stoch_k"] > 80, 1.0, 0.0)
df["stoch_oversold"] = np.where(df["stoch_k"] < 20, 1.0, 0.0)
# ── BB squeeze: width vs rolling mean of width ────────────────────────────
df["bb_width_ma20"] = df["bb_width"].rolling(20).mean()
df["bb_squeeze"] = np.where(df["bb_width"] < df["bb_width_ma20"], 1.0, 0.0)
# ── Interaction features ──────────────────────────────────────────────────
df["rsi_bb_pct"] = df["rsi"] * df["bb_pct"]
df["stoch_k_bb_pct"] = df["stoch_k"] * df["bb_pct"]
df["rsi_stoch_diff"] = df["rsi"] - df["stoch_k"]
# ── Lagged features (avoids look-ahead) ──────────────────────────────────
for lag in [1, 2, 3, 4]:
df[f"rsi_lag{lag}"] = df["rsi"].shift(lag)
df[f"stoch_k_lag{lag}"] = df["stoch_k"].shift(lag)
df[f"bb_pct_lag{lag}"] = df["bb_pct"].shift(lag)
df[f"roc4_lag{lag}"] = df["roc_4"].shift(lag)
# ── Fill NaN from warm-up ─────────────────────────────────────────────────
df = df.bfill().ffill()
return df
# SECTION 2 — STRATEGY CONFIG
def strategy_config():
return {
"title": "NZD/USD Stoch+BB+RSI Gradient Boosting Mean-Revert",
"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.56,
"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 NZD/USD 15-min. "
"GradientBoostingClassifier selected for its strong generalisation on "
"structured tabular data with noisy financial features. n_estimators=400 "
"with early stopping (n_iter_no_change=30) prevents overfitting. "
"max_depth=4 keeps trees shallow to reduce variance. subsample=0.8 + "
"max_features=sqrt add stochasticity for robustness. SL 0.5% / TP 1.0% "
"gives a minimum 2:1 reward-risk ratio. Session filter 07-20 UTC covers "
"Sydney open through NY overlap, maximising NZD/USD liquidity. "
"Reverse on opposite signal keeps the model continuously positioned in "
"the highest-confidence direction. min_atr filter avoids flat/illiquid "
"periods where the model edges degrade."
),
"notes": (
"Features: Bollinger Bands (20,2) width & %B, RSI(14), Stochastic K/D "
"(14,3), ATR(14)/NATR, EMA cross (20/50/100), SMA50 trend context, "
"price ROC (4/8/16 bars), candlestick body/wick ratios, HL range "
"normalisation, BB squeeze flag, RSI/Stoch overbought-oversold flags, "
"interaction terms (RSI*%B, StochK*%B), and 4 lags each of RSI, StochK, "
"%B and ROC4. Threshold 0.56 slightly above 0.50 to filter marginal "
"signals without sacrificing too many trades."
),
}
|
||||||||||