agent_panel: persona/synthesizer advisory consensus module

ai-hedge-fund-style panel: 4 DeepSeek personas (macro, technical, risk,
contrarian) review a finished SignalReport concurrently on a
panel-private thread pool, then a synthesizer chair issues
AGREE/NEUTRAL/OPPOSE + confidence + rationale. Advisory/display-only by
construction: consumers attach post-hoc via attach_panel; the fields
live on SignalReport with empty defaults and analyze_from_dataframes
never touches them (backtest parity, locked by test).

Spend control on top of the shared 500/day client cap: max_panels_per_day
UTC budget (default 20), exact per-bar result cache, and a reuse TTL
keyed (symbol, tf, direction) so a persisting signal re-serves its
verdict across bars instead of burning 5 requests per bar. <2 valid
takes skips the synthesizer; any failure degrades to no panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SisdfkxnXNL9iZsXxN3T15
This commit is contained in:
Harwinder Sekhon
2026-08-07 22:31:50 -04:00
parent 8d0bc27353
commit 26c8449877
3 changed files with 846 additions and 0 deletions
+307
View File
@@ -6979,6 +6979,313 @@ def _():
pass
import agent_panel as ap
from signal_aggregator import SignalReport, TradeSetup
class _PanelStub:
"""llm_client-singleton drop-in that routes replies by system prompt."""
available = True
def __init__(self, replies):
self.replies = replies # system prompt -> reply dict | None
self.calls = [] # (system_prompt, payload) in call order
def chat_json(self, system_prompt, user_payload):
self.calls.append((system_prompt, user_payload))
reply = self.replies.get(system_prompt)
return reply(user_payload) if callable(reply) else reply
_TAKE_OK = {"stance": "support", "confidence": 70, "note": "fine"}
_SYNTH_OK = {"verdict": "AGREE", "confidence": 75, "rationale": "panel backs it"}
_ALL_OK = {ap._MACRO_SYSTEM: _TAKE_OK, ap._TECH_SYSTEM: _TAKE_OK,
ap._RISK_SYSTEM: _TAKE_OK, ap._CONTRARIAN_SYSTEM: _TAKE_OK,
ap._SYNTH_SYSTEM: _SYNTH_OK}
_PANEL_ON = {"enabled": True, "telegram": True, "dashboard": False,
"min_quality": "B", "max_panels_per_day": 20,
"reuse_ttl_seconds": 3600}
def _panel_report(quality="A", signal=SignalStrength.BUY):
"""Real report via the pipeline, then forced directional/quality."""
config = PlatformConfig()
config.primary_timeframe = Timeframe.H1
config.confirmation_timeframes = [Timeframe.M15, Timeframe.H4]
agg = SignalAggregator(config)
dfs = {Timeframe.H1: make_ohlcv(300, seed=42),
Timeframe.M15: make_ohlcv(300, seed=43),
Timeframe.H4: make_ohlcv(300, seed=44)}
report = agg.analyze_from_dataframes("XAU/USD", dfs, use_news=False)
report.final_signal = signal
report.signal_quality = quality
return report
@test("agent_panel: payload builders compact, filtered, JSON-safe")
def _():
report = _panel_report()
now = _dt_m19.datetime.now(_dt_m19.timezone.utc)
ctx = FundamentalContext(
news_headlines=[NewsSentiment(
headline=f"h{i}", source="rss", timestamp=now,
currencies_mentioned=["USD"], sentiment_score=0.1, keywords=[])
for i in range(15)],
events_today=[EconomicEvent(
time=now, currency="USD", impact="high", event=f"e{i}",
forecast="1.0", actual="2.0") for i in range(12)],
)
macro = ap._macro_payload(report, ctx)
assert len(macro["headlines"]) == 10, "headline cap"
assert len(macro["events"]) == 8, "event cap"
assert macro["events"][0]["released"] is True
assert ap._macro_payload(report, None)["headlines"] == []
tech = ap._technical_payload(report)
assert tech["indicators"], "real analysis must yield indicators"
hidden = [n for n in tech["indicators"]
if n.startswith("_")
or report.primary_analysis.indicators[n].weight == 0]
assert not hidden, f"private/weight-0 leaked: {hidden}"
assert tech["brief"]["direction"] == "BUY"
assert tech["brief"]["timeframe"] == report.primary_analysis.timeframe
assert "setup" not in ap._risk_payload(report), "no setup when None"
report.trade_setup = TradeSetup(
pair="XAU/USD", display_name="XAU/USD", direction="SELL",
entry_price=2000.0, stop_loss=2010.0, take_profit_1=1985.0,
take_profit_2=1970.0, risk_reward=1.5, risk_pips=100.0,
reward_pips=150.0, pip_value=0.1)
risk = ap._risk_payload(report)
assert risk["setup"]["risk_reward"] == 1.5
assert risk["brief"]["direction"] == "SELL", "setup direction wins"
contra = ap._contrarian_payload(report)
assert len(contra["bullish_reasons"]) <= 3
for payload in (macro, tech, risk, contra):
json.dumps(payload, sort_keys=True, allow_nan=False)
@test("agent_panel: persona reply validation clamps and rejects")
def _():
v = ap._validate_take
assert v("macro", {"stance": "moon", "confidence": 50, "note": "x"}) is None
assert v("macro", {"stance": "support", "confidence": "hot"}) is None
assert v("macro", {"stance": "support"}) is None, "missing confidence"
assert v("macro", None) is None
assert v("macro", "support") is None
take = v("risk", {"stance": "oppose", "confidence": 250, "note": "n" * 300})
assert take.confidence == 100.0 and len(take.note) == 160
assert v("risk", {"stance": "neutral", "confidence": -3, "note": ""}).confidence == 0.0
@test("agent_panel: <2 valid takes → None without spending synthesizer")
def _():
try:
stub = _PanelStub({ap._MACRO_SYSTEM: _TAKE_OK}) # others → None
lc._client = stub
assert ap.run_panel(_panel_report()) is None
assert ap._SYNTH_SYSTEM not in [s for s, _ in stub.calls], \
"synthesizer request must not be spent on a failed panel"
assert len(stub.calls) == 4
finally:
lc._client = None
ap.reset_panel_state()
@test("agent_panel: synthesizer failure or garbage verdict → None")
def _():
try:
for synth in (None, {"verdict": "MAYBE", "confidence": 50},
{"verdict": "AGREE", "confidence": "hot"}):
lc._client = _PanelStub({**_ALL_OK, ap._SYNTH_SYSTEM: synth})
assert ap.run_panel(_panel_report()) is None, f"synth={synth}"
finally:
lc._client = None
ap.reset_panel_state()
@test("agent_panel: full run yields verdict + 4 ordered takes")
def _():
try:
stub = _PanelStub(dict(_ALL_OK))
stub.replies[ap._SYNTH_SYSTEM] = {
"verdict": "OPPOSE", "confidence": 72.4,
"rationale": "r" * 300}
lc._client = stub
v = ap.run_panel(_panel_report())
assert v.verdict == "OPPOSE" and v.confidence == 72.4
assert len(v.rationale) == 240, "rationale truncated"
assert [t.persona for t in v.takes] == \
["macro", "technical", "risk", "contrarian"]
synth_payload = dict(stub.calls)[ap._SYNTH_SYSTEM]
assert synth_payload["direction"] == "BUY"
assert len(synth_payload["takes"]) == 4
finally:
lc._client = None
ap.reset_panel_state()
@test("agent_panel: NEUTRAL signal → no panel even forced")
def _():
try:
stub = _PanelStub(dict(_ALL_OK))
lc._client = stub
report = _panel_report(signal=SignalStrength.NEUTRAL)
assert ap.run_panel(report) is None
assert ap.attach_panel(report, _PANEL_ON, force=True,
context=FundamentalContext()) is False
assert stub.calls == [], "NEUTRAL must spend nothing"
finally:
lc._client = None
ap.reset_panel_state()
@test("agent_panel: exact + reuse-TTL cache re-attach with zero spend")
def _():
try:
stub = _PanelStub(dict(_ALL_OK))
lc._client = stub
report = _panel_report()
assert ap.attach_panel(report, _PANEL_ON,
context=FundamentalContext()) is True
assert report.panel_verdict == "AGREE"
assert len(stub.calls) == 5
# same bar again → exact-key hit
report2 = _panel_report()
assert ap.attach_panel(report2, _PANEL_ON,
context=FundamentalContext()) is True
assert report2.panel_verdict == "AGREE" and len(stub.calls) == 5
# newer bar, same symbol/tf/direction → reuse-TTL hit
report3 = _panel_report()
report3.timestamp = "2099-01-01 01:00:00+00:00"
assert ap.attach_panel(report3, _PANEL_ON,
context=FundamentalContext()) is True
assert len(stub.calls) == 5, "persisting signal must not re-spend"
# TTL 0 → the reuse layer is off; a new bar re-runs the panel
report4 = _panel_report()
report4.timestamp = "2099-01-01 02:00:00+00:00"
assert ap.attach_panel(report4, {**_PANEL_ON, "reuse_ttl_seconds": 0},
context=FundamentalContext()) is True
assert len(stub.calls) == 10
finally:
lc._client = None
ap.reset_panel_state()
@test("agent_panel: daily budget exhausts and re-arms on UTC roll")
def _():
try:
stub = _PanelStub(dict(_ALL_OK))
lc._client = stub
settings = {**_PANEL_ON, "max_panels_per_day": 1,
"reuse_ttl_seconds": 0}
assert ap.attach_panel(_panel_report(), settings,
context=FundamentalContext()) is True
assert len(stub.calls) == 5
report = _panel_report()
report.timestamp = "2099-01-01 01:00:00+00:00"
assert ap.attach_panel(report, settings,
context=FundamentalContext()) is False
assert len(stub.calls) == 5, "budget-blocked attempt must spend nothing"
ap._panel_day = (_dt_m19.datetime.now(_dt_m19.timezone.utc).date()
- _dt_m19.timedelta(days=1))
assert ap.attach_panel(report, settings,
context=FundamentalContext()) is True
assert len(stub.calls) == 10
assert ap.panel_health()["used_today"] == 1
finally:
lc._client = None
ap.reset_panel_state()
@test("agent_panel: enabled/surface/min_quality gates; force bypasses")
def _():
try:
stub = _PanelStub(dict(_ALL_OK))
lc._client = stub
report = _panel_report()
ctx = FundamentalContext()
assert ap.attach_panel(report, {}, context=ctx) is False, \
"absent config block must keep the feature dark"
assert ap.attach_panel(report, {**_PANEL_ON, "enabled": False},
context=ctx) is False
assert ap.attach_panel(report, {**_PANEL_ON, "telegram": False},
context=ctx) is False
assert ap.attach_panel(report, _PANEL_ON, surface="dashboard",
context=ctx) is False, "dashboard defaults off"
assert ap.attach_panel(_panel_report(quality="C"), _PANEL_ON,
context=ctx) is False, "C < min_quality B"
assert ap.attach_panel(_panel_report(quality="F"), _PANEL_ON,
context=ctx) is False
assert stub.calls == [], "every gate must block before any spend"
assert ap.attach_panel(report, {**_PANEL_ON, "dashboard": True},
surface="dashboard", context=ctx) is True
ap.reset_panel_state()
assert ap.attach_panel(_panel_report(quality="C"),
{**_PANEL_ON, "enabled": False},
force=True, context=ctx) is True, \
"force bypasses enabled/surface/quality"
finally:
lc._client = None
ap.reset_panel_state()
@test("backtest parity: analyze_from_dataframes never populates panel")
def _():
try:
stub = _PanelStub(dict(_ALL_OK))
lc._client = stub
config = PlatformConfig()
config.primary_timeframe = Timeframe.H1
config.confirmation_timeframes = [Timeframe.M15, Timeframe.H4]
agg = SignalAggregator(config)
dfs = {Timeframe.H1: make_ohlcv(300, seed=42),
Timeframe.M15: make_ohlcv(300, seed=43),
Timeframe.H4: make_ohlcv(300, seed=44)}
for use_news in (True, False):
report = agg.analyze_from_dataframes(
"XAU/USD", dfs, context=FundamentalContext(),
use_news=use_news)
assert report.panel_verdict == ""
assert report.panel_confidence == 0.0
assert report.panel_rationale == ""
assert report.panel_takes == []
assert stub.calls == [], \
"the replayable core must never invoke the panel"
finally:
lc._client = None
ap.reset_panel_state()
@test("agent_panel: attach never raises (LLM explosion, garbage settings)")
def _():
class _Bomb:
available = True
def chat_json(self, system_prompt, user_payload):
raise RuntimeError("boom")
try:
lc._client = _Bomb()
report = _panel_report()
assert ap.attach_panel(report, _PANEL_ON,
context=FundamentalContext()) is False
assert report.panel_verdict == "" and report.panel_takes == []
logging.disable(logging.CRITICAL) # silence the expected traceback
try:
assert ap.attach_panel(report, 42,
context=FundamentalContext()) is False
finally:
logging.disable(logging.NOTSET)
finally:
lc._client = None
ap.reset_panel_state()
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════