"""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.personas import 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 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 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 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))