agent_panel: ATR-normalized candle + indicator-value enrichment, hardened prompts, verdict logging
This commit is contained in:
+64
-4
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user