Files
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

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()