Add M5 pullback entry archetype (XAU_PB): +140R combined on 12mo
Second entry pattern alongside the Keltner-breakout: enter on the retrace to EMA21/KC-mid in an established trend (EMA >=3/4 aligned, ADX>=20, retrace 0.8-6.0 ATR, resumption bar closed back in trend direction, RSI in the pullback zone not the extended zone). Detector lives in the aggregator so live and GPU-backtest paths share one implementation. Own gates (composite floor 15, confirmation-TF agreement, MTF opposition, vol regimes) with a diagnostic funnel. PB stops anchor to the pullback swing (~$8 vs ~$25 breakout), so PB trades get their own trail arm (0.8R vs 0.6R global) via per-trade trail overrides in the backtester and strategy-aware Telegram alerts. 12mo M5 validation: PB alone +38.3R/254 trades, positive both halves and all four full quarters; core breakout trades undisturbed (+101.9R vs +105.0 baseline). Combined book: 549 trades (~10.6/wk), +140.1R, PF 1.78, DD 11.3R vs 311 trades/+105.0R before. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+12
-1
@@ -1,6 +1,17 @@
|
|||||||
PULLBACK ENTRY PATTERN — IMPLEMENTATION PLAN
|
PULLBACK ENTRY PATTERN — IMPLEMENTATION PLAN
|
||||||
=============================================
|
=============================================
|
||||||
Drafted 2026-07-09. Status: PLANNED (not implemented).
|
Drafted 2026-07-09. Status: IMPLEMENTED & VALIDATED (same day).
|
||||||
|
|
||||||
|
OUTCOME: shipped as XAU_PB. 12mo M5 validation: 254 PB trades,
|
||||||
|
+38.3R, avgR +0.151, WR 58%, halves +20.1/+18.1, all four full
|
||||||
|
quarters positive. Core breakout trades undisturbed (-3.1R
|
||||||
|
displacement). Combined book: 549 trades (~10.6/wk), +140.1R,
|
||||||
|
PF 1.78, DD 11.3R. Calibration lesson: M5 gold trend legs measure
|
||||||
|
3-4 ATR swing-to-trough — the drafted 0.5-2.5 ATR retrace window
|
||||||
|
rejected 81% of valid pullbacks (fixed: 0.8-6.0). PB stops are
|
||||||
|
~$8 vs ~$25 core, so PB got its own trail arm (0.8R vs 0.6R).
|
||||||
|
Detector lives in signal_aggregator (NOT engine.py) so the GPU
|
||||||
|
backtest engine exercises the same code path as live.
|
||||||
|
|
||||||
WHY (evidence from 12mo M5 backtests, $0.30 spread, trailing exits)
|
WHY (evidence from 12mo M5 backtests, $0.30 spread, trailing exits)
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
|
|||||||
+26
-13
@@ -1094,7 +1094,10 @@ class Trade:
|
|||||||
final_confidence: float
|
final_confidence: float
|
||||||
market_regime: str
|
market_regime: str
|
||||||
volatility_regime: str = ""
|
volatility_regime: str = ""
|
||||||
strategy_tag: str = "" # "XAU", "EUR_PB", "GBP_PB", "GENERIC"
|
strategy_tag: str = "" # "XAU", "XAU_PB", "EUR_PB", "GBP_PB", "GENERIC"
|
||||||
|
# Per-trade trailing overrides (0 = use the run-level params).
|
||||||
|
trail_arm_r: float = 0.0
|
||||||
|
trail_dist_r: float = 0.0
|
||||||
|
|
||||||
# Per-instructions: entry reasoning and indicator breakdown ─────
|
# Per-instructions: entry reasoning and indicator breakdown ─────
|
||||||
entry_reasons: List[str] = field(default_factory=list)
|
entry_reasons: List[str] = field(default_factory=list)
|
||||||
@@ -1194,6 +1197,9 @@ def _simulate_trailing(
|
|||||||
t.exit_reason = "NO_DATA"
|
t.exit_reason = "NO_DATA"
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
arm = t.trail_arm_r if t.trail_arm_r > 0 else trail_arm_r
|
||||||
|
dist = t.trail_dist_r if t.trail_dist_r > 0 else trail_dist_r
|
||||||
|
|
||||||
cur_sl = t.sl
|
cur_sl = t.sl
|
||||||
peak = t.entry_price
|
peak = t.entry_price
|
||||||
exit_price = None
|
exit_price = None
|
||||||
@@ -1217,9 +1223,9 @@ def _simulate_trailing(
|
|||||||
peak = max(peak, fav) if buy else min(peak, fav)
|
peak = max(peak, fav) if buy else min(peak, fav)
|
||||||
exc = ((peak - t.entry_price) if buy
|
exc = ((peak - t.entry_price) if buy
|
||||||
else (t.entry_price - peak)) / risk
|
else (t.entry_price - peak)) / risk
|
||||||
if exc >= trail_arm_r:
|
if exc >= arm:
|
||||||
lvl = (peak - trail_dist_r * risk if buy
|
lvl = (peak - dist * risk if buy
|
||||||
else peak + trail_dist_r * risk)
|
else peak + dist * risk)
|
||||||
cur_sl = max(cur_sl, lvl) if buy else min(cur_sl, lvl)
|
cur_sl = max(cur_sl, lvl) if buy else min(cur_sl, lvl)
|
||||||
|
|
||||||
if exit_price is None:
|
if exit_price is None:
|
||||||
@@ -1617,16 +1623,19 @@ def run_backtest(
|
|||||||
if report.signal_quality not in bt_cfg.quality_filter:
|
if report.signal_quality not in bt_cfg.quality_filter:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Strategy tag (from bullish_reasons[0] which the aggregator pre-pends).
|
# Strategy tag (from reasons[0] which the aggregator pre-pends).
|
||||||
|
# Pullback SELLs mark bearish_reasons, so check both lists.
|
||||||
tag = "GENERIC"
|
tag = "GENERIC"
|
||||||
if report.bullish_reasons:
|
first_bull = report.bullish_reasons[0] if report.bullish_reasons else ""
|
||||||
first = report.bullish_reasons[0]
|
first_bear = report.bearish_reasons[0] if report.bearish_reasons else ""
|
||||||
if "XAU STRATEGY" in first:
|
if "XAU PULLBACK" in first_bull or "XAU PULLBACK" in first_bear:
|
||||||
tag = "XAU"
|
tag = "XAU_PB"
|
||||||
elif "EUR STRATEGY" in first:
|
elif "XAU STRATEGY" in first_bull:
|
||||||
tag = "EUR_PB"
|
tag = "XAU"
|
||||||
elif "GBP STRATEGY" in first:
|
elif "EUR STRATEGY" in first_bull:
|
||||||
tag = "GBP_PB"
|
tag = "EUR_PB"
|
||||||
|
elif "GBP STRATEGY" in first_bull:
|
||||||
|
tag = "GBP_PB"
|
||||||
|
|
||||||
if bt_cfg.strategies_only and tag not in bt_cfg.strategies_only:
|
if bt_cfg.strategies_only and tag not in bt_cfg.strategies_only:
|
||||||
continue
|
continue
|
||||||
@@ -1684,6 +1693,10 @@ def run_backtest(
|
|||||||
indicators_opposing=oppose,
|
indicators_opposing=oppose,
|
||||||
indicators_neutral=neutral,
|
indicators_neutral=neutral,
|
||||||
indicators_total=total,
|
indicators_total=total,
|
||||||
|
trail_arm_r=(platform_cfg.signals.pb_trail_arm_r
|
||||||
|
if tag == "XAU_PB" else 0.0),
|
||||||
|
trail_dist_r=(platform_cfg.signals.pb_trail_dist_r
|
||||||
|
if tag == "XAU_PB" else 0.0),
|
||||||
))
|
))
|
||||||
last_entry_pos = pos
|
last_entry_pos = pos
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,11 @@ class SignalConfig:
|
|||||||
# Set trail_arm_r = 0 to disable and exit at TP1 as before.
|
# Set trail_arm_r = 0 to disable and exit at TP1 as before.
|
||||||
trail_arm_r: float = 0.6
|
trail_arm_r: float = 0.6
|
||||||
trail_dist_r: float = 0.75
|
trail_dist_r: float = 0.75
|
||||||
|
# Pullback (XAU_PB) trades run tighter stops (~$8 vs ~$25), so the
|
||||||
|
# global 0.6R arm fires on noise. Arm later; whole 0.8-arm row of
|
||||||
|
# the grid is +35-38R vs +32R at global params (balanced halves).
|
||||||
|
pb_trail_arm_r: float = 0.8
|
||||||
|
pb_trail_dist_r: float = 0.75
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -213,6 +218,28 @@ class XauStrategyConfig:
|
|||||||
# halves of 6mo and 12mo backtests).
|
# halves of 6mo and 12mo backtests).
|
||||||
m5_sell_min_rsi: float = 25.0
|
m5_sell_min_rsi: float = 25.0
|
||||||
|
|
||||||
|
# ── M5 pullback entry (XAU_PB — see PULLBACK_ENTRY_PLAN.txt) ──
|
||||||
|
# Second entry archetype: enter on the retrace to EMA21/KC-mid in
|
||||||
|
# an established trend instead of at Keltner-breakout extension.
|
||||||
|
# Validated 2026-07: +38.3R/254 trades on 12mo, positive both
|
||||||
|
# halves and all four full quarters, core trades undisturbed.
|
||||||
|
m5_pullback_enabled: bool = True
|
||||||
|
# Composite score is weaker mid-pullback BY DESIGN (price sits
|
||||||
|
# below the momentum bands) — separate, lower floor than Gate 1.
|
||||||
|
m5_pullback_min_score: float = 15.0
|
||||||
|
m5_pullback_adx_min: float = 20.0
|
||||||
|
# RSI must be in the pullback zone, NOT the extended zone the
|
||||||
|
# breakout entries fire in (those run 65-80).
|
||||||
|
m5_pullback_rsi_buy: tuple = (35.0, 62.0)
|
||||||
|
m5_pullback_rsi_sell: tuple = (38.0, 65.0)
|
||||||
|
# Retrace depth from the pre-pullback swing extreme, in ATRs.
|
||||||
|
# Shallower = still extended; deeper = trend likely broken.
|
||||||
|
# M5 gold trend legs measure 3-4 ATR swing-to-trough (median 3.9),
|
||||||
|
# so the ceiling sits well above that of higher TFs.
|
||||||
|
m5_pullback_retrace_atr: tuple = (0.8, 6.0)
|
||||||
|
# Price must have touched EMA21/KC-mid within this many bars.
|
||||||
|
m5_pullback_touch_bars: int = 3
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FilterConfig:
|
class FilterConfig:
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class TradeSetup:
|
|||||||
risk_pips: float
|
risk_pips: float
|
||||||
reward_pips: float
|
reward_pips: float
|
||||||
pip_value: float
|
pip_value: float
|
||||||
|
# Entry archetype ("GENERIC", "XAU_PB") — selects trail params.
|
||||||
|
strategy: str = "GENERIC"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -93,6 +95,10 @@ class SignalReport:
|
|||||||
class SignalAggregator:
|
class SignalAggregator:
|
||||||
"""Master aggregator for live signal analysis."""
|
"""Master aggregator for live signal analysis."""
|
||||||
|
|
||||||
|
# Diagnostic funnel for the M5 pullback route (shared across
|
||||||
|
# instances; read by backtest tooling, harmless in live use).
|
||||||
|
pb_funnel: Dict[str, int] = {}
|
||||||
|
|
||||||
def __init__(self, config: PlatformConfig):
|
def __init__(self, config: PlatformConfig):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.data_provider = ForexDataProvider(config)
|
self.data_provider = ForexDataProvider(config)
|
||||||
@@ -377,6 +383,183 @@ class SignalAggregator:
|
|||||||
report.trade_setup = None
|
report.trade_setup = None
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# ── M5 pullback entry (XAU_PB) ──────────────────────────────────
|
||||||
|
|
||||||
|
def _detect_m5_pullback(
|
||||||
|
self, report: SignalReport, primary_df: pd.DataFrame
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""Detect a pullback-continuation candidate on the primary TF.
|
||||||
|
|
||||||
|
Trend intact (EMA alignment >= 3/4, ADX floor) + price retraced
|
||||||
|
to EMA21/KC-mid within the last few bars + resumption bar closed
|
||||||
|
back in trend direction + RSI in the pullback zone (not the
|
||||||
|
extended zone breakout entries fire in).
|
||||||
|
"""
|
||||||
|
cfg = self.config.xau_strategy
|
||||||
|
pa = report.primary_analysis
|
||||||
|
if pa is None or len(primary_df) < 60:
|
||||||
|
return None
|
||||||
|
levels = pa.key_levels
|
||||||
|
atr = levels.get("atr", 0.0)
|
||||||
|
ema_f = levels.get("ema_fast")
|
||||||
|
ema_m = levels.get("ema_medium")
|
||||||
|
ema_s = levels.get("ema_slow")
|
||||||
|
ema_t = levels.get("ema_trend")
|
||||||
|
if not atr or None in (ema_f, ema_m, ema_s, ema_t):
|
||||||
|
return None
|
||||||
|
|
||||||
|
adx_ind = pa.indicators.get("adx")
|
||||||
|
rsi_ind = pa.indicators.get("rsi")
|
||||||
|
if adx_ind is None or rsi_ind is None:
|
||||||
|
return None
|
||||||
|
if adx_ind.value < cfg.m5_pullback_adx_min:
|
||||||
|
return None
|
||||||
|
|
||||||
|
close = float(primary_df["Close"].iloc[-1])
|
||||||
|
open_ = float(primary_df["Open"].iloc[-1])
|
||||||
|
n_touch = cfg.m5_pullback_touch_bars
|
||||||
|
recent_low = float(primary_df["Low"].iloc[-n_touch:].min())
|
||||||
|
recent_high = float(primary_df["High"].iloc[-n_touch:].max())
|
||||||
|
# Pullback swing anchor for SL: extreme of the last 8 bars.
|
||||||
|
pb_low = float(primary_df["Low"].iloc[-8:].min())
|
||||||
|
pb_high = float(primary_df["High"].iloc[-8:].max())
|
||||||
|
kc_mid = levels.get("kc_mid", ema_m)
|
||||||
|
lo_atr, hi_atr = cfg.m5_pullback_retrace_atr
|
||||||
|
rsi = rsi_ind.value
|
||||||
|
|
||||||
|
# BUY: uptrend structure, dip to the band, bullish resumption.
|
||||||
|
up_align = sum([ema_f > ema_m, ema_m > ema_s, ema_s > ema_t,
|
||||||
|
close > ema_t])
|
||||||
|
if up_align >= 3:
|
||||||
|
touched = recent_low <= max(ema_m, kc_mid)
|
||||||
|
swing_high = float(primary_df["High"].iloc[-40:].max())
|
||||||
|
depth = (swing_high - pb_low) / atr
|
||||||
|
resumed = close > open_ and close > ema_f
|
||||||
|
lo_z, hi_z = cfg.m5_pullback_rsi_buy
|
||||||
|
if (touched and resumed and lo_atr <= depth <= hi_atr
|
||||||
|
and lo_z <= rsi <= hi_z):
|
||||||
|
return {"direction": "BUY", "anchor": pb_low,
|
||||||
|
"retrace_atr": depth}
|
||||||
|
|
||||||
|
# SELL: mirrored.
|
||||||
|
dn_align = sum([ema_f < ema_m, ema_m < ema_s, ema_s < ema_t,
|
||||||
|
close < ema_t])
|
||||||
|
if dn_align >= 3:
|
||||||
|
touched = recent_high >= min(ema_m, kc_mid)
|
||||||
|
swing_low = float(primary_df["Low"].iloc[-40:].min())
|
||||||
|
depth = (pb_high - swing_low) / atr
|
||||||
|
resumed = close < open_ and close < ema_f
|
||||||
|
lo_z, hi_z = cfg.m5_pullback_rsi_sell
|
||||||
|
if (touched and resumed and lo_atr <= depth <= hi_atr
|
||||||
|
and lo_z <= rsi <= hi_z):
|
||||||
|
return {"direction": "SELL", "anchor": pb_high,
|
||||||
|
"retrace_atr": depth}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _try_m5_pullback(
|
||||||
|
self, report: SignalReport, primary_df: pd.DataFrame, pip_value: float
|
||||||
|
) -> None:
|
||||||
|
"""Build an XAU_PB trade setup if a pullback candidate passes gates."""
|
||||||
|
cfg = self.config.xau_strategy
|
||||||
|
# Rejection funnel counters (class-level diagnostic, cheap).
|
||||||
|
funnel = SignalAggregator.pb_funnel
|
||||||
|
|
||||||
|
def _reject(reason: str) -> None:
|
||||||
|
funnel[reason] = funnel.get(reason, 0) + 1
|
||||||
|
|
||||||
|
pb = self._detect_m5_pullback(report, primary_df)
|
||||||
|
if pb is None:
|
||||||
|
return
|
||||||
|
_reject("detected")
|
||||||
|
direction = pb["direction"]
|
||||||
|
is_buy = direction == "BUY"
|
||||||
|
|
||||||
|
# Directional composite floor (lower than Gate 1 by design —
|
||||||
|
# price mid-pullback scores weaker on the momentum indicators).
|
||||||
|
signed = report.final_score if is_buy else -report.final_score
|
||||||
|
if signed < cfg.m5_pullback_min_score:
|
||||||
|
_reject("score")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Confirmation TF must agree (reuse Gate-3 threshold).
|
||||||
|
min_conf = cfg.m5_min_confirmation_score
|
||||||
|
confirmed = False
|
||||||
|
primary_tf = self.config.primary_timeframe.value
|
||||||
|
for tf_val, tf_score in report.mtf_scores.items():
|
||||||
|
if tf_val == primary_tf:
|
||||||
|
continue
|
||||||
|
if (is_buy and tf_score >= min_conf) or \
|
||||||
|
(not is_buy and tf_score <= -min_conf):
|
||||||
|
confirmed = True
|
||||||
|
break
|
||||||
|
if not confirmed:
|
||||||
|
_reject("confirmation_tf")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Never against multi-TF consensus (mirror of gate 6f).
|
||||||
|
sigs = list(report.mtf_signals.values())
|
||||||
|
bull_n = sum(1 for s in sigs if s in (SignalStrength.BUY, SignalStrength.STRONG_BUY))
|
||||||
|
bear_n = sum(1 for s in sigs if s in (SignalStrength.SELL, SignalStrength.STRONG_SELL))
|
||||||
|
thr = self.config.signals.mtf_agreement_threshold
|
||||||
|
if (is_buy and bear_n >= thr) or (not is_buy and bull_n >= thr):
|
||||||
|
_reject("mtf_opposition")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Volatility gates match the breakout path.
|
||||||
|
if report.volatility_regime in cfg.m5_blocked_vol_regimes:
|
||||||
|
_reject("vol_regime")
|
||||||
|
return
|
||||||
|
if (not is_buy and report.volatility_regime
|
||||||
|
and report.volatility_regime not in cfg.sell_vol_regimes):
|
||||||
|
_reject("sell_vol")
|
||||||
|
return
|
||||||
|
|
||||||
|
atr = report.primary_analysis.key_levels.get("atr", 0.0)
|
||||||
|
entry = report.current_price
|
||||||
|
min_sl_price = self.config.signals.min_sl_pips * pip_value
|
||||||
|
if is_buy:
|
||||||
|
sl_dist = max(entry - (pb["anchor"] - 0.5 * atr), min_sl_price)
|
||||||
|
sl = entry - sl_dist
|
||||||
|
tp1 = entry + 1.5 * sl_dist
|
||||||
|
tp2 = entry + 2.5 * sl_dist
|
||||||
|
else:
|
||||||
|
sl_dist = max((pb["anchor"] + 0.5 * atr) - entry, min_sl_price)
|
||||||
|
sl = entry + sl_dist
|
||||||
|
tp1 = entry - 1.5 * sl_dist
|
||||||
|
tp2 = entry - 2.5 * sl_dist
|
||||||
|
|
||||||
|
# Spread gate — same economics as the breakout generators.
|
||||||
|
min_tp = (self.config.signals.round_trip_cost
|
||||||
|
* self.config.signals.min_tp_cost_multiple)
|
||||||
|
if 1.5 * sl_dist < min_tp:
|
||||||
|
return
|
||||||
|
|
||||||
|
report.trade_setup = TradeSetup(
|
||||||
|
pair=report.symbol,
|
||||||
|
display_name=report.display_name,
|
||||||
|
direction=direction,
|
||||||
|
entry_price=entry,
|
||||||
|
stop_loss=sl,
|
||||||
|
take_profit_1=tp1,
|
||||||
|
take_profit_2=tp2,
|
||||||
|
risk_reward=1.5,
|
||||||
|
risk_pips=sl_dist / pip_value,
|
||||||
|
reward_pips=1.5 * sl_dist / pip_value,
|
||||||
|
pip_value=pip_value,
|
||||||
|
strategy="XAU_PB",
|
||||||
|
)
|
||||||
|
if report.signal_quality not in ("A+", "A", "B"):
|
||||||
|
report.signal_quality = "B"
|
||||||
|
report.final_signal = (SignalStrength.BUY if is_buy
|
||||||
|
else SignalStrength.SELL)
|
||||||
|
report.action_recommendation = (
|
||||||
|
f"{direction} — pullback continuation entry"
|
||||||
|
)
|
||||||
|
marker = (f"XAU PULLBACK STRATEGY: retrace {pb['retrace_atr']:.1f} "
|
||||||
|
f"ATR to EMA21/KC-mid, trend resumed")
|
||||||
|
target = report.bullish_reasons if is_buy else report.bearish_reasons
|
||||||
|
target.insert(0, marker)
|
||||||
|
|
||||||
# ── Public: bar-replayable entry point ─────────────────────────
|
# ── Public: bar-replayable entry point ─────────────────────────
|
||||||
|
|
||||||
def analyze_from_dataframes(
|
def analyze_from_dataframes(
|
||||||
@@ -577,6 +760,16 @@ class SignalAggregator:
|
|||||||
# ── 8. Reasons ─────────────────────────────────────────────
|
# ── 8. Reasons ─────────────────────────────────────────────
|
||||||
self._collect_reasons(report)
|
self._collect_reasons(report)
|
||||||
|
|
||||||
|
# ── 9. M5 pullback entry (XAU_PB) ──────────────────────────
|
||||||
|
# Second archetype: when no breakout setup fired, look for a
|
||||||
|
# trend-continuation pullback (retrace to EMA21/KC-mid). Runs
|
||||||
|
# after reasons so its marker lands at reasons[0] for tagging.
|
||||||
|
if (self.config.primary_timeframe == Timeframe.M5
|
||||||
|
and self.config.xau_strategy.m5_pullback_enabled
|
||||||
|
and report.trade_setup is None
|
||||||
|
and report.signal_quality != "F"):
|
||||||
|
self._try_m5_pullback(report, primary_df, pip_value)
|
||||||
|
|
||||||
return report
|
return report
|
||||||
|
|
||||||
# ── Internals ──────────────────────────────────────────────────
|
# ── Internals ──────────────────────────────────────────────────
|
||||||
|
|||||||
+12
-7
@@ -255,18 +255,23 @@ def format_signal_message(report: SignalReport) -> str:
|
|||||||
lines.append(f"⚖️ RR: 1:{ts.risk_reward:.1f}")
|
lines.append(f"⚖️ RR: 1:{ts.risk_reward:.1f}")
|
||||||
|
|
||||||
# Trailing-stop management plan (matches backtested exit model:
|
# Trailing-stop management plan (matches backtested exit model:
|
||||||
# TP1 is advisory, the trail is the exit).
|
# TP1 is advisory, the trail is the exit). Pullback setups run
|
||||||
|
# tighter stops and arm later.
|
||||||
sig_cfg = PlatformConfig().signals
|
sig_cfg = PlatformConfig().signals
|
||||||
if sig_cfg.trail_arm_r > 0:
|
is_pb = getattr(ts, "strategy", "GENERIC") == "XAU_PB"
|
||||||
|
arm_r = sig_cfg.pb_trail_arm_r if is_pb else sig_cfg.trail_arm_r
|
||||||
|
dist_r = sig_cfg.pb_trail_dist_r if is_pb else sig_cfg.trail_dist_r
|
||||||
|
if arm_r > 0:
|
||||||
risk_dist = abs(ts.entry_price - ts.stop_loss)
|
risk_dist = abs(ts.entry_price - ts.stop_loss)
|
||||||
if ts.direction == "BUY":
|
if ts.direction == "BUY":
|
||||||
arm_price = ts.entry_price + sig_cfg.trail_arm_r * risk_dist
|
arm_price = ts.entry_price + arm_r * risk_dist
|
||||||
else:
|
else:
|
||||||
arm_price = ts.entry_price - sig_cfg.trail_arm_r * risk_dist
|
arm_price = ts.entry_price - arm_r * risk_dist
|
||||||
trail_dist = sig_cfg.trail_dist_r * risk_dist
|
trail_dist = dist_r * risk_dist
|
||||||
|
tag = " (pullback)" if is_pb else ""
|
||||||
lines.append(
|
lines.append(
|
||||||
f"🔄 Trail: once price hits `{_fmt_price(arm_price)}` "
|
f"🔄 Trail{tag}: once price hits `{_fmt_price(arm_price)}` "
|
||||||
f"(+{sig_cfg.trail_arm_r:.1f}R), trail SL ${trail_dist:.2f} "
|
f"(+{arm_r:.1f}R), trail SL ${trail_dist:.2f} "
|
||||||
f"behind best price — TP1 is a guide, let it run"
|
f"behind best price — TP1 is a guide, let it run"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user