Files
trading-agent/execution/run_market_open.py
Aryan Singh e1ac514f30 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.
2026-07-07 22:00:20 -05:00

226 lines
9.7 KiB
Python

"""ENTRYPOINT #2 — morning execution, one account per persona.
Gated on Alpaca's clock. For each persona: read its picks, apply the risk guardrails against
ITS OWN account (equity/buying-power/holdings — no cross-agent dedup, since the books are
separate), place GTC bracket paper orders in that account, record, and reconcile.
uv run python -m execution.run_market_open [--force] [--date YYYY-MM-DD]
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from loguru import logger # noqa: E402
from agents.schemas import AgentOutput # noqa: E402
from config.agent_registry import load_personas # noqa: E402
from config.risk import DEFAULT_RISK # noqa: E402
from config.settings import get_settings # noqa: E402
from execution import broker as broker_mod # noqa: E402
from execution import reconcile as reconcile_mod # noqa: E402
from execution.guardrails import plan_orders # noqa: E402
from orchestration.budget import next_market_trade_date # noqa: E402
from orchestration.committee import load_verdicts # noqa: E402
from reporting import email_report, report_writer # noqa: E402
from state import db # noqa: E402
from tools.market_data import latest_price # noqa: E402
from tools.options import option_premium # noqa: E402
def _load_outputs(output_dir: Path, trade_date: str) -> list[AgentOutput]:
day_dir = output_dir / trade_date
outputs = []
if not day_dir.exists():
logger.warning("no output dir for {}", trade_date)
return outputs
for f in sorted(day_dir.glob("*.json")):
if f.name.startswith("_"):
continue
try:
outputs.append(AgentOutput.model_validate_json(f.read_text()))
except Exception as e:
logger.warning("skip {}: {}", f.name, e)
return outputs
def main(force: bool = False, date: str | None = None) -> int:
s = get_settings()
s.ensure_dirs()
db.init_db(s.db_path)
trade_date = date or next_market_trade_date(s.market_tz)
logger.add(s.log_dir / f"execution_{trade_date}.log", rotation="10 MB", retention="14 days")
logger.info("=== market-open execution trade_date={} force={} ===", trade_date, force)
# 1. market gate (account-agnostic clock)
try:
clock = broker_mod.any_broker().get_clock()
except Exception as e:
logger.error("cannot reach Alpaca (keys + secrets set in .env?): {}", e)
return 1
if not clock.is_open and not force:
logger.info("market closed (next open {}); nothing to do.", getattr(clock, "next_open", "?"))
return 0
# 2. load the night's picks, index by persona
outputs = _load_outputs(s.output_dir, trade_date)
if not outputs:
logger.warning("no agent outputs to act on.")
return 0
# enforce Investment Committee verdicts: veto = drop, reduce = halve size
verdicts = load_verdicts(trade_date)
if verdicts:
for o in outputs:
kept = []
for p in o.picks:
v = verdicts.get((o.persona_id, p.ticker))
if v and v["verdict"] == "veto":
logger.info("committee VETO {} {} — {}", o.persona_id, p.ticker, v["reason"])
continue
if v and v["verdict"] == "reduce":
p.requested_weight_pct = round(p.requested_weight_pct / 2, 4)
logger.info("committee REDUCE {} {} — {}", o.persona_id, p.ticker, v["reason"])
kept.append(p)
o.picks = kept
by_persona = {o.persona_id: o for o in outputs}
# 3. fresh prices — equity by ticker, options by contract premium (per share)
prices: dict[str, float] = {}
eq_tickers = {p.ticker for o in outputs for p in o.picks if p.instrument.value == "equity"}
opt_syms = {
p.contract_symbol
for o in outputs for p in o.picks
if p.instrument.value == "option" and p.contract_symbol
}
for t in eq_tickers:
px = latest_price(t)
if px:
prices[t] = px
for cs in opt_syms:
prem = option_premium(cs)
if prem:
prices[cs] = prem
con = db.connect(s.db_path)
for o in outputs:
for pick in o.picks:
db.record_thesis(con, trade_date, o.persona_id, pick.model_dump(mode="json"))
con.commit()
# 4. per-persona: own account, own guardrails, own orders (registry: an agent disabled
# on the dashboard after its research ran will NOT have its picks executed)
personas = load_personas()
total_placed = 0
for persona in personas:
out = by_persona.get(persona.id)
if not out or not out.picks:
continue
try:
pbroker = broker_mod.for_persona(persona.id)
acct = pbroker.get_account()
held = pbroker.held_symbols()
except Exception as e:
logger.error("{}: account unreachable (secret set?): {}", persona.id, e)
continue
orders, notes = plan_orders(
[out], equity=float(acct.equity), buying_power=float(acct.buying_power),
prices=prices, held_symbols=held, risk=DEFAULT_RISK,
persona_caps={persona.id: persona.max_position_pct},
)
for n in notes:
logger.info("{} guardrail: {}", persona.id, n)
logger.info("{}: planned {} orders (equity ${:.0f})", persona.id, len(orders), float(acct.equity))
for o in orders:
# options: buy-to-open the contract (defined risk = premium), entry-only
if o.instrument == "option":
try:
# marketable limit: 5% over mid to clear the spread and fill
ores = pbroker.submit_option(o.contract_symbol, o.qty, o.est_price * 1.05)
db.record_order(
con, trade_date=trade_date, agent=persona.id, ticker=o.contract_symbol,
side="buy", qty=o.qty, entry_type=f"option_{o.option_right}",
est_price=o.est_price, stop_price=None, take_price=None,
alpaca_order_id=ores.alpaca_order_id, status=ores.status,
)
total_placed += 1
except Exception as e:
logger.error("{} option order failed for {}: {}", persona.id, o.contract_symbol, e)
db.record_order(
con, trade_date=trade_date, agent=persona.id, ticker=o.contract_symbol,
side="buy", qty=o.qty, entry_type=f"option_{o.option_right}",
est_price=o.est_price, stop_price=None, take_price=None,
alpaca_order_id=None, status=f"error: {e}",
)
continue
stop_pct = o.stop_pct if o.stop_pct is not None else DEFAULT_RISK.default_stop_pct
take_pct = o.take_pct if o.take_pct is not None else DEFAULT_RISK.default_take_pct
side = "sell" if o.direction == "short" else "buy"
try:
res = pbroker.submit_bracket(
o.ticker, o.qty, o.est_price, stop_pct, take_pct, direction=o.direction
)
db.record_order(
con, trade_date=trade_date, agent=persona.id, ticker=o.ticker, side=side,
qty=o.qty, entry_type=f"bracket_{o.direction}", est_price=o.est_price,
stop_price=res.stop_price, take_price=res.take_price,
alpaca_order_id=res.alpaca_order_id, status=res.status,
)
total_placed += 1
except Exception as e:
logger.error("{} order failed for {}: {}", persona.id, o.ticker, e)
db.record_order(
con, trade_date=trade_date, agent=persona.id, ticker=o.ticker, side=side,
qty=o.qty, entry_type=f"bracket_{o.direction}", est_price=o.est_price,
stop_price=None, take_price=None, alpaca_order_id=None, status=f"error: {e}",
)
con.commit()
try:
reconcile_mod.reconcile(s.db_path, persona.id, pbroker)
except Exception as e:
logger.warning("{} reconcile failed: {}", persona.id, e)
con.close()
logger.info("=== execution complete: {} orders placed across accounts ===", total_placed)
# daily report: append execution results, render PDF, email it
try:
con2 = db.connect(s.db_path)
rows = con2.execute(
"SELECT agent, ticker, side, qty, status FROM orders WHERE trade_date=? "
"ORDER BY agent, ticker",
(trade_date,),
).fetchall()
lines = [f"**{total_placed} orders placed** across accounts for {trade_date}.", ""]
for persona in personas:
ords = [r for r in rows if r["agent"] == persona.id]
snap = db.latest_snapshot(con2, persona.id)
eq = f"${snap['equity']:,.2f}" if snap else "n/a"
detail = ", ".join(
f"{r['side']} {r['qty']} {r['ticker']} [{r['status']}]" for r in ords
) or "no orders"
lines.append(f"- **{persona.id}** (equity {eq}): {detail}")
con2.close()
exec_summary = "\n".join(lines)
daily = report_writer.write_daily_report(trade_date, outputs, exec_summary=exec_summary)
pdf = report_writer.to_pdf(daily)
email_report.send_daily(
trade_date, pdf, summary=f"{total_placed} orders placed for {trade_date}."
)
except Exception as e:
logger.warning("daily report/email step failed: {}", e)
return 0
if __name__ == "__main__":
argv = sys.argv[1:]
date_arg = argv[argv.index("--date") + 1] if "--date" in argv else None
raise SystemExit(main(force="--force" in argv, date=date_arg))