Files
ForexBot2/config.py
T
Harwinder Sekhon 44cadf7a9f Data provider: key from config.json/env, provider registry, drop hardcoded key
Resolves the Twelve Data API key through app_config.provider_api_key()
(env → config.json → no default) instead of a hardcoded field on
PlatformConfig. Adds data_provider.PROVIDER_REGISTRY + make_data_provider()
so signal_aggregator and the backtester build the active provider through
one seam; telegram_bot.py gets the same one-line WS-key fix now (per Task 4's
already-planned edit for that line) so the suite doesn't crash on import
between this commit and Task 4's full wiring pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPNF37MP9Kuv62B1VBrZ1b
2026-07-21 13:10:58 -04:00

358 lines
15 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
# 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
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
# ── 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
@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
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"