news corpus for a local scorer + agent panel v2 + dashboard read-only (2026-09-17); books byte-identical
Agent panel v2: personas return p_win anchored to the book base rates with a code-computed decision context; verdict = equal-weight mean (verdict_source "mean"); only the macro persona runs live (agent_panel.personas ["macro"]); prompt version and exact payloads logged; offline replay harness scripts/panel_replay.py; base-rate table agent_panel_base_rates.json (scripts/build_panel_base_rates.py). Backtester `strategy` column now carries the generator's own stamp (label-only; five reference books re-pinned, all 62 other columns identical). News corpus (after evaluating the owner's ML-NEWS scikit model against DeepSeek: golden set 39/78 vs 78/78, out-of-sample agreement 64% against a 90% self-agreement ceiling; verdict: fallback only, not a replacement): - NewsSentiment.xau_score + archive column xau_score keep DeepSeek's gold score for every headline (930 of the first 1,546 DeepSeek rows had none); an existing newsdata.db gains the column in place on first open - headlines naming no tracked currency are scored and archived (currencies_json []) but never returned to the platform - _apply_llm_sentiment sends every new headline in its cycle, 25 per call; a failed call ends the cycle's sending Dashboard publisher never writes a database: main() calls app_config.set_db_writes_for_process(False), honoured by signal_log_enabled() and news_archive_enabled() (owner decision: the bot is the single writer). Test suite: the hermetic app config now points both writers at a suite-private temp dir — the RSS fixture tests were writing fixture headlines into the real logs/newsdata.db — with a Module 32 test pinning it. 529 tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+47
-4
@@ -105,7 +105,7 @@ _KNOWN_AGENT_PANEL = {
|
||||
"enabled", "telegram", "dashboard", "min_quality",
|
||||
"max_panels_per_day", "reuse_ttl_seconds",
|
||||
"candles_enabled", "candle_bars_primary", "candle_bars_confirm",
|
||||
"verdict_source",
|
||||
"verdict_source", "personas",
|
||||
}
|
||||
_KNOWN_VIOLENT_WATCH = {"enabled", "alert_line", "headsup",
|
||||
"max_headsup_per_day", "headsup_min_level"}
|
||||
@@ -330,10 +330,27 @@ def load_app_config(path: str = CONFIG_PATH) -> AppConfig:
|
||||
f"config.json: agent_panel.{key} must be an integer "
|
||||
"between 1 and 100")
|
||||
if "verdict_source" in panel and panel["verdict_source"] not in \
|
||||
("macro", "chair"):
|
||||
("mean", "macro", "chair"):
|
||||
raise AppConfigError(
|
||||
'config.json: agent_panel.verdict_source must be "macro" or '
|
||||
'"chair"')
|
||||
'config.json: agent_panel.verdict_source must be "mean", '
|
||||
'"macro" or "chair"')
|
||||
if "personas" in panel:
|
||||
names = panel["personas"]
|
||||
valid = ("macro", "technical", "risk", "contrarian")
|
||||
if (not isinstance(names, list) or not names
|
||||
or any(not isinstance(n, str) or n not in valid for n in names)
|
||||
or len(set(names)) != len(names)):
|
||||
raise AppConfigError(
|
||||
"config.json: agent_panel.personas must be a non-empty list "
|
||||
f"of distinct names from {valid}")
|
||||
if panel.get("verdict_source") == "chair" and len(names) < 2:
|
||||
raise AppConfigError(
|
||||
'config.json: agent_panel.verdict_source "chair" needs at '
|
||||
"least two personas")
|
||||
if panel.get("verdict_source") == "macro" and "macro" not in names:
|
||||
raise AppConfigError(
|
||||
'config.json: agent_panel.verdict_source "macro" needs the '
|
||||
"macro persona in agent_panel.personas")
|
||||
app.agent_panel_settings = dict(panel)
|
||||
|
||||
if "market_outlook" in raw:
|
||||
@@ -492,10 +509,34 @@ def violent_watch_enabled(app: Optional[AppConfig] = None) -> bool:
|
||||
return bool(app.violent_watch_settings.get("enabled", False))
|
||||
|
||||
|
||||
# ── Process-level database write switch ──────────────────────────────
|
||||
|
||||
#: True in every process by default. The dashboard publisher turns it
|
||||
#: off at startup (owner decision 2026-09-17): it is not always running,
|
||||
#: so the Telegram bot must be the single writer of signals.db and
|
||||
#: newsdata.db — two writers were also two processes contending for the
|
||||
#: same WAL files. Every SQLite writer (signal_log, news_archive) reads
|
||||
#: the switch through signal_log_enabled() / news_archive_enabled(), so
|
||||
#: a switched-off process cannot write whatever config.json or the
|
||||
#: environment says. Read-only users of the databases are unaffected.
|
||||
_process_db_writes = True
|
||||
|
||||
|
||||
def set_db_writes_for_process(enabled: bool) -> None:
|
||||
global _process_db_writes
|
||||
_process_db_writes = bool(enabled)
|
||||
|
||||
|
||||
def db_writes_enabled() -> bool:
|
||||
return _process_db_writes
|
||||
|
||||
|
||||
# ── Signal-log resolution helpers (env → config.json → default) ──────
|
||||
|
||||
def signal_log_enabled(app: Optional[AppConfig] = None) -> bool:
|
||||
"""Default ON — signal history is the platform's audit trail."""
|
||||
if not _process_db_writes:
|
||||
return False
|
||||
env = os.environ.get("SIGNAL_LOG_ENABLED")
|
||||
if env:
|
||||
return env.strip().lower() in ("1", "true", "yes", "on")
|
||||
@@ -535,6 +576,8 @@ def news_archive_enabled(app: Optional[AppConfig] = None) -> bool:
|
||||
scorer and cannot be backfilled once a fetch window has passed. It
|
||||
lives in its own DB so a signals.db rotation cannot take it with it
|
||||
(which is how three weeks of news were lost on 2026-09-04)."""
|
||||
if not _process_db_writes:
|
||||
return False
|
||||
env = os.environ.get("NEWS_ARCHIVE_ENABLED")
|
||||
if env:
|
||||
return env.strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
Reference in New Issue
Block a user