feat(execution): per-account Alpaca GTC brackets, guardrails, shorts, options, reconcile
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
aa1ae0c95e
commit
709870b1d5
8 files changed
+813
No files matched your search
@@ -0,0 +1,28 @@
|
||||
"""Pure tests for bracket level math (long + short). No network."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from execution.broker import bracket_levels
|
||||
|
||||
|
||||
def test_long_levels_ordered():
|
||||
side, entry, stop, take = bracket_levels(100.0, 0.07, 0.15, "long")
|
||||
assert side == "buy"
|
||||
assert stop < entry < take
|
||||
assert (stop, take) == (93.0, 115.0)
|
||||
|
||||
|
||||
def test_short_levels_inverted_and_ordered():
|
||||
side, entry, stop, take = bracket_levels(100.0, 0.07, 0.15, "short")
|
||||
assert side == "sell"
|
||||
# short: profit below (take), loss above (stop)
|
||||
assert take < entry < stop
|
||||
assert (take, stop) == (85.0, 107.0)
|
||||
|
||||
|
||||
def test_tiny_take_does_not_invert_long():
|
||||
# adaptive buffer keeps entry inside the band even when take_pct is small
|
||||
side, entry, stop, take = bracket_levels(100.0, 0.10, 0.03, "long")
|
||||
assert stop < entry < take
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Pure-function tests for the risk guardrails — no network, deterministic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from agents.schemas import AgentOutput, Pick
|
||||
from config.risk import RiskConfig
|
||||
from execution.guardrails import plan_orders
|
||||
|
||||
EQUITY = 100_000.0
|
||||
CAPS = {"nova": 0.10, "warren_lite": 0.08, "atlas": 0.09}
|
||||
|
||||
|
||||
def _out(persona_id: str, picks: list[Pick]) -> AgentOutput:
|
||||
return AgentOutput(
|
||||
agent_name=persona_id,
|
||||
persona_id=persona_id,
|
||||
started_at=datetime.now(timezone.utc),
|
||||
picks=picks,
|
||||
)
|
||||
|
||||
|
||||
def _pick(ticker, conviction=0.7, weight=0.08, direction="long") -> Pick:
|
||||
return Pick(
|
||||
ticker=ticker, conviction=conviction, thesis="t",
|
||||
requested_weight_pct=weight, direction=direction,
|
||||
)
|
||||
|
||||
|
||||
def _plan(agent_outputs, prices, *, risk=None, held=None, buying_power=EQUITY):
|
||||
return plan_orders(
|
||||
agent_outputs,
|
||||
equity=EQUITY,
|
||||
buying_power=buying_power,
|
||||
prices=prices,
|
||||
held_symbols=held or set(),
|
||||
risk=risk or RiskConfig(),
|
||||
persona_caps=CAPS,
|
||||
)
|
||||
|
||||
|
||||
def test_shorts_dropped_when_disabled():
|
||||
orders, notes = _plan(
|
||||
[_out("nova", [_pick("NVDA"), _pick("TSLA", direction="short")])],
|
||||
{"NVDA": 200.0, "TSLA": 250.0},
|
||||
risk=RiskConfig(allow_shorts=False),
|
||||
)
|
||||
tickers = {o.ticker for o in orders}
|
||||
assert "NVDA" in tickers and "TSLA" not in tickers
|
||||
assert any("shorts disabled" in n for n in notes)
|
||||
|
||||
|
||||
def test_shorts_kept_when_enabled():
|
||||
orders, _ = _plan(
|
||||
[_out("nova", [_pick("NVDA"), _pick("TSLA", direction="short")])],
|
||||
{"NVDA": 200.0, "TSLA": 250.0},
|
||||
risk=RiskConfig(allow_shorts=True),
|
||||
)
|
||||
by_ticker = {o.ticker: o for o in orders}
|
||||
assert "TSLA" in by_ticker and by_ticker["TSLA"].direction == "short"
|
||||
assert by_ticker["NVDA"].direction == "long"
|
||||
|
||||
|
||||
def test_dedup_highest_conviction_wins():
|
||||
orders, _ = _plan(
|
||||
[
|
||||
_out("nova", [_pick("MSFT", conviction=0.6)]),
|
||||
_out("atlas", [_pick("MSFT", conviction=0.9)]),
|
||||
],
|
||||
{"MSFT": 400.0},
|
||||
)
|
||||
msft = [o for o in orders if o.ticker == "MSFT"]
|
||||
assert len(msft) == 1
|
||||
assert msft[0].persona_id == "atlas" and msft[0].conviction == 0.9
|
||||
|
||||
|
||||
def test_per_agent_trade_cap():
|
||||
picks = [_pick(t, conviction=c) for t, c in [("A", 0.9), ("B", 0.8), ("C", 0.7), ("D", 0.6)]]
|
||||
orders, notes = _plan(
|
||||
[_out("nova", picks)],
|
||||
{"A": 10.0, "B": 10.0, "C": 10.0, "D": 10.0},
|
||||
risk=RiskConfig(max_trades_per_agent=2),
|
||||
)
|
||||
assert len(orders) == 2
|
||||
assert {o.ticker for o in orders} == {"A", "B"} # top conviction kept
|
||||
|
||||
|
||||
def test_min_price_filter():
|
||||
orders, notes = _plan([_out("nova", [_pick("PENNY")])], {"PENNY": 1.5})
|
||||
assert orders == []
|
||||
assert any("min" in n for n in notes)
|
||||
|
||||
|
||||
def test_skip_already_held():
|
||||
orders, notes = _plan(
|
||||
[_out("nova", [_pick("NVDA"), _pick("AMD")])],
|
||||
{"NVDA": 200.0, "AMD": 150.0},
|
||||
held={"NVDA"},
|
||||
)
|
||||
assert {o.ticker for o in orders} == {"AMD"}
|
||||
assert any("already held" in n for n in notes)
|
||||
|
||||
|
||||
def test_sizing_and_weight_clamp():
|
||||
# requested 50% clamps to nova cap 10% -> qty = floor(0.10 * 100000 / 200) = 50
|
||||
orders, _ = _plan([_out("nova", [_pick("NVDA", weight=0.50)])], {"NVDA": 200.0})
|
||||
assert len(orders) == 1
|
||||
o = orders[0]
|
||||
assert abs(o.weight_pct - 0.10) < 1e-9
|
||||
assert o.qty == 50
|
||||
|
||||
|
||||
def test_day_cap_truncates_by_conviction():
|
||||
picks = [_pick(f"T{i}", conviction=0.5 + i / 100) for i in range(6)]
|
||||
orders, notes = _plan(
|
||||
[_out("nova", picks[:3]), _out("atlas", picks[3:])],
|
||||
{f"T{i}": 100.0 for i in range(6)},
|
||||
risk=RiskConfig(max_trades_per_day=3, max_trades_per_agent=5),
|
||||
)
|
||||
assert len(orders) == 3
|
||||
assert any("day cap" in n for n in notes)
|
||||
|
||||
|
||||
def test_option_sizing_by_premium():
|
||||
opt = Pick(
|
||||
ticker="AAPL", conviction=0.8, thesis="bullish", requested_weight_pct=0.10,
|
||||
instrument="option", option_right="call",
|
||||
contract_symbol="AAPL260724C00295000", strike=295.0, expiry="2026-07-24",
|
||||
)
|
||||
orders, _ = _plan([_out("nova", [opt])], {"AAPL260724C00295000": 7.39})
|
||||
assert len(orders) == 1
|
||||
o = orders[0]
|
||||
assert o.instrument == "option" and o.multiplier == 100
|
||||
assert o.contract_symbol == "AAPL260724C00295000"
|
||||
# qty = floor(0.10 * 100000 / (7.39 * 100)) = floor(10000 / 739) = 13
|
||||
assert o.qty == 13
|
||||
|
||||
|
||||
def test_buying_power_trim():
|
||||
# 2 orders needing 50k each but only 60k buying power -> keep the higher conviction one
|
||||
orders, notes = _plan(
|
||||
[_out("nova", [_pick("HI", conviction=0.9, weight=0.10),
|
||||
_pick("LO", conviction=0.5, weight=0.10)])],
|
||||
{"HI": 200.0, "LO": 200.0},
|
||||
buying_power=12_000.0, # only one 50-share*200=10k order fits
|
||||
)
|
||||
assert len(orders) == 1
|
||||
assert orders[0].ticker == "HI"
|
||||
assert any("buying power" in n for n in notes)
|
||||
Reference in new issue
Block a user