Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
178 lines
7.5 KiB
Python
178 lines
7.5 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 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)
|
|
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))
|