Claude-Session: https://claude.ai/code/session_01VPNF37MP9Kuv62B1VBrZ1b
7.8 KiB
Centralized Configuration (config.json) — Design
Date: 2026-07-21 Status: Approved (user confirmed all four scoping decisions)
Goal
Move deployment settings — Telegram credentials, data-provider API keys and
selection, scanned pairs, scan timeframes, per-pair base blocked hours, alert
quality boot default, log level — into a single gitignored config.json at the
repo root, loaded once at startup.
Hard constraint: zero change to signal logic, indicator weights, thresholds,
or gates. With an unedited config.json (or none at all), every produced
PlatformConfig must be byte-identical to today's factory output. This is
test-enforced (see Testing).
Scoping decisions (user-confirmed 2026-07-21)
- Secrets live in config.json (gitignored). A committed
config.example.jsoncarries placeholders. Env vars still override. The hardcoded credential fallbacks inconfig.pyandtelegram_bot.pyare deleted — on a machine with neither config.json nor env vars, the bot refuses to start with an explicit error instead of silently using committed credentials. (Recommend rotating the Telegram token and Twelve Data key after merge; both are exposed in git history.) - Multi-provider is structure only: a
data_providerssection with named provider blocks and anactiveselector. Only Twelve Data is implemented. No failover logic (nothing to fail over to yet). - Blocked hours: base list per pair only. The validated per-timeframe
deltas (M5 appends hour 9; M15 SELL-block 15/17 UTC) stay in
pair_signals/xauusd.pynext to their validation evidence. The overlay is applied before those deltas and updates both mirrored copies (xau_strategy.blocked_hours_utcandbacktest.blocked_hours_utc) so they cannot drift. - Extra keys included: scan timeframes, alert min quality (boot default), log level. Everything else (indicator params, RR geometry, M5 gates, CP knobs, news config) deliberately stays code-only.
File schema
config.json (gitignored; config.example.json committed with placeholders
for secrets and today's tuned values for everything else):
{
"telegram": {
"token": "<bot token>",
"chat_id": "<owner chat id>",
"channel_id": "<-100… or empty string to disable the channel feed>",
"alert_min_quality": "C"
},
"data_providers": {
"active": "twelve_data",
"twelve_data": { "api_key": "<key>" }
},
"scanning": {
"pairs": ["XAU/USD"],
"timeframes": ["M5", "M15", "H1", "H4"]
},
"pairs": {
"XAU/USD": { "blocked_hours_utc": [0, 1, 2, 10, 11, 13, 16, 21] }
},
"log_level": "INFO"
}
Timeframe strings use the Timeframe enum names (M5, M15, H1, H4, …).
Pair keys use canonical symbols (XAU/USD).
Architecture
New module: app_config.py
Single responsibility: read + validate config.json, expose typed accessors,
apply the pair-level overlay. No signal knowledge.
AppConfigdataclass:telegram(token, chat_id, channel_id, alert_min_quality),data_providers(active name + per-provider dicts),scan_pairs,scan_timeframes,pair_overrides(symbol → {blocked_hours_utc}),log_level.load_app_config(path=REPO_ROOT/"config.json") -> AppConfig— module-level cached (loaded once per process). Missing file →AppConfigof pure defaults + one warning log. Malformed JSON, unknown top-level/section keys, unknown pair symbols, hours outside 0–23, unknown timeframe names, unknownactiveprovider → raiseAppConfigErrorwith a named message (fail fast at boot).- Precedence, applied inside the accessors:
env var → config.json → code default. Secrets (
TELEGRAM_TOKEN,TELEGRAM_CHAT_ID,TELEGRAM_CHANNEL_ID,TWELVE_DATA_API_KEY) have no code default after this change. apply_pair_overrides(cfg: PlatformConfig, app: AppConfig) -> PlatformConfig— sets the pair's baseblocked_hours_utcon bothcfg.xau_strategy.blocked_hours_utcandcfg.backtest.blocked_hours_utc, pluscfg.scan_pairs/cfg.scan_timeframes. Called by the pair factory (see below). A no-op when the pair has no override block.
Wiring points (all consumers)
| Consumer | Change |
|---|---|
pair_signals/xauusd.py |
make_config calls apply_pair_overrides before the per-TF deltas (so M5 still appends hour 9 to whatever base the JSON sets, and mirrors stay in sync). |
data_provider.py |
API key resolution becomes app_config accessor for the active provider (env TWELVE_DATA_API_KEY still wins). config.twelve_data_api_key field and its hardcoded default are deleted. Missing key keeps today's behavior: provider constructed with _td=None, errors on first fetch. |
telegram_bot.py |
TOKEN, CHAT_ID, CHANNEL_ID come from app_config (hardcoded fallbacks deleted; missing token/chat_id → explicit startup error). _alert_min_quality boot default and logging level come from app_config. SCAN_PAIRS (today built from a bare PlatformConfig()) reads app_config.scan_pairs directly; apply_pair_overrides also stamps cfg.scan_pairs/cfg.scan_timeframes so the dataclass field stays truthful for any other reader. |
main.py |
No structural change — it uses the factory, which now applies the overlay. |
backtest/gpu_backtest.py |
No structural change — it uses the factory too, so an edited base blocked-hours list flows into replays identically to live (parity by construction). Data fetches pick up the provider key via data_provider.py. |
config.py |
twelve_data_api_key field removed. Everything else untouched. |
Multi-provider structure
data_provider.py gains a minimal registry:
PROVIDER_REGISTRY = {"twelve_data": ForexDataProvider}. active selects
from it; an unknown name fails at load. Adding a provider later = one config
block + one adapter class registered here. No shared base class extracted now
(YAGNI — with one provider there is nothing to abstract over yet).
Error handling summary
| Condition | Behavior |
|---|---|
| No config.json | Defaults + warning. Secrets must come from env or startup fails (bot) / fetch fails (provider). |
| Malformed JSON / bad values / unknown keys | AppConfigError at load, process exits with named message. |
| Missing Telegram token or chat_id at bot boot | Explicit startup error naming the missing key and both places it can be set. |
| Missing data-provider key | Same as today: provider builds, first fetch errors. |
Migration
- Create the local
config.jsonpopulated with the current live values (the ones being deleted from source) so the running bot keeps working unchanged..gitignoregainsconfig.json. - Commit
config.example.jsonwith placeholders. - CLAUDE.md: update the env-var table, repository map, and config section.
- Post-merge recommendation (user action): rotate the Telegram token and Twelve Data key, since both are in git history.
Testing (test_all.py, new module)
- Parity pin (the critical one): a config.json containing the example defaults, and no config.json at all, both produce factory output identical to today's (full dataclass equality across all four TFs).
- Overlay mapping: JSON blocked hours land on both mirrored fields; M5 factory still appends hour 9 on top of a custom base.
- Precedence: env var beats config.json beats default, per key.
- Validation: malformed JSON, bad hour, unknown pair/timeframe/provider each
raise
AppConfigError. - Missing-file behavior: defaults + no crash.
- Existing factory-equivalence test stays green (equivalence is now defined over factory-with-overlay vs inline-config-with-overlay).
Out of scope
Hot reload (config is read once at boot), a second concrete provider, failover, moving any tuned strategy value beyond the base blocked-hours list, news/indicator/geometry settings in JSON.