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.
This commit is contained in:
Harwinder Sekhon
2026-09-17 19:49:29 -04:00
parent 183df7832b
commit ad14ea4fed
21 changed files with 3803 additions and 259 deletions
+47 -4
View File
@@ -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")