signal_log: outcomes for every setup, event dedup, interned news; fix over-scan, dead outlook, panel OPPOSE bias
- seed an advisory outcome on every setup row; same-setup re-logs within 60 min are DUPLICATE rows (advisory_event_id); resolver rebuilds bar streams across gaps - headlines/events interned into news_snapshots (DB 667 MB -> 262 MB); readers use signals_with_news / read_news and mode=ro (immutable=1 missed WAL rows) - post-gate truth columns: gate_reason, actionable, cleaned_warnings, scan_kind, config_fingerprint; SELL-hour gates fail closed - telegram: breach rescan reuses this bar's report; grade dropped from alert fingerprint (upgrade-only re-alert) - market_outlook: hour-bucket dedup (old rule skipped alternate hours), skip heartbeats - agent_panel: macro-anchored OPPOSE guard (opposed trades were +30R over 3 weeks) - analyze_signal_log --stored-candles/--parity; backfill + news migration scripts - tests: Module 25 (analyze_signal_log) + 27 (observability); 429 total Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC4s6VCURFte4dub4xKxYL
This commit is contained in:
+53
-3
@@ -253,9 +253,13 @@ Weigh the takes into ONE consensus on whether to act on the signal:
|
||||
"AGREE" (the panel backs the trade), "OPPOSE" (the panel is against it),
|
||||
or "NEUTRAL" (genuinely split or low conviction). The risk manager and
|
||||
contrarian have intentionally defensive roles; their objections are not
|
||||
automatic vetoes. Do not count repeated versions of the same warning as
|
||||
independent evidence. Weigh role relevance, evidence quality, and
|
||||
confidence rather than averaging blindly.
|
||||
automatic vetoes: "OPPOSE" requires that the macro analyst opposes, or
|
||||
that the technical analyst opposes while the macro analyst is not
|
||||
supportive. When only the defensive roles object, or the macro analyst
|
||||
supports the trade, the most the panel can say is "NEUTRAL". Do not
|
||||
count repeated versions
|
||||
of the same warning as independent evidence. Weigh role relevance,
|
||||
evidence quality, and confidence rather than averaging blindly.
|
||||
Base the verdict and rationale ONLY on the takes given — do not add
|
||||
outside analysis.
|
||||
Confidence scale (0-100): 80-100 the panel clearly leans one way; 50-79
|
||||
@@ -674,6 +678,15 @@ def _panel_jobs(
|
||||
)
|
||||
|
||||
|
||||
# NOTE (2026-09 audit): the TTL reuse key differs from the exact key
|
||||
# only by the contrarian's bar timestamp, and every other block (price,
|
||||
# ATR-normalized candles, indicator readings, drifting entry/stop) moves
|
||||
# each closed bar — so TTL_REUSE never fired live (30/30 attaches MISS).
|
||||
# That is the documented, test-locked decision ("changed evidence never
|
||||
# inherits a direction-only cached verdict"); spend is bounded by
|
||||
# max_panels_per_day instead. Relaxing the identity is an owner call.
|
||||
|
||||
|
||||
def _panel_payload_hash(
|
||||
jobs: Tuple[Tuple[str, str, Dict[str, Any]], ...],
|
||||
ignore_bar_timestamp: bool = False,
|
||||
@@ -747,11 +760,48 @@ def run_panel(report: "SignalReport",
|
||||
if verdict not in _VALID_VERDICTS or confidence is None:
|
||||
return None
|
||||
rationale = str(reply.get("rationale", "")).strip()[:240]
|
||||
verdict, confidence, rationale = _apply_oppose_guard(
|
||||
verdict, confidence, rationale, takes)
|
||||
return PanelVerdict(verdict=verdict, confidence=confidence,
|
||||
rationale=rationale, takes=takes,
|
||||
created_ts=time.time(), payload_hash=payload_hash)
|
||||
|
||||
|
||||
# Deterministic backstop for the chair (ADVISORY-ONLY, no gate reads it).
|
||||
#
|
||||
# Evidence (2026-09 signal-log audit, 133 live verdicts, 107 with a
|
||||
# replayed outcome): the contrarian and risk personas opposed 117/133
|
||||
# alerts each and the technical persona 88/133 — they object to the
|
||||
# trend extension the engine already discounted — so the chair said
|
||||
# OPPOSE 73% of the time while the OPPOSED trades made +30.1R (PF 2.6).
|
||||
# The macro persona was the only informative voice: macro=support
|
||||
# trades won 88% (+0.56R mean, n=69); macro=oppose won 25%; macro=neutral
|
||||
# 38%. With this rule the OPPOSE bucket becomes the weakest one
|
||||
# (n=29, +0.08R mean, 41% win) instead of the best. Small sample, one
|
||||
# bullish episode — re-score with analyze_signal_log --stored-candles
|
||||
# after more history before tightening further.
|
||||
#
|
||||
# Rule: OPPOSE stands only if the macro analyst opposes, or the technical
|
||||
# analyst opposes while the macro analyst is not supportive. Otherwise
|
||||
# the verdict is NEUTRAL (confidence capped below 50).
|
||||
|
||||
|
||||
def _apply_oppose_guard(verdict: str, confidence: float, rationale: str,
|
||||
takes: List[PersonaTake]):
|
||||
if verdict != "OPPOSE":
|
||||
return verdict, confidence, rationale
|
||||
stance = {t.persona: t.stance for t in takes}
|
||||
macro = stance.get("macro")
|
||||
technical = stance.get("technical")
|
||||
if macro == "oppose":
|
||||
return verdict, confidence, rationale
|
||||
if technical == "oppose" and macro != "support":
|
||||
return verdict, confidence, rationale
|
||||
note = ("The big-picture view does not object, so the panel stays "
|
||||
"neutral rather than against. ")
|
||||
return "NEUTRAL", min(float(confidence), 49.0), (note + rationale)[:240]
|
||||
|
||||
|
||||
# ── Cache, budget, attach ────────────────────────────────────────────
|
||||
|
||||
_state_lock = threading.Lock()
|
||||
|
||||
Reference in New Issue
Block a user