1793d9167a
Two bodies of work that touch the same files, so one commit. Direction and setup-size campaign (2026-09-30 → 10-01, owner's RISK-FIRST rule, written before the runs): - SIGNAL: M15 daily-trend veto (factory override 7, xau_strategy.daily_trend_veto "both"). A finished setup against a clear daily trend (closed D1 bars, Sunday stubs dropped; close vs its 50-bar EMA AND vs the close 60 kept days earlier) is removed as the last step of the core. One definition (signal_aggregator.daily_trend_series) for live, the bar replay and the twin's host side; D1 is a required frame of the M15 scan; mirrored in cuda_signal_kernels.py K2. W3 824 / +205.12R / PF 1.73 / max DD 12.4R vs 1105 / +195.79R / 1.49 / 20.9R; 2019 → 2026 1473 / +223.33R / 1.42 / 41.5R vs 2091 / +192.53R / 1.24 / 64.8R; unseen holdout −4.62R vs −6.63R. - FAILED, not shipped: the veto on M5 (W3 drawdown 10.1R → 15.2R) and on H1 (−9.7R), buy-only and sell-only on M15, the wide-stop cap (xau_strategy.wide_stop_cap_atr stays 0.0, a research knob). - New M15 pins: W2 dfc647ee…, OOS e87b6de1…, W3 ad3b0dbd…. M5 / H1 / H4 pins unchanged. --set xau_strategy.daily_trend_veto=off reproduces the previous M15 pins byte for byte. Twin and reference script MD5-equal on every window; recorder and feature-export parity re-proved; base-rate table rebuilt. - DISPLAY (books byte-identical): exposure.py — the lot size for trade_size.risk_per_trade (new config.json block, off by default, private to the owner) and the count of alerted same-direction plans still open (signals.alerted_at, read-only open_alerted_setups); the daily trend in words on Telegram, the CLI and the dashboard; signals.daily_trend logged. - providers/oanda: a date window with no bars is an empty page, not an error (a D1 pad starting on a Saturday could not be fetched). - Reports only, nothing shipped: seven context screens (no state nominated) and eight outside data sources (none adds a forecast to gold's own daily trend). - Independent verification (report section 5.13): every figure reproduced on both scripts, scoring identical to the previous commit on all four timeframes, no look-ahead. Caveats recorded there: this is a risk filter, not a more accurate direction call; nine tenths of the 2019 → 2026 gain is 2022; the neighbour clause is thin (a simple 100-day average misses the R line by 1.2R); "never adds a trade" is an observation, not a guarantee. Bug review of the whole codebase (2026-10-01). Signal path clean. 17 defects elsewhere, plus 4 found by an independent review of the fixes; all fixed, each with a test; no trade book changed: - telegram_bot: one _deliver_alert for the candle scan and the level break. The row is stamped alerted_at BEFORE the send, on a one-worker alert-db pool, and un-stamped when no chat received it or the task is cancelled mid-stamp, so two alerts at one candle close count each other and no SQLite write runs on the event loop. The 5-bar cooldown counts bars of the scan's own frame (== gpu_backtest._apply_cooldown; clock time released the 22:00 UTC bar after the settlement pause) and is restored from signals.db at start-up. The lot-size line goes to the owner's own chat only (a group the owner ran /autoscan in used to receive it). /status gains a news-model line and escapes failure details. - signal_log: mark_alerted returns the stamp it wrote; unmark_alerted; last_alerted_bars (read-only). - advisory_outcome: candle continuity is judged bar by bar inside the walk; a hole after a trade's exit no longer voids it. The stored-candle replay resolves 108 of 111 logged setups instead of 99, all equal to the live resolver's R. - providers/oanda: an empty date window is accepted only when it is 5 days or shorter and no malformed candle was skipped; a longer one is retried, then refused, instead of being cached as history with a hole. - news_engine: blank RSS titles skipped; one unreadable headline no longer costs the local model its whole batch (checked on the real bundle); model health recorded; no DeepSeek audit call in a process that does not archive; event-direction cache keyed per release; peek_week_events never fetches. - llm_client: chat_json never raises and never logs the API key (requests quotes the Authorization value in InvalidHeader; error bodies are redacted). - signal_aggregator: the scan's news block is guarded; the replay path (use_news=False) assigns exactly the values it did before. - agent_panel: PANEL_PROMPT_VERSION v2.2 — same prompt text, corrected input (event timing over the cached calendar week, headlines newest first). scripts/panel_replay.py replays the logged payload per persona; scripts/label_scan_outcomes.py counts with an ISO cutoff. - test_all.py: no ALTER TABLE … DROP COLUMN (the live box has SQLite 3.34); tests that write Parquet print a notice and return without pyarrow. Verification after the last code change: suite 632 passed, 0 failed (also with both Parquet engines blocked and FB2_BACKEND=cpu, the live box's view); the five reference MD5s reproduce on both backtesters, 0 cache fallbacks; Module 37 green; static Python 3.9 / SQLite 3.34 check clean on every touched file. Evidence: backtest/research/daily_trend_campaign_2026-09-30.md, context_screens_2026-10.md, new_inputs_2026-10.md; CLAUDE.md sections 2, 4, 6, 8, 9, 10 and 11; pair_signals/xauusd.py docstring (M15 override 7). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
397 lines
18 KiB
Python
Executable File
397 lines
18 KiB
Python
Executable File
"""
|
||
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
|
||
|
||
|
||
# ── Supported pairs ──────────────────────────────────────────────
|
||
XAUUSD = "XAU/USD"
|
||
|
||
# price_precision: decimals for SL/TP rounding and display.
|
||
# default_spread: $0.30 round-trip cost retained for TD-era cross-book
|
||
# comparability and used when the backtester receives no --spread override.
|
||
# Oanda's measured round trip is $0.65-$0.72; research reports headline it
|
||
# separately with --spread 0.65.
|
||
PAIR_INFO = {
|
||
XAUUSD: {"display": "XAU/USD", "pip_value": 0.10,
|
||
"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
|
||
# 4-EMA mode only: score partial ribbon alignment (2-3 of 4 checks)
|
||
# as NEUTRAL instead of a weak BUY/SELL. Research knob (2026-08-04
|
||
# indicator audit: partial-alignment trades averaged -0.008R vs
|
||
# +0.230R fully aligned on the M5 12mo Oanda book); default False =
|
||
# legacy behavior until a replay campaign validates flipping it.
|
||
ema_partial_neutral: bool = False
|
||
|
||
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."""
|
||
mtf_agreement_threshold: int = 2 # of 3 timeframes
|
||
# RESEARCH-ONLY grading rule switch (campaign 2026-09-27,
|
||
# backtest/research/conviction_campaign_2026-09-27.md). "pair" is the
|
||
# shipped two-number rule (a score floor AND a confidence floor, the
|
||
# literal table in signal_aggregator._quality_pair); "min" and "product"
|
||
# grade on ONE conviction number (min(|score|, 0.6 x confidence) or
|
||
# |score| x confidence / 100, thresholds in
|
||
# signal_aggregator.CONVICTION_THRESHOLDS). Read by
|
||
# SignalAggregator._compute_final_signal and gate 6c; packed for the CUDA
|
||
# twin. Any value other than "pair" is a rule-1 event.
|
||
grading_mode: str = "pair"
|
||
|
||
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 ────────────────────────────────────────────
|
||
# Deliberately $0.30 for TD-era cross-book comparability. Oanda's measured
|
||
# round trip is $0.65-$0.72, reported separately in re-pin campaigns.
|
||
# This is also a LIVE gate input: increasing it to $0.65 raises the TP1
|
||
# floor from about $9 to $19.50, so it requires rule-1 revalidation.
|
||
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).
|
||
# Part of the setup geometry the gates judge (min-RR, spread gate).
|
||
tp_floor_rr: float = 0.5
|
||
# Printed-target floor (2026-09-26, owner rule "TP1 at least 1:1"):
|
||
# every generator raises the PRINTED TP1 to at least this fraction
|
||
# of the stop AFTER the min-RR and spread gates have judged the
|
||
# generator's own target, so which generator serves a signal (and
|
||
# the stop) never changes and the books stay trade-identical; only
|
||
# the exported tp1 / rr_planned columns move. TP1 is advisory in the
|
||
# tested exit (the trail), so this is a guide, not a book change.
|
||
# The effective floor is max(tp_floor_rr, tp_print_floor_rr).
|
||
tp_print_floor_rr: float = 1.0
|
||
|
||
# ── 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 (calendar-replay study; tooling since removed).
|
||
event_risk_caps_signals: bool = False
|
||
# Whether headline sentiment moves the SIGNAL (aligned: +5 confidence;
|
||
# conflicting: 10-point penalty). Warnings are always emitted
|
||
# regardless. Default False = news is informational only; decisions
|
||
# are pure technical, matching the backtester (use_news=False).
|
||
sentiment_affects_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
|
||
# Maximum age of RSS headlines accepted for scoring. Older headlines
|
||
# (and headlines with no parsable publish time) are discarded before
|
||
# keyword analysis and LLM enrichment. 12 h → 4 h on 2026-09-04: the
|
||
# 3-week signal log showed the 12 h mean was a trailing echo of the
|
||
# previous day's price move (see backtest/research/
|
||
# news_sentiment_value_2026-09-04.md). Display/warning-only either way.
|
||
headline_max_age_seconds: int = 14400 # 4 hours
|
||
|
||
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."""
|
||
enabled: bool = True
|
||
tp_atr_multiplier: float = 1.0
|
||
sl_atr_multiplier: float = 2.0
|
||
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
|
||
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.
|
||
# Relaxed 2026-07-30 (30→25, 10→5) in the relaxation campaign:
|
||
# the combined config (with the M5 hour-21 unblock) beat control
|
||
# in ALL 6 calendar years of the 5yr Oanda replay (+230.9R vs
|
||
# +168.0R, PF 1.26 vs 1.21, max DD 21.3 vs 27.0), 12mo +107.4R
|
||
# vs +84.6R PF 1.36 both halves positive, OOS 2024-06→2025-03
|
||
# +26.1R vs +17.2R, and at $0.70 spread +86.9R vs +66.4R.
|
||
# Score floors below 25 are vacuous (grade C requires |score|≥25).
|
||
m5_require_mtf: bool = True
|
||
m5_min_abs_score: float = 25.0
|
||
m5_min_confirmation_score: float = 5.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
|
||
|
||
# ── Chart-pattern entry (XAU_CP) ──────────────────────────────
|
||
# Third entry archetype: completion of a classical chart pattern
|
||
# (see chart_patterns.py for the pattern set and design contract).
|
||
# Defaults are textbook geometry, NOT tuned values. Disabled by
|
||
# default — per-TF adoption happens in pair_signals/xauusd.py only
|
||
# after the pattern book passes the standard validation rule
|
||
# (positive R both halves of ≥12mo, PF ≥ 1.3, $0.30 spread,
|
||
# trailing exits, next-open fills).
|
||
cp_enabled: bool = False
|
||
cp_patterns: List[str] = field(default_factory=lambda: [
|
||
"inv_hs", "hs_top", "double_bottom", "double_top",
|
||
"bull_flag", "bear_flag",
|
||
])
|
||
cp_lookback: int = 72 # bars scanned for pattern structure
|
||
cp_swing_window: int = 3 # bars each side to confirm a swing
|
||
cp_recency_bars: int = 20 # last extreme must be this fresh
|
||
cp_min_sep_bars: int = 8 # min bars between double extremes
|
||
cp_tolerance_atr: float = 0.5 # double top/bottom level match tol
|
||
cp_min_height_atr: float = 1.5 # pattern height floor (noise gate)
|
||
cp_max_height_atr: float = 8.0 # pattern height ceiling
|
||
cp_hs_head_min_atr: float = 0.5 # head must exceed shoulders by this
|
||
cp_hs_shoulder_tol_atr: float = 1.0 # shoulder level match tol
|
||
cp_flag_pole_atr: float = 3.0 # min pole size
|
||
cp_flag_pole_bars: int = 12 # pole measured over ≤ this many bars
|
||
cp_flag_min_bars: int = 3 # flag consolidation length bounds
|
||
cp_flag_max_bars: int = 12
|
||
cp_flag_retrace: tuple = (0.1, 0.5) # flag depth as fraction of pole
|
||
|
||
# ── Big-candle entry gate ──────────────────────────────────────
|
||
# When the recent window on the scan's own primary TF contains an
|
||
# abnormally large candle (bar range >= K x the PRIOR bar's ATR —
|
||
# institutional burst; price may reverse violently), no XAU entry
|
||
# of ANY kind may fire (core, XAU_PB, XAU_CP). 0.0 = disabled.
|
||
# Per-TF factories opt in only after the standard validation rule.
|
||
big_candle_atr_multiple: float = 0.0
|
||
big_candle_lookback_hours: float = 24.0
|
||
|
||
# ── Daily-trend veto (2026-09-30) ──────────────────────────────
|
||
# The engine sees at most the H1 chart, and on M15 trades against a
|
||
# CLEAR daily trend earn nothing after costs. The daily trend is +1 /
|
||
# -1 when the last closed D1 close sits above / below its 50-bar EMA
|
||
# AND above / below the close 60 daily bars earlier, else 0
|
||
# (signal_aggregator.daily_trend_series; Sunday stub bars dropped).
|
||
# "both" removes any finished setup that goes against a non-zero
|
||
# daily trend, "buy" only BUYs against a falling one, "sell" only
|
||
# SELLs against a rising one, "off" nothing. Off by default; the M15
|
||
# factory sets "both" (override 7). It FAILED its rule on M5 and H1
|
||
# and as "buy" / "sell" on M15
|
||
# (backtest/research/daily_trend_campaign_2026-09-30.md).
|
||
daily_trend_veto: str = "off"
|
||
|
||
# ── Wide-stop cap (2026-09-30, research knob) ──────────────────
|
||
# Caps the stop distance of the swing (GENERIC) and chart-pattern
|
||
# setups at max(K x ATR, the setup's own stop floor). Applied AFTER
|
||
# the min-RR and spread gates, like the printed-target floor, so
|
||
# the set of trades cannot change. 0.0 = off, and it ships off
|
||
# everywhere: K = 6 FAILED its rule on M15 (a narrow peak; K = 5
|
||
# loses 18.6R over 2019-2026). Same campaign report.
|
||
wide_stop_cap_atr: float = 0.0
|
||
|
||
|
||
@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"
|
||
|
||
primary_timeframe: Timeframe = Timeframe.H1
|
||
confirmation_timeframes: List[Timeframe] = field(
|
||
default_factory=lambda: [Timeframe.M15, Timeframe.H4]
|
||
)
|
||
|
||
data_lookback_days: int = 60
|
||
|
||
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]
|
||
)
|
||
ws_cooldown_seconds: int = 300
|
||
|
||
log_level: str = "INFO"
|