""" ForexBot2 — Configuration (XAU/USD focused) ========================================== """ from dataclasses import dataclass, field from enum import Enum from typing import Dict, List class Timeframe(Enum): M1 = "1min" M5 = "5min" M15 = "15min" M30 = "30min" H1 = "1h" H4 = "4h" D1 = "1d" W1 = "1wk" class SignalStrength(Enum): STRONG_BUY = 2 BUY = 1 NEUTRAL = 0 SELL = -1 STRONG_SELL = -2 class MarketSession(Enum): SYDNEY = "sydney" TOKYO = "tokyo" LONDON = "london" NEW_YORK = "new_york" OVERLAP_LONDON_NY = "london_ny_overlap" # ── Supported pairs ────────────────────────────────────────────── XAUUSD = "XAU/USD" # price_precision: decimals for SL/TP rounding and display. # default_spread: round-trip cost per trade in price units, charged by # the backtester when --spread is not given. PAIR_INFO = { XAUUSD: {"display": "XAU/USD", "pip_value": 0.10, "is_jpy_pair": False, "price_precision": 2, "default_spread": 0.30}, } @dataclass class TechnicalConfig: """Technical indicator parameters.""" ema_fast: int = 9 ema_medium: int = 21 ema_slow: int = 50 ema_trend: int = 200 rsi_period: int = 14 rsi_overbought: float = 70.0 rsi_oversold: float = 30.0 rsi_neutral_upper: float = 60.0 rsi_neutral_lower: float = 40.0 macd_fast: int = 12 macd_slow: int = 26 macd_signal: int = 9 bb_period: int = 20 bb_std: float = 2.0 atr_period: int = 14 adx_period: int = 14 adx_strong_trend: float = 25.0 adx_very_strong: float = 50.0 divergence_lookback: int = 40 swing_sr_lookback: int = 50 disabled_indicators: List[str] = field(default_factory=list) ema_mode: str = "4ema" # "4ema" (9/21/50/200) or "2ema" (50/200) rsi_mode: str = "regime_aware" # "classic" or "regime_aware" macd_as_pillar: bool = True # MACD histogram is a scored signal # ROC (Rate of Change) roc_period: int = 10 roc_threshold: float = 1.0 # % threshold for strong signal # Keltner Channels kc_period: int = 20 kc_atr_period: int = 10 kc_multiplier: float = 1.5 # ADX slope (trend acceleration detection) adx_slope_lookback: int = 5 adx_slope_emerging_threshold: float = 3.0 regime_weights_override: Dict[str, Dict[str, float]] = field(default_factory=dict) @dataclass class SignalConfig: """Signal scoring & filtering.""" min_confidence: float = 55.0 weights: Dict[str, float] = field(default_factory=lambda: { "ema_trend": 1.0, "rsi": 1.5, "bollinger": 1.5, "macd": 1.5, "keltner": 1.5, "roc": 1.5, "candlestick": 1.0, "divergence": 1.5, }) mtf_agreement_threshold: int = 2 # of 3 timeframes cooldown_bars: int = 5 default_risk_pct: float = 1.0 default_rr_ratio: float = 2.0 atr_sl_multiplier: float = 1.5 min_rr_ratio: float = 0.5 # TP tightening (May 2026) was tuned on the pre-spread backtester. # Re-tested 2026-07-08 with $0.30 spread + gap fills: 0.8 nearly # halves total R (+36.1R vs +66.8R, worse in both train/val halves) # — smaller wins make the fixed spread a larger share of every TP. tp_tightening_factor: float = 1.0 # ── Trade geometry ──────────────────────────────────────────── # Round-trip transaction cost estimate in price units (XAU ~$0.30). round_trip_cost: float = 0.30 # Spread gate: reject setups whose TP1 distance is below # round_trip_cost × this multiple (caps cost at ~3% of target). # Validated 2026-07: small-target trades lose in both data halves # on M5 while TP≥$10 trades earn ~+0.08R/trade. min_tp_cost_multiple: float = 30.0 # Fixed SL floor in pips (80 pips = $8 on XAU). 0 disables — SL then # floors at ATR × v2_sl_atr_multiplier only. min_sl_pips: float = 80.0 # TP1 floor as a fraction of SL distance (0.5 = historical RR 0.5). tp_floor_rr: float = 0.5 # ── Trailing-stop exit (2026-07) ────────────────────────────── # Once favorable excursion reaches trail_arm_r × risk, the stop # trails trail_dist_r × risk behind the best price (updated at bar # close). TP1 becomes advisory — the trail is the exit. Validated # on 12mo M5: +105R vs +48R with the fixed RR-0.5 target, balanced # train/val, positive every quarter, PF 2.27. The RR-0.5 target was # the binding constraint — winners kept running after TP. # Set trail_arm_r = 0 to disable and exit at TP1 as before. trail_arm_r: float = 0.6 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 class NewsConfig: """Economic calendar & sentiment.""" tracked_currencies: List[str] = field(default_factory=lambda: ["USD", "XAU"]) pre_event_buffer_minutes: int = 30 post_event_buffer_minutes: int = 15 # Whether event risk gates the SIGNAL (danger: confidence hard-capped # at 30; caution: 15-point penalty). Warnings are always emitted # regardless. Default False = XAU-tuned: replaying the live gates # against the validated 12mo XAU M5 book (547 trades, +139.9R) # showed they suppress 25-35 trades worth +4.6..+7.2R/yr — trades # entered near USD releases OUTPERFORM (+0.475R vs +0.256R avg). # Audit 2026-07-11 via backtest/research/calendar_store. event_risk_caps_signals: bool = False # Wider stay-out window after a high-impact release that surprised # vs forecast (inline releases use post_event_buffer_minutes). post_event_surprise_buffer_minutes: int = 45 # How long fetched calendar/headline data is reused across scans. # Must stay below pre_event_buffer_minutes so an event can't cross # into the danger window while a stale cache says it's clear. context_cache_ttl_seconds: int = 600 rss_feeds: List[str] = field(default_factory=lambda: [ "https://investinglive.com/feed/", "https://www.fxstreet.com/rss", # Google News query targeted at gold drivers — also carries Kitco, # Reuters etc. whose direct feeds are unavailable. "https://news.google.com/rss/search?q=gold+price+OR+fed+OR+inflation&hl=en-US&gl=US&ceid=US:en", # CNBC "Economy" section "https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=20910258", ]) @dataclass class XauStrategyConfig: """XAU/USD strategy config. Legacy gates (H4 RSI, H1 MACD, M15 score) removed in favor of new momentum indicators (ROC, Keltner squeeze, ADX slope). """ enabled: bool = True rsi_entry_threshold: float = 45.0 tp_atr_multiplier: float = 1.0 sl_atr_multiplier: float = 2.0 min_ema200_tfs: int = 2 allow_generic_trades: bool = True blocked_hours_utc: List[int] = field(default_factory=lambda: [0, 1, 2, 10, 11, 13, 16, 21]) weekend_filter: bool = True min_atr_threshold: float = 1.0 v2_sl_atr_multiplier: float = 2.0 buy_threshold_boost: int = 0 m5_confirmation_bars: int = 0 h4_rsi_gate: bool = False h1_macd_gate: bool = False rsi_zone: tuple = (40.0, 65.0) allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4, 6]) # Mon-Fri + Sun (Sydney open) sell_min_quality: List[str] = field(default_factory=lambda: ["A+", "A", "B"]) # Volatility regimes in which SELL setups are allowed (low/normal-vol # shorts lose consistently against gold's macro uptrend). sell_vol_regimes: List[str] = field(default_factory=lambda: ["high", "extreme"]) # Market regimes in which no XAU setup may fire (any direction). # Empty by default; per-timeframe factories opt in — e.g. the mixed # "trending" regime (strong ADX, EMAs disagree) on M15. blocked_regimes: List[str] = field(default_factory=list) # Market regimes in which XAU SELL setups may not fire. Empty by # default; per-timeframe factories opt in. sell_blocked_regimes: List[str] = field(default_factory=list) # Bar-CLOSE (decision-time) UTC hours in which XAU SELL setups may # not fire. Empty by default; per-timeframe factories opt in. # BUYs are unaffected (unlike blocked_hours_utc, which is both-way). sell_blocked_hours_utc: List[int] = field(default_factory=list) # ── M5-specific filters ────────────────────────────────────── # M5 signals are noisy; require stronger confirmation before # generating trade setups. Without these, M5 is breakeven # (+0.01R/trade) with 21% drawdown. m5_require_mtf: bool = True m5_min_abs_score: float = 30.0 m5_min_confirmation_score: float = 10.0 # A-grade M5 SELLs enter at the extreme of temporary dips and # lose (-0.017R/trade on 163 trades) — maximum conviction at # the worst entry. Cap SELL quality so A-grade can't trigger # the ATR-based setup generator; B-grade swing setups survive # because they catch moderate drops, not extremes. m5_sell_max_quality: List[str] = field(default_factory=lambda: ["B", "C"]) # 'high' vol regime on M5 is chop transition — elevated noise # without directional expansion. 61% WR / negative total across # 12mo while low/normal/extreme are all positive (validated 2026-07). m5_blocked_vol_regimes: List[str] = field(default_factory=lambda: ["high"]) # Block M5 SELLs when RSI is already < this floor: the dip is # extended and mean-reverts before the target (negative in both # halves of 6mo and 12mo backtests). m5_sell_min_rsi: float = 25.0 # ── M5 pullback entry (XAU_PB) ── # 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 class FilterConfig: """Regime and session filters for live signals.""" regime_filter: bool = False regime_filter_timeframe: Timeframe = Timeframe.H4 regime_filter_min_adx: float = 25.0 session_filter: bool = True session_start_hour: int = 3 session_end_hour: int = 23 blocked_hours_utc: List[int] = field(default_factory=lambda: [0, 1, 2, 10, 11, 13, 16, 21]) allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4, 6]) # Mon-Fri + Sun (Sydney open) @dataclass class PlatformConfig: """Top-level configuration.""" technical: TechnicalConfig = field(default_factory=TechnicalConfig) signals: SignalConfig = field(default_factory=SignalConfig) news: NewsConfig = field(default_factory=NewsConfig) backtest: FilterConfig = field(default_factory=FilterConfig) xau_strategy: XauStrategyConfig = field(default_factory=XauStrategyConfig) symbol: str = XAUUSD display_name: str = "XAU/USD" pip_value: float = 0.10 primary_timeframe: Timeframe = Timeframe.H1 confirmation_timeframes: List[Timeframe] = field( default_factory=lambda: [Timeframe.M15, Timeframe.H4] ) data_lookback_days: int = 60 twelve_data_api_key: str = "f563b22ffdfd4924955decbe1a8c8701" refresh_interval: int = 300 scan_pairs: List[str] = field( default_factory=lambda: [XAUUSD] ) scan_timeframes: List[Timeframe] = field( default_factory=lambda: [Timeframe.M5, Timeframe.M15, Timeframe.H1, Timeframe.H4] ) candle_buffer_seconds: int = 10 ws_cooldown_seconds: int = 300 log_level: str = "INFO"