Introduces a dashboard-managed agent registry (`state/agents.json`) used by nightly research and market-open execution, including support for custom/disabled agents and per-agent time budgets. Expands personas to four agents (adds Maverick), shifts pick targets to include mandatory options, and updates prompts/finalization nudges to enforce option research without hard-failing runs. Strengthens execution safety by adding option-specific guardrails (conviction floor, premium cap, reserved option slot through trade/day/buying-power caps), validates OCC option symbols in schemas, and normalizes invalid option direction cases. The dashboard was refactored into modular views, adds an Agents admin tab, and improves live portfolio visibility with Alpaca-first data and snapshot fallback.
95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
"""get_portfolio — persona-scoped. Each agent sees ONLY its own paper account.
|
|
|
|
LIVE-first: pulls the account + positions straight from Alpaca at call time, so overnight
|
|
research reflects intraday bracket exits (a stop/take that filled at 2pm is gone from the
|
|
book by night). Falls back to the last reconciled DB snapshot (execution/reconcile.py)
|
|
if Alpaca is unreachable — clearly labeled as cached so the agent knows the freshness.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from langchain_core.tools import tool
|
|
|
|
from config.settings import get_settings
|
|
from state import db as statedb
|
|
|
|
|
|
def _live_portfolio(account: str) -> str | None:
|
|
"""Live account + positions from Alpaca. None if unreachable/unconfigured."""
|
|
try:
|
|
from execution.broker import for_persona
|
|
|
|
b = for_persona(account)
|
|
acct = b.get_account()
|
|
positions = b.get_positions()
|
|
except Exception:
|
|
return None
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
lines = [
|
|
f"LIVE from Alpaca as of {now}: equity=${float(acct.equity):,.2f}, "
|
|
f"cash=${float(acct.cash):,.2f}, buying_power=${float(acct.buying_power):,.2f}"
|
|
]
|
|
if not positions:
|
|
lines.append("Holdings: none (all cash).")
|
|
else:
|
|
lines.append("Holdings:")
|
|
for p in positions:
|
|
lines.append(
|
|
f" {p.symbol}: {p.qty} @ {p.avg_entry_price} "
|
|
f"-> mv={p.market_value}, unrealized_pl={p.unrealized_pl}"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _snapshot_portfolio(account: str) -> str:
|
|
"""Last reconciled DB snapshot — the offline fallback."""
|
|
path = get_settings().db_path
|
|
if not path.exists():
|
|
return "No portfolio data yet (fresh account, no trades recorded)."
|
|
try:
|
|
con = statedb.connect(path)
|
|
snap = statedb.latest_snapshot(con, account or None)
|
|
con.close()
|
|
except Exception as e:
|
|
return f"No portfolio data yet ({e})."
|
|
if snap is None:
|
|
return "No holdings yet (fresh account, all cash)."
|
|
|
|
lines = [
|
|
f"CACHED snapshot from {snap['ts']} (live fetch unavailable — intraday fills after "
|
|
f"that time are NOT reflected): equity=${snap['equity']:,.2f}, "
|
|
f"cash=${snap['cash']:,.2f}, buying_power=${snap['buying_power']:,.2f}"
|
|
]
|
|
try:
|
|
positions = json.loads(snap["positions_json"] or "[]")
|
|
except Exception:
|
|
positions = []
|
|
if not positions:
|
|
lines.append("Holdings: none (all cash).")
|
|
else:
|
|
lines.append("Holdings:")
|
|
for p in positions:
|
|
lines.append(
|
|
f" {p.get('symbol')}: {p.get('qty')} @ {p.get('avg_entry')} "
|
|
f"-> mv={p.get('market_value')}, unrealized_pl={p.get('unrealized_pl')}"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def make_get_portfolio(account: str):
|
|
"""Return a get_portfolio tool bound to one account (persona id)."""
|
|
|
|
@tool
|
|
def get_portfolio() -> str:
|
|
"""Return YOUR current paper-account holdings, unrealized PnL, and cash
|
|
(live from Alpaca; falls back to the last recorded snapshot if offline).
|
|
|
|
Use this BEFORE proposing trades so you account for what you already own.
|
|
"""
|
|
return _live_portfolio(account) or _snapshot_portfolio(account)
|
|
|
|
return get_portfolio
|