upgrade xauusd atr breakout continuation

This commit is contained in:
Stanley Isaac
2026-04-06 15:33:49 +00:00
parent 94db6bd974
commit 1a5d209ccb
3 changed files with 109 additions and 14 deletions
+19 -5
View File
@@ -1,13 +1,27 @@
# XAUUSD ATR Breakout
# XAUUSD ATR Breakout Continuation
Custom Python breakout baseline for XAUUSD.
Custom Python breakout + continuation baseline for XAUUSD.
This project avoids ML on purpose. It is useful as a transparent baseline to compare against heavier gold models like XGBoost or LightGBM.
This project avoids ML on purpose. It is useful as a transparent benchmark to compare against heavier gold models like XGBoost or LightGBM.
The default gate is intentionally responsive so PPE produces more events than a strict long-range breakout filter:
The default gate is intentionally responsive so PPE produces more events than a strict long-range breakout filter. It combines:
- hard ATR breakout entries
- near-breakout pressure entries
- trend-continuation entries after range pressure
- EMA trend alignment
- short momentum measured in ATR units
Current defaults:
- `lookback`: `96`
- `breakout_window`: `12`
- `atr_mult`: `0.05`
- `near_breakout_atr`: `0.18`
- `pullback_atr`: `0.35`
- `min_momentum_atr`: `0.08`
- `fast_ema`: `8`
- `slow_ema`: `34`
- assigned runtime: Modal, because this starter uses `pandas`
If it overtrades, raise `atr_mult` first. If it is still too quiet, shorten `breakout_window`.
If it overtrades, raise `near_breakout_atr` more carefully than `atr_mult`: `atr_mult` controls hard breakout distance, while `near_breakout_atr` and `pullback_atr` control the extra continuation paths.
@@ -5,10 +5,15 @@
"runtime_target": "modal",
"artifact_format": "python_bundle",
"parameters": {
"lookback": 64,
"lookback": 96,
"atr_window": 14,
"breakout_window": 12,
"atr_mult": 0.05
"atr_mult": 0.05,
"near_breakout_atr": 0.18,
"pullback_atr": 0.35,
"min_momentum_atr": 0.08,
"fast_ema": 8,
"slow_ema": 34
},
"training_requirements": [
"numpy",
+83 -7
View File
@@ -15,42 +15,118 @@ def _atr(df, n):
return tr.ewm(span=n, adjust=False).mean()
def _ema(series, n):
return series.ewm(span=n, adjust=False).mean()
def _clip01(value):
return float(np.clip(value, 0.0, 1.0))
def train(data, config):
params = config.get("parameters", {})
return {
"params": {
"lookback": int(params.get("lookback", 64)),
"lookback": int(params.get("lookback", 96)),
"atr_window": int(params.get("atr_window", 14)),
"breakout_window": int(params.get("breakout_window", 12)),
"atr_mult": float(params.get("atr_mult", 0.05)),
"near_breakout_atr": float(params.get("near_breakout_atr", 0.18)),
"pullback_atr": float(params.get("pullback_atr", 0.35)),
"min_momentum_atr": float(params.get("min_momentum_atr", 0.08)),
"fast_ema": int(params.get("fast_ema", 8)),
"slow_ema": int(params.get("slow_ema", 34)),
},
"name": "xauusd_atr_breakout",
}, {"training_bars": int(len(data)), "model": "modal_python_rule_baseline"}
"name": "xauusd_atr_breakout_v2",
}, {"training_bars": int(len(data)), "model": "modal_python_rule_breakout_continuation"}
def predict(model, market_data, config):
params = {**model.get("params", {}), **config.get("parameters", {})}
lookback = int(params.get("lookback", 64))
lookback = int(params.get("lookback", 96))
atr_window = int(params.get("atr_window", 14))
breakout_window = int(params.get("breakout_window", 12))
atr_mult = float(params.get("atr_mult", 0.05))
near_breakout_atr = float(params.get("near_breakout_atr", 0.18))
pullback_atr = float(params.get("pullback_atr", 0.35))
min_momentum_atr = float(params.get("min_momentum_atr", 0.08))
fast_ema = int(params.get("fast_ema", 8))
slow_ema = int(params.get("slow_ema", 34))
candles = market_data.get("candles", [])
if len(candles) < lookback:
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}}
df = _df(candles[-lookback:])
atr = float(_atr(df, atr_window).iloc[-1])
if not np.isfinite(atr) or atr <= 0:
return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "invalid_atr"}}
close = float(df["close"].iloc[-1])
prev_close = float(df["close"].iloc[-2])
high = float(df["high"].iloc[-breakout_window:-1].max())
low = float(df["low"].iloc[-breakout_window:-1].min())
upper = high + atr * atr_mult
lower = low - atr * atr_mult
range_width = max(high - low, atr)
ema_fast = float(_ema(df["close"], fast_ema).iloc[-1])
ema_slow = float(_ema(df["close"], slow_ema).iloc[-1])
ema_gap_atr = (ema_fast - ema_slow) / atr
momentum_3 = (close - float(df["close"].iloc[-4])) / atr if len(df) >= 4 else 0.0
momentum_6 = (close - float(df["close"].iloc[-7])) / atr if len(df) >= 7 else momentum_3
body_atr = (close - float(df["open"].iloc[-1])) / atr
close_position = (close - low) / range_width
trend_up = ema_fast > ema_slow and close >= ema_slow and momentum_3 > -min_momentum_atr
trend_down = ema_fast < ema_slow and close <= ema_slow and momentum_3 < min_momentum_atr
meta = {
"atr": round(atr, 5),
"high": round(high, 5),
"low": round(low, 5),
"upper": round(upper, 5),
"lower": round(lower, 5),
"ema_gap_atr": round(float(ema_gap_atr), 4),
"momentum_3_atr": round(float(momentum_3), 4),
"momentum_6_atr": round(float(momentum_6), 4),
"close_position": round(float(close_position), 4),
}
def confidence(edge_atr, quality, floor=0.56):
return round(min(floor + edge_atr * 0.22 + quality * 0.16, 0.92), 4)
if close > upper:
edge = (close - upper) / max(atr, 1e-9)
return {"signal": "UP", "confidence": round(min(0.55 + edge, 0.92), 4), "metadata": {"breakout": "high", "atr": atr, "upper": upper}}
quality = _clip01(max(ema_gap_atr, 0.0) * 0.35 + max(momentum_3, 0.0) * 0.35 + max(body_atr, 0.0) * 0.20)
return {"signal": "UP", "confidence": confidence(edge, quality), "metadata": {**meta, "setup": "breakout_high"}}
if close < lower:
edge = (lower - close) / max(atr, 1e-9)
return {"signal": "DOWN", "confidence": round(min(0.55 + edge, 0.92), 4), "metadata": {"breakout": "low", "atr": atr, "lower": lower}}
quality = _clip01(max(-ema_gap_atr, 0.0) * 0.35 + max(-momentum_3, 0.0) * 0.35 + max(-body_atr, 0.0) * 0.20)
return {"signal": "DOWN", "confidence": confidence(edge, quality), "metadata": {**meta, "setup": "breakout_low"}}
return {"signal": "HOLD", "confidence": 0.2, "metadata": {"high": high, "low": low, "atr": atr, "upper": upper, "lower": lower}}
# If price is pressing a range boundary with trend/momentum confirmation,
# enter before the hard breakout instead of waiting for an extreme close.
upper_pressure = (high - close) / atr
lower_pressure = (close - low) / atr
if trend_up and upper_pressure <= near_breakout_atr and momentum_3 >= min_momentum_atr:
edge = max(near_breakout_atr - upper_pressure, 0.0)
quality = _clip01(max(ema_gap_atr, 0.0) * 0.35 + max(momentum_3, 0.0) * 0.35 + close_position * 0.20)
return {"signal": "UP", "confidence": confidence(edge, quality, floor=0.57), "metadata": {**meta, "setup": "near_breakout_high"}}
if trend_down and lower_pressure <= near_breakout_atr and momentum_3 <= -min_momentum_atr:
edge = max(near_breakout_atr - lower_pressure, 0.0)
quality = _clip01(max(-ema_gap_atr, 0.0) * 0.35 + max(-momentum_3, 0.0) * 0.35 + (1.0 - close_position) * 0.20)
return {"signal": "DOWN", "confidence": confidence(edge, quality, floor=0.57), "metadata": {**meta, "setup": "near_breakout_low"}}
# Continuation path: after a breakout, gold often retests the fast EMA
# without closing outside the range again. This keeps the system alive
# while still requiring trend and momentum context.
if trend_up and close > high - atr * pullback_atr and prev_close <= close and momentum_6 >= 0:
edge = max((close - (high - atr * pullback_atr)) / atr, 0.0)
quality = _clip01(max(ema_gap_atr, 0.0) * 0.35 + max(momentum_6, 0.0) * 0.25 + close_position * 0.25)
return {"signal": "UP", "confidence": confidence(edge, quality, floor=0.55), "metadata": {**meta, "setup": "trend_continuation_high"}}
if trend_down and close < low + atr * pullback_atr and prev_close >= close and momentum_6 <= 0:
edge = max(((low + atr * pullback_atr) - close) / atr, 0.0)
quality = _clip01(max(-ema_gap_atr, 0.0) * 0.35 + max(-momentum_6, 0.0) * 0.25 + (1.0 - close_position) * 0.25)
return {"signal": "DOWN", "confidence": confidence(edge, quality, floor=0.55), "metadata": {**meta, "setup": "trend_continuation_low"}}
return {"signal": "HOLD", "confidence": 0.2, "metadata": {**meta, "setup": "inside_range"}}