agent_panel: ATR-normalized candle + indicator-value enrichment, hardened prompts, verdict logging

This commit is contained in:
Harwinder Sekhon
2026-08-15 15:24:28 -04:00
parent 8b7b63853c
commit 96c926803d
9 changed files with 865 additions and 29 deletions
+64 -4
View File
@@ -10,6 +10,13 @@ Hooked ONLY into SignalAggregator.analyze_pair (the live entry point);
the backtester calls analyze_from_dataframes directly and therefore
never writes here (live == backtest parity — CLAUDE.md rule 4).
Panel fields arrive POST-HOC: the row is inserted during analyze_pair
(before consumers attach the advisory panel), so log_signal stashes the
rowid on the report and agent_panel.attach_panel calls
log_panel_update() to fill panel_verdict / panel_confidence /
panel_rationale / panel_takes_json on that same row. A report without a
rowid (backtest replay, logging disabled) is a silent no-op.
Deployment settings come from the config.json "signal_log" block via
app_config (env vars SIGNAL_LOG_ENABLED / SIGNAL_LOG_PATH win):
@@ -292,10 +299,28 @@ class SignalLogger:
context: Any = None,
source: str = "live",
timeframe: str = "",
) -> None:
) -> int:
"""Insert one scan row; returns the rowid (panel-update key)."""
row = _report_row(report, context, source, timeframe)
with self._lock:
self._conn.execute(_INSERT, row)
cur = self._conn.execute(_INSERT, row)
self._conn.commit()
return int(cur.lastrowid)
def update_panel(
self,
rowid: int,
verdict: str,
confidence: Optional[float],
rationale: str,
takes_json: str,
) -> None:
"""Fill the panel columns on an already-inserted scan row."""
with self._lock:
self._conn.execute(
"UPDATE signals SET panel_verdict=?, panel_confidence=?,"
" panel_rationale=?, panel_takes_json=? WHERE id=?",
(verdict, confidence, rationale, takes_json, rowid))
self._conn.commit()
def close(self) -> None:
@@ -316,7 +341,9 @@ def log_signal(
timeframe: str = "",
) -> None:
"""Persist one scan if the signal_log config block enables it.
NEVER raises — a logging failure must not break a live scan."""
Stashes the rowid on the report (``_signal_log_rowid``) so a post-hoc
panel attach can update this same row. NEVER raises — a logging
failure must not break a live scan."""
try:
if not app_config.signal_log_enabled():
return
@@ -326,12 +353,45 @@ def log_signal(
if lg is None:
lg = SignalLogger(path)
_loggers[path] = lg
lg.log(report, context=context, source=source, timeframe=timeframe)
rowid = lg.log(report, context=context, source=source,
timeframe=timeframe)
try:
report._signal_log_rowid = rowid
except Exception:
pass # exotic report object — panel update just won't happen
except Exception:
logger.warning("signal_log: failed to persist signal",
exc_info=True)
def log_panel_update(report: Any) -> None:
"""Write post-hoc panel fields onto the row logged for this scan.
Matches on the private rowid hand-off from log_signal; a report that
was never logged (backtest replay, logging disabled, logger in
another process) is a silent no-op. NEVER raises."""
try:
rowid = getattr(report, "_signal_log_rowid", None)
if rowid is None or not app_config.signal_log_enabled():
return
verdict = getattr(report, "panel_verdict", "") or ""
if not verdict:
return
path = app_config.signal_log_path()
with _loggers_lock:
lg = _loggers.get(path)
if lg is None:
return
lg.update_panel(
int(rowid), verdict,
_finite(getattr(report, "panel_confidence", None)),
getattr(report, "panel_rationale", "") or "",
_dumps(getattr(report, "panel_takes", None) or []))
except Exception:
logger.warning("signal_log: failed to persist panel verdict",
exc_info=True)
def reset_loggers_for_tests() -> None:
"""Close and drop all cached loggers (test isolation)."""
with _loggers_lock: