Files
trading-agent/execution/run_market_open.py
T

207 lines
8.8 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.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))