Add dynamic agent registry and options mandate

Introduces a dashboard-managed agent registry (`state/agents.json`) used by nightly research and market-open execution, including support for custom/disabled agents and per-agent time budgets. Expands personas to four agents (adds Maverick), shifts pick targets to include mandatory options, and updates prompts/finalization nudges to enforce option research without hard-failing runs.

Strengthens execution safety by adding option-specific guardrails (conviction floor, premium cap, reserved option slot through trade/day/buying-power caps), validates OCC option symbols in schemas, and normalizes invalid option direction cases. The dashboard was refactored into modular views, adds an Agents admin tab, and improves live portfolio visibility with Alpaca-first data and snapshot fallback.
This commit is contained in:
Aryan Singh committed 2026-07-07 22:00:20 -05:00
1 parent b3821abd69
commit e1ac514f30
36 files changed
+1476 -294

No files matched your search

+58
View File
@@ -133,7 +133,9 @@ def test_option_sizing_by_premium():
o = orders[0]
assert o.instrument == "option" and o.multiplier == 100
assert o.contract_symbol == "AAPL260724C00295000"
# premium cap: weight = min(requested, persona cap, max_option_weight_pct=0.10)
# qty = floor(0.10 * 100000 / (7.39 * 100)) = floor(10000 / 739) = 13
assert abs(o.weight_pct - 0.10) < 1e-9
assert o.qty == 13
@@ -148,3 +150,59 @@ def test_buying_power_trim():
assert len(orders) == 1
assert orders[0].ticker == "HI"
assert any("buying power" in n for n in notes)
def _opt_pick(ticker="AAPL", conviction=0.8, weight=0.05, right="call"):
sym = f"{ticker}260724{'C' if right == 'call' else 'P'}00295000"
return Pick(
ticker=ticker, conviction=conviction, thesis="t", requested_weight_pct=weight,
instrument="option", option_right=right, contract_symbol=sym,
strike=295.0, expiry="2026-07-24",
)
def test_option_conviction_gate():
lo = _opt_pick("AAPL", conviction=0.4)
hi = _opt_pick("MSFT", conviction=0.8)
orders, notes = _plan(
[_out("nova", [lo, hi])],
{lo.contract_symbol: 3.0, hi.contract_symbol: 3.0},
)
assert {o.ticker for o in orders} == {"MSFT"}
assert any("conviction" in n and "floor" in n for n in notes)
def test_option_reserved_in_per_agent_cap():
# option is the LOWEST conviction of 5 picks with cap 4 — it must still survive
eq = [_pick(t, conviction=c) for t, c in [("A", 0.9), ("B", 0.85), ("C", 0.8), ("D", 0.75)]]
opt = _opt_pick("AAPL", conviction=0.7)
prices = {p.ticker: 100.0 for p in eq} | {opt.contract_symbol: 3.0}
orders, notes = _plan([_out("nova", [*eq, opt])], prices)
assert len(orders) == 4
assert any(o.instrument == "option" for o in orders)
assert any("reserved option slot" in n for n in notes)
def test_option_reserved_in_day_cap():
# day cap 2, option ranks 3rd by conviction -> swapped back in
eq = [_pick(t, conviction=c) for t, c in [("A", 0.9), ("B", 0.85)]]
opt = _opt_pick("AAPL", conviction=0.7)
prices = {p.ticker: 100.0 for p in eq} | {opt.contract_symbol: 3.0}
orders, notes = _plan(
[_out("nova", [*eq, opt])], prices,
risk=RiskConfig(max_trades_per_day=2, max_trades_per_agent=5),
)
assert len(orders) == 2
assert any(o.instrument == "option" for o in orders)
assert any("reserved option" in n for n in notes)
def test_option_survives_buying_power_trim():
# option has lowest conviction but equities get dropped first
eq = [_pick("BIG", conviction=0.9, weight=0.10)]
opt = _opt_pick("AAPL", conviction=0.7, weight=0.05)
prices = {"BIG": 200.0, opt.contract_symbol: 5.0}
# equity order = 50 * 200 = 10k; option = qty 10 * 5 * 100 = 5k; BP only fits the option
orders, notes = _plan([_out("nova", [*eq, opt])], prices, buying_power=6_000.0)
assert len(orders) == 1
assert orders[0].instrument == "option"
+86
View File
@@ -0,0 +1,86 @@
"""Agent registry merge/override semantics + creds fallback — no network, temp-dir isolated."""
from __future__ import annotations
import json
import pytest
from config import agent_registry as ar
from config.personas import PERSONAS
@pytest.fixture
def reg(tmp_path, monkeypatch):
"""Point the registry at a temp file so tests never touch state/agents.json."""
path = tmp_path / "agents.json"
monkeypatch.setattr(ar, "registry_path", lambda: path)
return path
def test_defaults_when_no_file(reg):
personas = ar.load_personas()
assert [p.id for p in personas] == [p.id for p in PERSONAS]
def test_builtin_override_merges(reg):
ar.upsert_agent("nova", {"time_budget_min": 90, "target_pick_count": 5})
nova = next(p for p in ar.load_personas() if p.id == "nova")
assert nova.time_budget_min == 90
assert nova.target_pick_count == 5
# untouched fields keep code defaults
assert nova.display_name.startswith("Nova")
def test_only_diff_is_stored_for_builtins(reg):
nova_defaults = next(p for p in PERSONAS if p.id == "nova")
ar.upsert_agent("nova", {"time_budget_min": 90, "max_position_pct": nova_defaults.max_position_pct})
stored = ar.load_overrides()["nova"]
assert stored == {"time_budget_min": 90} # same-as-default values not persisted
def test_disabled_agents_excluded(reg):
ar.upsert_agent("atlas", {"enabled": False})
assert "atlas" not in [p.id for p in ar.load_personas()]
assert "atlas" in [p.id for p in ar.load_personas(include_disabled=True)]
def test_custom_agent_created_and_deleted(reg):
ar.upsert_agent("scalper", {"display_name": "Scalper", "system_prompt": "scalp fast"})
scalper = next(p for p in ar.load_personas() if p.id == "scalper")
assert scalper.options_quota == 1 # custom base defaults applied
ar.delete_agent("scalper")
assert "scalper" not in [p.id for p in ar.load_personas()]
def test_custom_agent_requires_prompt_fields(reg):
with pytest.raises(Exception):
ar.upsert_agent("broken", {"display_name": "no prompt"})
def test_corrupt_file_falls_back_to_builtins(reg):
reg.write_text("{not json")
personas = ar.load_personas()
assert [p.id for p in personas] == [p.id for p in PERSONAS]
def test_invalid_override_falls_back_for_that_agent(reg):
reg.write_text(json.dumps({"agents": {"nova": {"max_position_pct": "not-a-number"}}}))
nova = next(p for p in ar.load_personas() if p.id == "nova")
assert nova.max_position_pct == next(p for p in PERSONAS if p.id == "nova").max_position_pct
def test_reset_builtin(reg):
ar.upsert_agent("nova", {"time_budget_min": 45})
ar.delete_agent("nova")
nova = next(p for p in ar.load_personas() if p.id == "nova")
assert nova.time_budget_min is None
def test_env_creds_fallback_for_custom_ids(monkeypatch):
from config.settings import get_settings
monkeypatch.setenv("ALPACA_SCALPER_KEY", "k123")
monkeypatch.setenv("ALPACA_SCALPER_SECRET", "s456")
key, secret = get_settings().alpaca_creds("scalper")
assert (key, secret) == ("k123", "s456")
+38
View File
@@ -71,3 +71,41 @@ def test_run_manifest_minimal():
)
assert m.status == "running"
assert m.agents == []
def test_option_pick_rejects_fabricated_occ_symbol():
import pytest as _pytest
from agents.schemas import Pick
bad = dict(
ticker="JPM", conviction=0.8, thesis="t", requested_weight_pct=0.05,
instrument="option", option_right="call",
)
# missing underlying root (a real LLM fabrication seen in testing)
with _pytest.raises(Exception):
Pick(**bad, contract_symbol="20270119C0035000")
# 6-digit strike instead of 8 (another real fabrication)
with _pytest.raises(Exception):
Pick(**bad, contract_symbol="PFE260729C002500")
# valid OCC symbol passes and is normalized to uppercase
p = Pick(**bad, contract_symbol="jpm270115c00300000")
assert p.contract_symbol == "JPM270115C00300000"
def test_option_direction_normalization():
import pytest as _pytest
from agents.schemas import Direction, Pick
base = dict(
ticker="JNJ", conviction=0.7, thesis="bearish", requested_weight_pct=0.05,
instrument="option", contract_symbol="JNJ260715P00155000",
)
# short + put -> normalized to a long put (same economic view, buy-to-open)
p = Pick(**base, option_right="put", direction="short")
assert p.direction == Direction.LONG
# short + call has no long-only equivalent -> rejected
with _pytest.raises(Exception):
Pick(**{**base, "contract_symbol": "JNJ260715C00155000"},
option_right="call", direction="short")