Files
quant-trading-strategy-temp…/templates/xauusd-atr-breakout/strategy.py
T

137 lines
7.0 KiB
Python

import numpy as np
import pandas as pd
def _df(candles):
df = pd.DataFrame(candles, columns=["open", "high", "low", "close", "volume"])
for col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df.dropna().reset_index(drop=True)
def _atr(df, n):
prev = df["close"].shift()
tr = pd.concat([(df["high"] - df["low"]), (df["high"] - prev).abs(), (df["low"] - prev).abs()], axis=1).max(axis=1)
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", 96)),
"atr_window": int(params.get("atr_window", 14)),
"breakout_window": int(params.get("breakout_window", 45)),
"atr_mult": float(params.get("atr_mult", 0.02)),
"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.0)),
"fast_ema": int(params.get("fast_ema", 8)),
"slow_ema": int(params.get("slow_ema", 34)),
"enable_near_breakout": bool(params.get("enable_near_breakout", False)),
"enable_continuation": bool(params.get("enable_continuation", False)),
},
"name": "xauusd_atr_breakout_precision",
}, {"training_bars": int(len(data)), "model": "modal_python_rule_precision_breakout"}
def predict(model, market_data, config):
params = {**model.get("params", {}), **config.get("parameters", {})}
lookback = int(params.get("lookback", 96))
atr_window = int(params.get("atr_window", 14))
breakout_window = int(params.get("breakout_window", 45))
atr_mult = float(params.get("atr_mult", 0.02))
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.0))
fast_ema = int(params.get("fast_ema", 8))
slow_ema = int(params.get("slow_ema", 34))
enable_near_breakout = bool(params.get("enable_near_breakout", False))
enable_continuation = bool(params.get("enable_continuation", False))
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)
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)
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"}}
# 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 enable_near_breakout and 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 enable_near_breakout and 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 enable_continuation and 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 enable_continuation and 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"}}