From 94db6bd9740fa0f1a034ebd78bd4e25a80999626 Mon Sep 17 00:00:00 2001 From: Stanley Isaac Date: Mon, 6 Apr 2026 13:40:10 +0000 Subject: [PATCH] route xauusd atr breakout to modal --- templates/xauusd-atr-breakout/README.md | 2 +- .../xauusd-atr-breakout/quant.config.json | 12 ++- templates/xauusd-atr-breakout/strategy.py | 85 ++++++------------- 3 files changed, 34 insertions(+), 65 deletions(-) diff --git a/templates/xauusd-atr-breakout/README.md b/templates/xauusd-atr-breakout/README.md index 89e6846..738e44f 100644 --- a/templates/xauusd-atr-breakout/README.md +++ b/templates/xauusd-atr-breakout/README.md @@ -8,6 +8,6 @@ The default gate is intentionally responsive so PPE produces more events than a - `breakout_window`: `12` - `atr_mult`: `0.05` -- no runtime package dependencies +- assigned runtime: Modal, because this starter uses `pandas` If it overtrades, raise `atr_mult` first. If it is still too quiet, shorten `breakout_window`. diff --git a/templates/xauusd-atr-breakout/quant.config.json b/templates/xauusd-atr-breakout/quant.config.json index d185a13..3c4d705 100644 --- a/templates/xauusd-atr-breakout/quant.config.json +++ b/templates/xauusd-atr-breakout/quant.config.json @@ -2,7 +2,7 @@ "pair": "XAUUSD", "timeframe": "15m", "model_family": "custom Python", - "runtime_target": "edge", + "runtime_target": "modal", "artifact_format": "python_bundle", "parameters": { "lookback": 64, @@ -10,8 +10,14 @@ "breakout_window": 12, "atr_mult": 0.05 }, - "training_requirements": [], - "inference_requirements": [], + "training_requirements": [ + "numpy", + "pandas" + ], + "inference_requirements": [ + "numpy", + "pandas" + ], "symbol": "XAUUSD", "description": "XAUUSD ATR breakout rule baseline", "disclaimer": "Educational template only. Not financial advice." diff --git a/templates/xauusd-atr-breakout/strategy.py b/templates/xauusd-atr-breakout/strategy.py index b274434..9406609 100644 --- a/templates/xauusd-atr-breakout/strategy.py +++ b/templates/xauusd-atr-breakout/strategy.py @@ -1,34 +1,18 @@ -def _clean_candles(candles): - cleaned = [] - for candle in candles: - try: - cleaned.append({ - "open": float(candle[0]), - "high": float(candle[1]), - "low": float(candle[2]), - "close": float(candle[3]), - "volume": float(candle[4]) if len(candle) > 4 else 1.0, - }) - except Exception: - continue - return cleaned +import numpy as np +import pandas as pd -def _atr(candles, n): - if len(candles) < 2: - return 0.0 - alpha = 2.0 / (n + 1.0) - value = None - prev_close = candles[0]["close"] - for candle in candles[1:]: - tr = max( - candle["high"] - candle["low"], - abs(candle["high"] - prev_close), - abs(candle["low"] - prev_close), - ) - value = tr if value is None else value + alpha * (tr - value) - prev_close = candle["close"] - return float(value or 0.0) +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 train(data, config): @@ -41,7 +25,7 @@ def train(data, config): "atr_mult": float(params.get("atr_mult", 0.05)), }, "name": "xauusd_atr_breakout", - }, {"training_bars": int(len(data)), "model": "edge_rule_baseline"} + }, {"training_bars": int(len(data)), "model": "modal_python_rule_baseline"} def predict(model, market_data, config): @@ -50,44 +34,23 @@ def predict(model, market_data, config): 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)) - candles = _clean_candles(market_data.get("candles", [])) - + candles = market_data.get("candles", []) if len(candles) < lookback: - return { - "signal": "HOLD", - "confidence": 0.0, - "metadata": {"reason": "not_enough_candles", "got": len(candles), "need": lookback}, - } + return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_candles"}} - window = candles[-lookback:] - previous = window[-breakout_window - 1:-1] - if len(previous) < breakout_window: - return {"signal": "HOLD", "confidence": 0.0, "metadata": {"reason": "not_enough_breakout_window"}} - - atr = _atr(window, atr_window) - close = window[-1]["close"] - high = max(candle["high"] for candle in previous) - low = min(candle["low"] for candle in previous) + df = _df(candles[-lookback:]) + atr = float(_atr(df, atr_window).iloc[-1]) + close = float(df["close"].iloc[-1]) + 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 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": round(atr, 6), "upper": round(upper, 6), "close": round(close, 6)}, - } + return {"signal": "UP", "confidence": round(min(0.55 + edge, 0.92), 4), "metadata": {"breakout": "high", "atr": atr, "upper": upper}} 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": round(atr, 6), "lower": round(lower, 6), "close": round(close, 6)}, - } + return {"signal": "DOWN", "confidence": round(min(0.55 + edge, 0.92), 4), "metadata": {"breakout": "low", "atr": atr, "lower": lower}} - return { - "signal": "HOLD", - "confidence": 0.2, - "metadata": {"high": round(high, 6), "low": round(low, 6), "atr": round(atr, 6), "upper": round(upper, 6), "lower": round(lower, 6)}, - } + return {"signal": "HOLD", "confidence": 0.2, "metadata": {"high": high, "low": low, "atr": atr, "upper": upper, "lower": lower}}