183f148bb4
Replaying the live danger/caution gates against the validated 12mo XAU M5 book showed they suppress 25-35 trades worth +4.6..+7.2R/yr — trades entered near USD releases outperform the book average (+0.475R vs +0.256R). New NewsConfig.event_risk_caps_signals (default False = XAU-tuned) keeps EVENT RISK warnings in alerts but no longer guts confidence; EUR factory re-enables the caps (evidence is gold-specific). Pinned XAU M5 regression: 547 trades, +139.94R, trade-identical. Tests: 177 passed.
368 lines
14 KiB
Python
Executable File
368 lines
14 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
|
||
|
||
|
||
class MarketSession(Enum):
|
||
SYDNEY = "sydney"
|
||
TOKYO = "tokyo"
|
||
LONDON = "london"
|
||
NEW_YORK = "new_york"
|
||
OVERLAP_LONDON_NY = "london_ny_overlap"
|
||
|
||
|
||
# ── Supported pairs ──────────────────────────────────────────────
|
||
XAUUSD = "XAU/USD"
|
||
EURUSD = "EUR/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},
|
||
EURUSD: {"display": "EUR/USD", "pip_value": 0.0001, "is_jpy_pair": False,
|
||
"price_precision": 5, "default_spread": 0.0001},
|
||
}
|
||
|
||
|
||
@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", "EUR"])
|
||
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"])
|
||
|
||
# ── 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 — 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
|
||
class EurUsdStrategyConfig:
|
||
"""EUR/USD strategy config — separate from XAU for independent tuning.
|
||
|
||
Values ported from feature/eurusd-support are a pre-realism baseline
|
||
(tuned before spread charging and next-open fills) and are re-tuned
|
||
against the realistic backtester. Cost knobs (round_trip_cost,
|
||
min_tp_cost_multiple, min_sl_pips) are EUR-local so nothing
|
||
gold-calibrated from SignalConfig leaks into the EUR path.
|
||
"""
|
||
enabled: bool = True
|
||
blocked_hours_utc: List[int] = field(default_factory=lambda: [22, 23, 0, 1, 2, 3])
|
||
weekend_filter: bool = True
|
||
allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4])
|
||
min_atr_threshold: float = 0.0003
|
||
sl_atr_multiplier: float = 1.5 # swing-buffer ATR multiple for SL placement
|
||
tp_atr_multiplier: float = 1.2
|
||
|
||
sl_atr_floor: float = 1.2
|
||
min_rr_ratio: float = 1.0
|
||
mature_move_threshold: float = 35.0
|
||
tp_quality_scale: Dict[str, float] = field(default_factory=lambda: {
|
||
"A+": 1.0, "A": 1.0, "B": 1.0, "C": 1.0,
|
||
})
|
||
blocked_regimes: List[str] = field(default_factory=lambda: ["ranging", "transitioning"])
|
||
min_quality_grades: List[str] = field(default_factory=lambda: ["A+", "A", "B"])
|
||
max_exhaustion_tags: int = 99
|
||
block_near_ema200: bool = False
|
||
session_hours: List[int] = field(default_factory=list)
|
||
min_entry_score: float = 38.0
|
||
min_entry_confidence: float = 60.0
|
||
block_rsi_extended: bool = True
|
||
blocked_vol_regimes: List[str] = field(default_factory=list)
|
||
min_indicator_agreement: int = 0
|
||
be_trigger_r: float = 0.3
|
||
be_buffer_r: float = 0.15
|
||
|
||
# ── EUR-local trade-cost knobs (replace SignalConfig's gold-dollar
|
||
# values in the EUR path) ─────────────────────────────────────────
|
||
# Round-trip cost per trade in price units (1.0 pip).
|
||
round_trip_cost: float = 0.0001
|
||
# Reject setups whose TP1 distance < round_trip_cost × this multiple
|
||
# (10 → 10-pip minimum target; EUR M5 ATR runs ~5-8 pips).
|
||
min_tp_cost_multiple: float = 10.0
|
||
# Minimum SL distance in pips.
|
||
min_sl_pips: float = 8.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)
|
||
eur_strategy: EurUsdStrategyConfig = field(default_factory=EurUsdStrategyConfig)
|
||
|
||
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"
|