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:
Harwinder Sekhon
2026-09-17 19:49:29 -04:00
parent 552199857a
commit 1f4741b557
21 changed files with 3803 additions and 259 deletions
+15 -12
View File
@@ -1634,16 +1634,16 @@ def _replay_span(span: Tuple[int, int]):
if report.signal_quality not in bt_cfg.quality_filter:
continue
# Strategy tag (from reasons[0] which the aggregator pre-pends).
tag = "GENERIC"
first_bull = report.bullish_reasons[0] if report.bullish_reasons else ""
first_bear = report.bearish_reasons[0] if report.bearish_reasons else ""
if "XAU PULLBACK" in first_bull or "XAU PULLBACK" in first_bear:
tag = "XAU_PB"
elif "XAU CHART PATTERN" in first_bull or "XAU CHART PATTERN" in first_bear:
tag = "XAU_CP"
elif "XAU STRATEGY" in first_bull:
tag = "XAU"
# Strategy tag: the generator's own stamp (XAU_ATR / XAU_PB /
# XAU_CP / GENERIC), the same string the live log stores in
# setup_json.strategy. Until 2026-09-17 this was inferred from the
# text of the FIRST reason ("XAU STRATEGY" -> "XAU"), which
# labelled every gold ATR setup whose first reason was another
# indicator — and every SELL — as GENERIC (live review §6.3).
# Label-only: entries, exits and R never depended on it, so the
# books are trade-identical; the CSV `strategy` column changed
# and the reference MD5s were re-pinned with that proof.
tag = str(getattr(report.trade_setup, "strategy", "") or "GENERIC")
if bt_cfg.strategies_only and tag not in bt_cfg.strategies_only:
continue
@@ -2378,7 +2378,9 @@ def _parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
"entries/stops/gates unchanged.")
p.add_argument("--quality", default="A+,A,B,C", help="Comma-separated allowed grades")
p.add_argument("--strategies", default="",
help="Comma-separated strategy tags to include (XAU,XAU_PB,XAU_CP,GENERIC). Empty = all.")
help="Comma-separated strategy tags to include "
"(XAU_ATR,XAU_PB,XAU_CP,GENERIC; the legacy name XAU "
"means XAU_ATR). Empty = all.")
p.add_argument("--cp", action="store_true",
help="Force-enable the chart-pattern entry (XAU_CP) regardless "
"of the factory default — for validating the candidate.")
@@ -2566,7 +2568,8 @@ def main(argv: Optional[List[str]] = None) -> int:
max_hold_bars=args.max_hold_bars,
use_cache=not args.no_cache,
quality_filter=[q.strip() for q in args.quality.split(",") if q.strip()],
strategies_only=[s.strip() for s in args.strategies.split(",") if s.strip()],
strategies_only=[("XAU_ATR" if s.strip() == "XAU" else s.strip())
for s in args.strategies.split(",") if s.strip()],
spread=spread,
workers=args.workers,
fixed_rr_tp=args.fixed_rr_tp or 0.0,