From bd59b5cd5bebbb4118f7fc506a3ebf53489bf0e3 Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Tue, 21 Jul 2026 12:25:08 -0400 Subject: [PATCH] Spec: centralized config.json design (deployment settings overlay) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VPNF37MP9Kuv62B1VBrZ1b --- .../2026-07-21-centralized-config-design.md | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-21-centralized-config-design.md diff --git a/docs/superpowers/specs/2026-07-21-centralized-config-design.md b/docs/superpowers/specs/2026-07-21-centralized-config-design.md new file mode 100644 index 0000000..f6d0f33 --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-centralized-config-design.md @@ -0,0 +1,154 @@ +# 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) + +1. **Secrets live in config.json** (gitignored). A committed + `config.example.json` carries placeholders. Env vars still override. + The hardcoded credential fallbacks in `config.py` and `telegram_bot.py` + are **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.) +2. **Multi-provider is structure only**: a `data_providers` section with named + provider blocks and an `active` selector. Only Twelve Data is implemented. + No failover logic (nothing to fail over to yet). +3. **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.py` next to their validation evidence. The overlay is + applied **before** those deltas and updates **both** mirrored copies + (`xau_strategy.blocked_hours_utc` and `backtest.blocked_hours_utc`) so they + cannot drift. +4. **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): + +```json +{ + "telegram": { + "token": "", + "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": "" } + }, + "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. + +- `AppConfig` dataclass: `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 → `AppConfig` of pure + defaults + one warning log. Malformed JSON, unknown top-level/section keys, + unknown pair symbols, hours outside 0–23, unknown timeframe names, unknown + `active` provider → raise `AppConfigError` with 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 base `blocked_hours_utc` on **both** + `cfg.xau_strategy.blocked_hours_utc` and `cfg.backtest.blocked_hours_utc`, + plus `cfg.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.json` populated with the **current live values** + (the ones being deleted from source) so the running bot keeps working + unchanged. `.gitignore` gains `config.json`. +- Commit `config.example.json` with 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) + +1. **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). +2. Overlay mapping: JSON blocked hours land on **both** mirrored fields; M5 + factory still appends hour 9 on top of a custom base. +3. Precedence: env var beats config.json beats default, per key. +4. Validation: malformed JSON, bad hour, unknown pair/timeframe/provider each + raise `AppConfigError`. +5. Missing-file behavior: defaults + no crash. +6. 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.