config: agent_panel block (enabled/surfaces/min_quality/daily budget)

New optional config.json block for the upcoming LLM agent-panel feature:
enabled, telegram, dashboard (per-surface toggles), min_quality,
max_panels_per_day, reuse_ttl_seconds. Absent block → {} → feature dark.
Validation mirrors the deepseek block. Module 23 opened with parse +
reject tests.

Claude-Session: https://claude.ai/code/session_01SisdfkxnXNL9iZsXxN3T15
This commit is contained in:
Harwinder Sekhon
2026-08-07 22:26:43 -04:00
parent 250078c987
commit 0b604f1ad0
3 changed files with 75 additions and 1 deletions
+31 -1
View File
@@ -86,7 +86,7 @@ _VALID_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
_SCANNABLE_TFS = ("M5", "M15", "H1", "H4") _SCANNABLE_TFS = ("M5", "M15", "H1", "H4")
_KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level", _KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level",
"oanda", "deepseek"} "oanda", "deepseek", "agent_panel"}
_KNOWN_OANDA = { _KNOWN_OANDA = {
"account_id", "api_token", "base_url", "lot_size", "account_id", "api_token", "base_url", "lot_size",
"quality_gate", "max_hold_bars", "quality_gate", "max_hold_bars",
@@ -94,6 +94,10 @@ _KNOWN_OANDA = {
_KNOWN_DEEPSEEK = { _KNOWN_DEEPSEEK = {
"api_key", "model", "enabled", "timeout_seconds", "daily_request_cap", "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_TELEGRAM = {"token", "chat_id", "channel_id", "alert_min_quality"}
_KNOWN_SCANNING = {"pairs", "timeframes"} _KNOWN_SCANNING = {"pairs", "timeframes"}
_KNOWN_PAIR_KEYS = {"blocked_hours_utc"} _KNOWN_PAIR_KEYS = {"blocked_hours_utc"}
@@ -118,6 +122,7 @@ class AppConfig:
pair_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict) pair_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict)
oanda_settings: 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) deepseek_settings: Dict[str, Any] = field(default_factory=dict)
agent_panel_settings: Dict[str, Any] = field(default_factory=dict)
log_level: str = "INFO" 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") "config.json: deepseek.model must be a non-empty string")
app.deepseek_settings = dict(deepseek) 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) level = raw.get("log_level", app.log_level)
if level not in _VALID_LOG_LEVELS: if level not in _VALID_LOG_LEVELS:
raise AppConfigError( raise AppConfigError(
+8
View File
@@ -40,5 +40,13 @@
"timeout_seconds": 12, "timeout_seconds": 12,
"daily_request_cap": 500 "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" "log_level": "INFO"
} }
+36
View File
@@ -6943,6 +6943,42 @@ def _():
assert "must be > 0" in str(e) 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 # SUMMARY
# ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════