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
Whitespace-only changes.
@@ -0,0 +1,153 @@
|
||||
"""Alpaca paper-trading wrapper — one Broker per persona account.
|
||||
|
||||
Each persona trades its own paper account (see settings.alpaca_creds). Bracket design
|
||||
(resolves the market-entry TIF trap): the entry is a *marketable limit* (limit set a hair
|
||||
above current price so it fills like a market order at the open) with time_in_force=GTC, so
|
||||
the take-profit and stop-loss legs are GTC too and survive multi-day holds instead of being
|
||||
cancelled at session close. Stop/take are derived from the FRESH price at execution time.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from config.settings import get_settings
|
||||
|
||||
_ENTRY_BUFFER = 0.02 # 2% above ref so a limit buy fills promptly at the open
|
||||
|
||||
|
||||
@dataclass
|
||||
class BracketResult:
|
||||
ticker: str
|
||||
qty: int
|
||||
alpaca_order_id: str
|
||||
status: str
|
||||
entry_limit: float
|
||||
stop_price: float
|
||||
take_price: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class OptionResult:
|
||||
contract_symbol: str
|
||||
qty: int
|
||||
alpaca_order_id: str
|
||||
status: str
|
||||
|
||||
|
||||
def bracket_levels(ref_price: float, stop_pct: float, take_pct: float, direction: str = "long"):
|
||||
"""Pure level math for a marketable-limit bracket. Returns (side, entry, stop, take).
|
||||
|
||||
long: buy entry just ABOVE ref; stop below, take above (stop < entry < take)
|
||||
short: sell entry just BELOW ref; stop above, take below (take < entry < stop)
|
||||
Adaptive buffer keeps entry strictly inside the stop/take band.
|
||||
"""
|
||||
buffer = min(_ENTRY_BUFFER, take_pct * 0.4, stop_pct * 0.4)
|
||||
if direction == "short":
|
||||
entry = round(ref_price * (1 - buffer), 2)
|
||||
take = round(ref_price * (1 - take_pct), 2)
|
||||
stop = round(ref_price * (1 + stop_pct), 2)
|
||||
if not (take < entry < stop):
|
||||
raise ValueError(f"invalid short bracket: take={take} entry={entry} stop={stop}")
|
||||
return "sell", entry, stop, take
|
||||
entry = round(ref_price * (1 + buffer), 2)
|
||||
take = round(ref_price * (1 + take_pct), 2)
|
||||
stop = round(ref_price * (1 - stop_pct), 2)
|
||||
if not (stop < entry < take):
|
||||
raise ValueError(f"invalid long bracket: stop={stop} entry={entry} take={take}")
|
||||
return "buy", entry, stop, take
|
||||
|
||||
|
||||
class Broker:
|
||||
def __init__(self, key: str, secret: str, paper: bool = True):
|
||||
from alpaca.trading.client import TradingClient
|
||||
|
||||
self.client = TradingClient(key, secret, paper=paper)
|
||||
|
||||
def get_clock(self):
|
||||
return self.client.get_clock()
|
||||
|
||||
def get_account(self):
|
||||
return self.client.get_account()
|
||||
|
||||
def get_positions(self) -> list:
|
||||
return self.client.get_all_positions()
|
||||
|
||||
def held_symbols(self) -> set[str]:
|
||||
try:
|
||||
return {p.symbol for p in self.get_positions()}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
def is_tradable(self, symbol: str) -> bool:
|
||||
try:
|
||||
return bool(self.client.get_asset(symbol).tradable)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def submit_bracket(
|
||||
self, ticker: str, qty: int, ref_price: float, stop_pct: float, take_pct: float,
|
||||
direction: str = "long",
|
||||
) -> BracketResult:
|
||||
"""Submit a marketable-limit GTC bracket around ref_price (long or short)."""
|
||||
from alpaca.trading.enums import OrderClass, OrderSide, TimeInForce
|
||||
from alpaca.trading.requests import (
|
||||
LimitOrderRequest,
|
||||
StopLossRequest,
|
||||
TakeProfitRequest,
|
||||
)
|
||||
|
||||
side_str, entry, stop, take = bracket_levels(ref_price, stop_pct, take_pct, direction)
|
||||
side = OrderSide.SELL if side_str == "sell" else OrderSide.BUY
|
||||
|
||||
req = LimitOrderRequest(
|
||||
symbol=ticker,
|
||||
qty=qty,
|
||||
side=side,
|
||||
time_in_force=TimeInForce.GTC, # exits persist across sessions
|
||||
order_class=OrderClass.BRACKET,
|
||||
limit_price=entry,
|
||||
take_profit=TakeProfitRequest(limit_price=take),
|
||||
stop_loss=StopLossRequest(stop_price=stop),
|
||||
)
|
||||
order = self.client.submit_order(req)
|
||||
logger.info("submitted {} {} x{} bracket entry={} stop={} take={} id={}",
|
||||
direction, ticker, qty, entry, stop, take, order.id)
|
||||
return BracketResult(
|
||||
ticker=ticker, qty=qty, alpaca_order_id=str(order.id),
|
||||
status=str(order.status), entry_limit=entry, stop_price=stop, take_price=take,
|
||||
)
|
||||
|
||||
def submit_option(self, contract_symbol: str, qty: int, limit_price: float) -> OptionResult:
|
||||
"""Buy-to-open a single-leg long option (call/put) with a marketable LIMIT (Alpaca
|
||||
rejects options market orders). Max loss = premium paid, so no stop leg; Alpaca has no
|
||||
bracket class for options, so this is entry-only (Phase 6a MVP)."""
|
||||
from alpaca.trading.enums import OrderSide, TimeInForce
|
||||
from alpaca.trading.requests import LimitOrderRequest
|
||||
|
||||
req = LimitOrderRequest(
|
||||
symbol=contract_symbol, qty=qty, side=OrderSide.BUY,
|
||||
time_in_force=TimeInForce.DAY, limit_price=round(limit_price, 2),
|
||||
)
|
||||
order = self.client.submit_order(req)
|
||||
logger.info("submitted option BUY {} x{} limit={} id={}",
|
||||
contract_symbol, qty, round(limit_price, 2), order.id)
|
||||
return OptionResult(
|
||||
contract_symbol=contract_symbol, qty=qty,
|
||||
alpaca_order_id=str(order.id), status=str(order.status),
|
||||
)
|
||||
|
||||
|
||||
def for_persona(persona_id: str) -> Broker:
|
||||
s = get_settings()
|
||||
key, secret = s.alpaca_creds(persona_id)
|
||||
return Broker(key, secret, paper=s.alpaca_paper)
|
||||
|
||||
|
||||
def any_broker() -> Broker:
|
||||
"""Broker for account-agnostic calls (e.g. the market clock). Uses any configured account."""
|
||||
s = get_settings()
|
||||
key, secret = s.alpaca_data_creds()
|
||||
return Broker(key, secret, paper=s.alpaca_paper)
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Pure, unit-tested risk guardrails. Turns the agents' picks into a safe order plan.
|
||||
|
||||
No network, no Alpaca objects — takes plain data (equity, prices, held symbols) so the
|
||||
whole pipeline is deterministic and testable. Handles equities and single-leg long options
|
||||
(sized by premium × 100). The LLM's requested sizing / stops are inputs that get clamped
|
||||
here; nothing is trusted as-is.
|
||||
|
||||
Pipeline: collect (long-only unless enabled) -> dedup across agents (highest conviction) ->
|
||||
per-agent trade cap -> price/liquidity filter + skip already-held -> size & clamp weight ->
|
||||
per-day cap -> buying-power trim.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import floor
|
||||
|
||||
from agents.schemas import AgentOutput, Direction, Instrument, Pick
|
||||
from config.risk import RiskConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlannedOrder:
|
||||
ticker: str
|
||||
persona_id: str
|
||||
qty: int
|
||||
est_price: float # share price (equity) or premium per share (option)
|
||||
weight_pct: float
|
||||
conviction: float
|
||||
stop_pct: float | None
|
||||
take_pct: float | None
|
||||
entry_type: str
|
||||
direction: str = "long"
|
||||
instrument: str = "equity"
|
||||
contract_symbol: str | None = None
|
||||
multiplier: int = 1 # 100 for options
|
||||
option_right: str | None = None
|
||||
|
||||
|
||||
def _exec_key(pick: Pick) -> str:
|
||||
"""The symbol we price/dedup/hold on: contract for options, ticker for equities."""
|
||||
return pick.contract_symbol if pick.instrument == Instrument.OPTION else pick.ticker
|
||||
|
||||
|
||||
def _multiplier(pick: Pick) -> int:
|
||||
return 100 if pick.instrument == Instrument.OPTION else 1
|
||||
|
||||
|
||||
def plan_orders(
|
||||
agent_outputs: list[AgentOutput],
|
||||
*,
|
||||
equity: float,
|
||||
buying_power: float,
|
||||
prices: dict[str, float],
|
||||
held_symbols: set[str],
|
||||
risk: RiskConfig,
|
||||
persona_caps: dict[str, float],
|
||||
) -> tuple[list[PlannedOrder], list[str]]:
|
||||
notes: list[str] = []
|
||||
|
||||
# 1. collect candidates (long-only unless shorts allowed; options are long-only here)
|
||||
candidates: list[tuple[str, Pick]] = []
|
||||
for out in agent_outputs:
|
||||
for pick in out.picks:
|
||||
if pick.direction != Direction.LONG and not risk.allow_shorts:
|
||||
notes.append(f"drop {pick.ticker} ({out.persona_id}): shorts disabled")
|
||||
continue
|
||||
if pick.instrument == Instrument.OPTION and pick.direction != Direction.LONG:
|
||||
notes.append(f"drop {pick.ticker} ({out.persona_id}): only long options supported")
|
||||
continue
|
||||
candidates.append((out.persona_id, pick))
|
||||
|
||||
# 2. dedup across agents — highest conviction wins the exec key
|
||||
best: dict[str, tuple[str, Pick]] = {}
|
||||
for pid, pick in candidates:
|
||||
k = _exec_key(pick)
|
||||
cur = best.get(k)
|
||||
if cur is None or pick.conviction > cur[1].conviction:
|
||||
if cur is not None:
|
||||
notes.append(f"dedup {pick.ticker}: {pid} outranks {cur[0]}")
|
||||
best[k] = (pid, pick)
|
||||
|
||||
# 3. per-agent trade cap — keep each persona's top-conviction picks
|
||||
by_persona: dict[str, list[Pick]] = {}
|
||||
for pid, pick in best.values():
|
||||
by_persona.setdefault(pid, []).append(pick)
|
||||
kept: list[tuple[str, Pick]] = []
|
||||
for pid, picks in by_persona.items():
|
||||
picks.sort(key=lambda p: p.conviction, reverse=True)
|
||||
if len(picks) > risk.max_trades_per_agent:
|
||||
notes.append(f"{pid}: capped to {risk.max_trades_per_agent} trades")
|
||||
kept.extend((pid, p) for p in picks[: risk.max_trades_per_agent])
|
||||
|
||||
# 4. price / liquidity filter + skip already-held (min_price only applies to equities)
|
||||
priced: list[tuple[str, Pick, float]] = []
|
||||
for pid, pick in kept:
|
||||
key = _exec_key(pick)
|
||||
px = prices.get(key)
|
||||
if not px or px <= 0:
|
||||
notes.append(f"drop {pick.ticker}: no price")
|
||||
elif pick.instrument == Instrument.EQUITY and px < risk.min_price:
|
||||
notes.append(f"drop {pick.ticker}: price {px:.2f} < min {risk.min_price}")
|
||||
elif pick.instrument == Instrument.EQUITY and risk.max_price and px > risk.max_price:
|
||||
notes.append(f"drop {pick.ticker}: price {px:.2f} > max {risk.max_price}")
|
||||
elif key in held_symbols:
|
||||
notes.append(f"skip {pick.ticker}: already held")
|
||||
else:
|
||||
priced.append((pid, pick, px))
|
||||
|
||||
# 5. clamp weight, size to whole shares/contracts
|
||||
orders: list[PlannedOrder] = []
|
||||
for pid, pick, px in priced:
|
||||
cap = min(
|
||||
persona_caps.get(pid, risk.max_position_pct),
|
||||
risk.max_position_pct,
|
||||
risk.max_symbol_exposure_pct,
|
||||
)
|
||||
weight = min(pick.requested_weight_pct, cap)
|
||||
mult = _multiplier(pick)
|
||||
qty = floor(weight * equity / (px * mult))
|
||||
if qty < 1:
|
||||
notes.append(f"drop {pick.ticker}: qty<1 at weight {weight:.3f}")
|
||||
continue
|
||||
orders.append(
|
||||
PlannedOrder(
|
||||
ticker=pick.ticker, persona_id=pid, qty=qty, est_price=px,
|
||||
weight_pct=weight, conviction=pick.conviction,
|
||||
stop_pct=pick.stop_loss_pct, take_pct=pick.take_profit_pct,
|
||||
entry_type=pick.entry_type.value, direction=pick.direction.value,
|
||||
instrument=pick.instrument.value, contract_symbol=pick.contract_symbol,
|
||||
multiplier=mult, option_right=pick.option_right,
|
||||
)
|
||||
)
|
||||
|
||||
# 6. per-day cap — rank all by conviction, truncate
|
||||
orders.sort(key=lambda o: o.conviction, reverse=True)
|
||||
if len(orders) > risk.max_trades_per_day:
|
||||
notes.append(f"day cap: {len(orders)} -> {risk.max_trades_per_day}")
|
||||
orders = orders[: risk.max_trades_per_day]
|
||||
|
||||
# 7. buying-power trim — drop lowest-conviction until it fits (notional = qty*price*mult)
|
||||
def _notional(o: PlannedOrder) -> float:
|
||||
return o.qty * o.est_price * o.multiplier
|
||||
|
||||
total = sum(_notional(o) for o in orders)
|
||||
while orders and total > buying_power:
|
||||
dropped = orders.pop()
|
||||
total -= _notional(dropped)
|
||||
notes.append(f"drop {dropped.ticker}: buying power (need ${total:,.0f} > ${buying_power:,.0f})")
|
||||
|
||||
return orders, notes
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Snapshot one persona's Alpaca account into the state DB (keyed by persona id), so the
|
||||
dashboard and the next nightly run (via get_portfolio) see that account's holdings + PnL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from execution.broker import Broker
|
||||
from state import db
|
||||
|
||||
|
||||
def _f(x, default=0.0) -> float:
|
||||
try:
|
||||
return float(x)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def reconcile(db_path, account: str, broker: Broker) -> dict:
|
||||
acct = broker.get_account()
|
||||
positions = broker.get_positions()
|
||||
|
||||
pos_list = [
|
||||
{
|
||||
"symbol": p.symbol,
|
||||
"qty": _f(p.qty),
|
||||
"avg_entry": _f(p.avg_entry_price),
|
||||
"market_value": _f(p.market_value),
|
||||
"unrealized_pl": _f(p.unrealized_pl),
|
||||
}
|
||||
for p in positions
|
||||
]
|
||||
|
||||
con = db.connect(db_path)
|
||||
db.record_snapshot(
|
||||
con, account=account, equity=_f(acct.equity), cash=_f(acct.cash),
|
||||
buying_power=_f(acct.buying_power), positions=pos_list,
|
||||
)
|
||||
for p in pos_list:
|
||||
db.upsert_position(con, account=account, **p)
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
logger.info("reconciled {}: equity=${:.2f} positions={}", account, _f(acct.equity), len(pos_list))
|
||||
return {"account": account, "equity": _f(acct.equity), "positions": pos_list}
|
||||
@@ -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