feat(execution): per-account Alpaca GTC brackets, guardrails, shorts, options, reconcile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
This commit is contained in:
1 parent
aa1ae0c95e
commit
709870b1d5
8 files changed
+813
No files matched your search
@@ -0,0 +1,177 @@
|
||||
"""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))
|
||||
Reference in new issue
Block a user