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.
112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
"""Phase-0 gate: the output contract validates and rejects garbage."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from agents.schemas import AgentOutput, AgentStatus, Direction, Pick, RunManifest
|
|
|
|
|
|
def _pick(**kw) -> Pick:
|
|
base = dict(ticker="nvda", conviction=0.8, thesis="momentum", requested_weight_pct=0.05)
|
|
base.update(kw)
|
|
return Pick(**base)
|
|
|
|
|
|
def test_pick_valid_and_ticker_uppercased():
|
|
p = _pick()
|
|
assert p.ticker == "NVDA"
|
|
assert p.direction == Direction.LONG
|
|
|
|
|
|
def test_conviction_out_of_range_rejected():
|
|
with pytest.raises(ValidationError):
|
|
_pick(conviction=1.5)
|
|
|
|
|
|
def test_weight_must_be_positive_fraction():
|
|
with pytest.raises(ValidationError):
|
|
_pick(requested_weight_pct=0.0)
|
|
with pytest.raises(ValidationError):
|
|
_pick(requested_weight_pct=1.5)
|
|
|
|
|
|
def test_stop_take_pct_must_be_fraction():
|
|
with pytest.raises(ValidationError):
|
|
_pick(stop_loss_pct=7.0) # 700% — clearly a mistake
|
|
assert _pick(stop_loss_pct=0.07).stop_loss_pct == 0.07
|
|
|
|
|
|
def test_long_absolute_levels_ordering_enforced():
|
|
with pytest.raises(ValidationError):
|
|
_pick(entry_type="limit", limit_price=100.0, stop_price=105.0) # stop above entry
|
|
with pytest.raises(ValidationError):
|
|
_pick(entry_type="limit", limit_price=100.0, take_price=95.0) # take below entry
|
|
ok = _pick(entry_type="limit", limit_price=100.0, stop_price=93.0, take_price=120.0)
|
|
assert ok.limit_price == 100.0
|
|
|
|
|
|
def test_agent_output_roundtrip():
|
|
out = AgentOutput(
|
|
agent_name="Nova",
|
|
persona_id="nova",
|
|
started_at=datetime.now(timezone.utc),
|
|
status=AgentStatus.COMPLETE,
|
|
picks=[_pick(), _pick(ticker="amd", conviction=0.6)],
|
|
)
|
|
restored = AgentOutput.model_validate_json(out.model_dump_json())
|
|
assert restored.agent_name == "Nova"
|
|
assert [p.ticker for p in restored.picks] == ["NVDA", "AMD"]
|
|
|
|
|
|
def test_run_manifest_minimal():
|
|
m = RunManifest(
|
|
run_id="r1",
|
|
trade_date="2026-07-01",
|
|
model="gpt-oss-20b",
|
|
started_at=datetime.now(timezone.utc),
|
|
)
|
|
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")
|