feat(agents): pydantic output schemas (Pick/AgentOutput/RunManifest) + tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
This commit is contained in:
1 parent
031a051687
commit
f41cf75d8a
3 files changed
+207
No files matched your search
@@ -0,0 +1,73 @@
|
||||
"""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 == []
|
||||
Reference in new issue
Block a user