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:
1 parent
b3821abd69
commit
e1ac514f30
36 files changed
+1476
-294
No files matched your search
+47
-10
@@ -5,9 +5,14 @@ whole pipeline is deterministic and testable. Handles equities and single-leg lo
|
||||
(sized by premium × 100). The LLM's requested sizing / stops are inputs that get clamped
|
||||
here; nothing is trusted as-is.
|
||||
|
||||
Pipeline: collect (long-only unless enabled) -> dedup across agents (highest conviction) ->
|
||||
per-agent trade cap -> price/liquidity filter + skip already-held -> size & clamp weight ->
|
||||
per-day cap -> buying-power trim.
|
||||
Pipeline: collect (long-only unless enabled; options quality gate) -> dedup across agents
|
||||
(highest conviction) -> per-agent trade cap (option slot reserved) -> price/liquidity filter
|
||||
+ skip already-held -> size & clamp weight (options premium-capped) -> per-day cap ->
|
||||
buying-power trim (reserved option dropped last).
|
||||
|
||||
The "reserved option": each night one options trade is mandated per agent, so the
|
||||
highest-conviction option that passes the quality gate is protected from the caps and the
|
||||
buying-power trim — otherwise conviction-ranked truncation would routinely cut it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -59,6 +64,8 @@ def plan_orders(
|
||||
notes: list[str] = []
|
||||
|
||||
# 1. collect candidates (long-only unless shorts allowed; options are long-only here)
|
||||
# + options quality gate: the mandate is a HIGH-PROBABILITY options trade, so option
|
||||
# picks below the conviction floor never reach sizing.
|
||||
candidates: list[tuple[str, Pick]] = []
|
||||
for out in agent_outputs:
|
||||
for pick in out.picks:
|
||||
@@ -68,6 +75,12 @@ def plan_orders(
|
||||
if pick.instrument == Instrument.OPTION and pick.direction != Direction.LONG:
|
||||
notes.append(f"drop {pick.ticker} ({out.persona_id}): only long options supported")
|
||||
continue
|
||||
if pick.instrument == Instrument.OPTION and pick.conviction < risk.min_option_conviction:
|
||||
notes.append(
|
||||
f"drop {pick.ticker} option ({out.persona_id}): conviction "
|
||||
f"{pick.conviction:.2f} < floor {risk.min_option_conviction}"
|
||||
)
|
||||
continue
|
||||
candidates.append((out.persona_id, pick))
|
||||
|
||||
# 2. dedup across agents — highest conviction wins the exec key
|
||||
@@ -80,16 +93,22 @@ def plan_orders(
|
||||
notes.append(f"dedup {pick.ticker}: {pid} outranks {cur[0]}")
|
||||
best[k] = (pid, pick)
|
||||
|
||||
# 3. per-agent trade cap — keep each persona's top-conviction picks
|
||||
# 3. per-agent trade cap — keep each persona's top-conviction picks, but the persona's
|
||||
# best option pick survives the cap (options-trade mandate)
|
||||
by_persona: dict[str, list[Pick]] = {}
|
||||
for pid, pick in best.values():
|
||||
by_persona.setdefault(pid, []).append(pick)
|
||||
kept: list[tuple[str, Pick]] = []
|
||||
for pid, picks in by_persona.items():
|
||||
picks.sort(key=lambda p: p.conviction, reverse=True)
|
||||
keep = picks[: risk.max_trades_per_agent]
|
||||
if len(picks) > risk.max_trades_per_agent:
|
||||
notes.append(f"{pid}: capped to {risk.max_trades_per_agent} trades")
|
||||
kept.extend((pid, p) for p in picks[: risk.max_trades_per_agent])
|
||||
options = [p for p in picks if p.instrument == Instrument.OPTION]
|
||||
if options and not any(p.instrument == Instrument.OPTION for p in keep):
|
||||
notes.append(f"{pid}: reserved option slot for {options[0].ticker}")
|
||||
keep[-1] = options[0]
|
||||
kept.extend((pid, p) for p in keep)
|
||||
|
||||
# 4. price / liquidity filter + skip already-held (min_price only applies to equities)
|
||||
priced: list[tuple[str, Pick, float]] = []
|
||||
@@ -107,7 +126,8 @@ def plan_orders(
|
||||
else:
|
||||
priced.append((pid, pick, px))
|
||||
|
||||
# 5. clamp weight, size to whole shares/contracts
|
||||
# 5. clamp weight, size to whole shares/contracts (options additionally premium-capped:
|
||||
# weight = premium notional, i.e. the max loss — keep any single bet survivable)
|
||||
orders: list[PlannedOrder] = []
|
||||
for pid, pick, px in priced:
|
||||
cap = min(
|
||||
@@ -115,6 +135,8 @@ def plan_orders(
|
||||
risk.max_position_pct,
|
||||
risk.max_symbol_exposure_pct,
|
||||
)
|
||||
if pick.instrument == Instrument.OPTION:
|
||||
cap = min(cap, risk.max_option_weight_pct)
|
||||
weight = min(pick.requested_weight_pct, cap)
|
||||
mult = _multiplier(pick)
|
||||
qty = floor(weight * equity / (px * mult))
|
||||
@@ -132,19 +154,34 @@ def plan_orders(
|
||||
)
|
||||
)
|
||||
|
||||
# 6. per-day cap — rank all by conviction, truncate
|
||||
# the mandated trade: highest-conviction option that made it through sizing is protected
|
||||
# from the day cap and the buying-power trim below
|
||||
reserved = max(
|
||||
(o for o in orders if o.instrument == "option"), key=lambda o: o.conviction, default=None
|
||||
)
|
||||
|
||||
# 6. per-day cap — rank all by conviction, truncate (reserved option swapped back in)
|
||||
orders.sort(key=lambda o: o.conviction, reverse=True)
|
||||
if len(orders) > risk.max_trades_per_day:
|
||||
keep, cut = orders[: risk.max_trades_per_day], orders[risk.max_trades_per_day :]
|
||||
if reserved is not None and reserved in cut:
|
||||
swapped = keep.pop()
|
||||
cut.remove(reserved)
|
||||
keep.append(reserved)
|
||||
notes.append(f"day cap: reserved option {reserved.ticker} replaces {swapped.ticker}")
|
||||
notes.append(f"day cap: {len(orders)} -> {risk.max_trades_per_day}")
|
||||
orders = orders[: risk.max_trades_per_day]
|
||||
orders = keep
|
||||
|
||||
# 7. buying-power trim — drop lowest-conviction until it fits (notional = qty*price*mult)
|
||||
# 7. buying-power trim — drop lowest-conviction until it fits (notional = qty*price*mult);
|
||||
# the reserved option goes last (it's also the cheapest kind of trade to keep)
|
||||
def _notional(o: PlannedOrder) -> float:
|
||||
return o.qty * o.est_price * o.multiplier
|
||||
|
||||
total = sum(_notional(o) for o in orders)
|
||||
while orders and total > buying_power:
|
||||
dropped = orders.pop()
|
||||
droppable = [o for o in orders if o is not reserved] or orders
|
||||
dropped = min(droppable, key=lambda o: o.conviction)
|
||||
orders.remove(dropped)
|
||||
total -= _notional(dropped)
|
||||
notes.append(f"drop {dropped.ticker}: buying power (need ${total:,.0f} > ${buying_power:,.0f})")
|
||||
|
||||
|
||||
Reference in new issue
Block a user