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.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user