39875112a0
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.
122 lines
3.4 KiB
Python
122 lines
3.4 KiB
Python
"""CLI entry point. Usage:
|
|
|
|
alpaclaudia tick # one-shot iteration
|
|
alpaclaudia loop # poll forever (intended for systemd)
|
|
alpaclaudia report # post daily summary to Discord
|
|
alpaclaudia status # print account snapshot + last tick
|
|
|
|
Honours BOT_MODE=dry (default) — nothing is submitted until BOT_MODE=live.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, time as dtime, timezone
|
|
|
|
import typer
|
|
from rich import print as rprint
|
|
from rich.table import Table
|
|
|
|
from .client import (
|
|
build_clients,
|
|
get_account_snapshot,
|
|
list_positions,
|
|
list_recent_orders,
|
|
)
|
|
from .config import load_config
|
|
from .logger import get_logger, setup_logging
|
|
from .reporter import build_daily_summary, post_to_discord
|
|
from .scheduler import loop as run_loop, tick as run_tick
|
|
from .state import Store
|
|
|
|
app = typer.Typer(help="alpaclaudia — Alpaca paper-trading bot.")
|
|
log = get_logger(__name__)
|
|
|
|
|
|
def _bootstrap():
|
|
cfg = load_config()
|
|
setup_logging(cfg.log_dir)
|
|
store = Store(cfg.data_dir / "alpaclaudia.db")
|
|
clients = build_clients(cfg.alpaca)
|
|
return cfg, store, clients
|
|
|
|
|
|
@app.command()
|
|
def tick():
|
|
"""Run one planning/execution cycle."""
|
|
cfg, store, clients = _bootstrap()
|
|
result = run_tick(cfg, clients, store)
|
|
rprint(result)
|
|
|
|
|
|
@app.command()
|
|
def loop():
|
|
"""Run indefinitely, polling every TICK_INTERVAL_SECONDS."""
|
|
cfg, store, clients = _bootstrap()
|
|
run_loop(cfg, clients, store)
|
|
|
|
|
|
@app.command()
|
|
def status():
|
|
"""Print account + positions snapshot."""
|
|
cfg, store, clients = _bootstrap()
|
|
acct = get_account_snapshot(clients)
|
|
positions = list_positions(clients)
|
|
rprint({"config_mode": cfg.mode, "env": cfg.alpaca.env, "account": acct})
|
|
t = Table(title="Positions")
|
|
for col in ("symbol", "qty", "avg_entry_price", "current_price", "market_value", "unrealized_pl"):
|
|
t.add_column(col)
|
|
for p in positions:
|
|
t.add_row(
|
|
str(p["symbol"]),
|
|
f"{p['qty']:g}",
|
|
f"{p['avg_entry_price']:.2f}",
|
|
f"{p['current_price']:.2f}",
|
|
f"{p['market_value']:.2f}",
|
|
f"{p['unrealized_pl']:+.2f}",
|
|
)
|
|
rprint(t)
|
|
|
|
|
|
@app.command()
|
|
def report():
|
|
"""Build today's summary and post to Discord webhook if configured."""
|
|
cfg, store, clients = _bootstrap()
|
|
acct = get_account_snapshot(clients)
|
|
positions = list_positions(clients)
|
|
today_start = datetime.now(tz=timezone.utc).replace(
|
|
hour=0, minute=0, second=0, microsecond=0
|
|
).isoformat()
|
|
intents_today = store.intents_since(today_start)
|
|
summary = build_daily_summary(
|
|
account=acct,
|
|
positions=positions,
|
|
intents_today=intents_today,
|
|
mode=cfg.mode,
|
|
)
|
|
rprint(summary)
|
|
if cfg.discord_webhook:
|
|
ok = post_to_discord(cfg.discord_webhook, summary)
|
|
rprint({"discord_posted": ok})
|
|
else:
|
|
rprint("(DISCORD_WEBHOOK_URL not set — skipping post)")
|
|
|
|
|
|
@app.command()
|
|
def dump_state(limit: int = 50):
|
|
"""Dump recent ticks + intents as JSON (for dashboards / debugging)."""
|
|
cfg, store, _ = _bootstrap()
|
|
rprint(
|
|
json.dumps(
|
|
{
|
|
"ticks": store.recent_ticks(limit),
|
|
"intents": store.recent_intents(limit),
|
|
},
|
|
default=str,
|
|
indent=2,
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|