diff --git a/app_config.py b/app_config.py index 2709487..813b700 100644 --- a/app_config.py +++ b/app_config.py @@ -86,7 +86,7 @@ _VALID_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") _SCANNABLE_TFS = ("M5", "M15", "H1", "H4") _KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level", - "oanda", "deepseek"} + "oanda", "deepseek", "agent_panel"} _KNOWN_OANDA = { "account_id", "api_token", "base_url", "lot_size", "quality_gate", "max_hold_bars", @@ -94,6 +94,10 @@ _KNOWN_OANDA = { _KNOWN_DEEPSEEK = { "api_key", "model", "enabled", "timeout_seconds", "daily_request_cap", } +_KNOWN_AGENT_PANEL = { + "enabled", "telegram", "dashboard", "min_quality", + "max_panels_per_day", "reuse_ttl_seconds", +} _KNOWN_TELEGRAM = {"token", "chat_id", "channel_id", "alert_min_quality"} _KNOWN_SCANNING = {"pairs", "timeframes"} _KNOWN_PAIR_KEYS = {"blocked_hours_utc"} @@ -118,6 +122,7 @@ class AppConfig: pair_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict) oanda_settings: Dict[str, Any] = field(default_factory=dict) deepseek_settings: Dict[str, Any] = field(default_factory=dict) + agent_panel_settings: Dict[str, Any] = field(default_factory=dict) log_level: str = "INFO" @@ -276,6 +281,31 @@ def load_app_config(path: str = CONFIG_PATH) -> AppConfig: "config.json: deepseek.model must be a non-empty string") app.deepseek_settings = dict(deepseek) + panel = raw.get("agent_panel", {}) + _check_keys(panel, _KNOWN_AGENT_PANEL, '"agent_panel"') + for flag in ("enabled", "telegram", "dashboard"): + if flag in panel and not isinstance(panel[flag], bool): + raise AppConfigError( + f"config.json: agent_panel.{flag} must be true or false") + if "min_quality" in panel and panel["min_quality"] not in _VALID_QUALITIES: + raise AppConfigError( + f'config.json: agent_panel.min_quality "{panel["min_quality"]}" ' + f"not in {_VALID_QUALITIES}") + if "max_panels_per_day" in panel: + mpd = panel["max_panels_per_day"] + if isinstance(mpd, bool) or not isinstance(mpd, int) or mpd < 1: + raise AppConfigError( + "config.json: agent_panel.max_panels_per_day must be a " + "positive integer") + if "reuse_ttl_seconds" in panel: + ttl = panel["reuse_ttl_seconds"] + if isinstance(ttl, bool) or not isinstance(ttl, (int, float)) \ + or ttl < 0: + raise AppConfigError( + "config.json: agent_panel.reuse_ttl_seconds must be a " + "non-negative number") + app.agent_panel_settings = dict(panel) + level = raw.get("log_level", app.log_level) if level not in _VALID_LOG_LEVELS: raise AppConfigError( diff --git a/config.json.example b/config.json.example index 0e4eba6..8cffe08 100644 --- a/config.json.example +++ b/config.json.example @@ -40,5 +40,13 @@ "timeout_seconds": 12, "daily_request_cap": 500 }, + "agent_panel": { + "enabled": false, + "telegram": true, + "dashboard": false, + "min_quality": "B", + "max_panels_per_day": 20, + "reuse_ttl_seconds": 3600 + }, "log_level": "INFO" } diff --git a/test_all.py b/test_all.py index acb562a..c1b9a44 100644 --- a/test_all.py +++ b/test_all.py @@ -6943,6 +6943,42 @@ def _(): assert "must be > 0" in str(e) +print("\n═══ Module 23: LLM agent panel ═══") + + +@test("app_config: agent_panel block parses; absent block → {}") +def _(): + app = load_app_config(_write_json({"agent_panel": { + "enabled": True, "telegram": True, "dashboard": False, + "min_quality": "A", "max_panels_per_day": 10, + "reuse_ttl_seconds": 600}})) + assert app.agent_panel_settings["enabled"] is True + assert app.agent_panel_settings["min_quality"] == "A" + assert app.agent_panel_settings["max_panels_per_day"] == 10 + assert load_app_config(_write_json({})).agent_panel_settings == {} + assert AppConfig().agent_panel_settings == {} + + +@test("app_config: agent_panel rejects unknown keys and bad types") +def _(): + for payload in ( + {"agent_panel": {"enabld": True}}, # unknown key + {"agent_panel": {"enabled": "yes"}}, # not a bool + {"agent_panel": {"telegram": 1}}, # not a bool + {"agent_panel": {"dashboard": "no"}}, # not a bool + {"agent_panel": {"min_quality": "Z"}}, # invalid grade + {"agent_panel": {"max_panels_per_day": 0}}, # not positive + {"agent_panel": {"max_panels_per_day": True}}, # bool is not an int + {"agent_panel": {"reuse_ttl_seconds": -5}}, # negative + {"agent_panel": "oops"}, # not an object + ): + try: + load_app_config(_write_json(payload)) + assert False, f"should have raised for {payload}" + except AppConfigError: + pass + + # ══════════════════════════════════════════════════════════════════════ # SUMMARY # ══════════════════════════════════════════════════════════════════════