Files
alpaclaudia/bot/alpaclaudia/client.py
T
admin 39875112a0 initial: alpaclaudia paper-trading bot + dashboard
Python bot (bot/alpaclaudia): alpaca-py client, wheel strategy (CSP + covered
calls) plus equity trailing stops, risk gates (cash buffer, cost-basis guard,
per-symbol concentration cap), SQLite state log, Typer CLI (tick/loop/status/
report/dump-state), Discord daily report, pytest suite.

Next.js 14 dashboard (dashboard/): read-only — reads the bot's SQLite directly
and pulls live account/positions/orders from Alpaca. KPIs, equity chart,
positions, bot-intents audit table, and orders table. Dark UI with Tailwind.

systemd/: user-unit templates for the polling loop and the post-close report
timer.

docs/STRATEGY.md: wheel mechanics, risk invariants, later candidates.

Defaults to BOT_MODE=dry — nothing is submitted to Alpaca until explicitly
enabled in .env. ALPACA_ENV=paper by default; flipping to live requires an
explicit second guard.
2026-04-16 21:38:25 +02:00

140 lines
5.3 KiB
Python

"""Thin Alpaca client wrappers + helpers for spot, chains, and account state."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import date, timedelta
from typing import Any
from alpaca.data.historical.option import OptionHistoricalDataClient
from alpaca.data.historical.stock import StockHistoricalDataClient
from alpaca.data.requests import OptionChainRequest, StockLatestQuoteRequest
from alpaca.trading.client import TradingClient
from alpaca.trading.enums import AssetClass
from alpaca.trading.requests import GetOptionContractsRequest
from .config import AlpacaConfig
@dataclass
class Clients:
trading: TradingClient
stock_data: StockHistoricalDataClient
option_data: OptionHistoricalDataClient
def build_clients(cfg: AlpacaConfig) -> Clients:
if not cfg.key or not cfg.secret:
raise RuntimeError(
"Alpaca credentials missing. Set ALPACA_API_KEY / ALPACA_API_SECRET."
)
trading = TradingClient(cfg.key, cfg.secret, paper=cfg.is_paper)
stock_data = StockHistoricalDataClient(cfg.key, cfg.secret)
option_data = OptionHistoricalDataClient(cfg.key, cfg.secret)
return Clients(trading=trading, stock_data=stock_data, option_data=option_data)
def get_latest_spot(clients: Clients, symbol: str) -> float | None:
req = StockLatestQuoteRequest(symbol_or_symbols=symbol)
resp = clients.stock_data.get_stock_latest_quote(req)
q = resp.get(symbol) if isinstance(resp, dict) else getattr(resp, symbol, None)
if q is None:
return None
# mid-price if both sides present, else fallback to either
bid = getattr(q, "bid_price", None) or 0
ask = getattr(q, "ask_price", None) or 0
if bid and ask:
return (bid + ask) / 2
return bid or ask or None
def list_option_contracts(
clients: Clients,
underlying: str,
*,
option_type: str, # "put" or "call"
dte_min: int,
dte_max: int,
) -> list[Any]:
today = date.today()
req = GetOptionContractsRequest(
underlying_symbols=[underlying],
type=option_type,
expiration_date_gte=today + timedelta(days=dte_min),
expiration_date_lte=today + timedelta(days=dte_max),
status="active",
limit=500,
)
resp = clients.trading.get_option_contracts(req)
return list(getattr(resp, "option_contracts", resp) or [])
def get_option_snapshots(clients: Clients, underlying: str) -> dict[str, Any]:
"""Returns {contract_symbol: snapshot} with greeks/quote when available."""
try:
req = OptionChainRequest(underlying_symbol=underlying)
chain = clients.option_data.get_option_chain(req)
return dict(chain) if chain else {}
except Exception:
return {}
def get_account_snapshot(clients: Clients) -> dict[str, Any]:
acct = clients.trading.get_account()
return {
"equity": float(getattr(acct, "equity", 0) or 0),
"cash": float(getattr(acct, "cash", 0) or 0),
"buying_power": float(getattr(acct, "buying_power", 0) or 0),
"portfolio_value": float(getattr(acct, "portfolio_value", 0) or 0),
"status": str(getattr(acct, "status", "")),
"pattern_day_trader": bool(getattr(acct, "pattern_day_trader", False)),
}
def list_positions(clients: Clients) -> list[dict[str, Any]]:
pos = clients.trading.get_all_positions() or []
out: list[dict[str, Any]] = []
for p in pos:
out.append(
{
"symbol": getattr(p, "symbol", ""),
"asset_class": str(getattr(p, "asset_class", "")),
"qty": float(getattr(p, "qty", 0) or 0),
"avg_entry_price": float(getattr(p, "avg_entry_price", 0) or 0),
"market_value": float(getattr(p, "market_value", 0) or 0),
"unrealized_pl": float(getattr(p, "unrealized_pl", 0) or 0),
"unrealized_plpc": float(getattr(p, "unrealized_plpc", 0) or 0),
"current_price": float(getattr(p, "current_price", 0) or 0),
"side": str(getattr(p, "side", "")),
"is_option": str(getattr(p, "asset_class", "")).lower().endswith("option"),
}
)
return out
def list_recent_orders(clients: Clients, *, limit: int = 100) -> list[dict[str, Any]]:
from alpaca.trading.requests import GetOrdersRequest
req = GetOrdersRequest(status="all", limit=limit, nested=True)
orders = clients.trading.get_orders(filter=req) or []
out: list[dict[str, Any]] = []
for o in orders:
out.append(
{
"id": str(getattr(o, "id", "")),
"symbol": getattr(o, "symbol", ""),
"side": str(getattr(o, "side", "")),
"qty": float(getattr(o, "qty", 0) or 0),
"filled_qty": float(getattr(o, "filled_qty", 0) or 0),
"order_type": str(getattr(o, "order_type", "")),
"status": str(getattr(o, "status", "")),
"submitted_at": str(getattr(o, "submitted_at", "")),
"filled_avg_price": (
float(getattr(o, "filled_avg_price", 0) or 0)
if getattr(o, "filled_avg_price", None) is not None
else None
),
"asset_class": str(getattr(o, "asset_class", AssetClass.US_EQUITY)),
}
)
return out