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:
2026-04-16 21:38:25 +02:00
commit 39875112a0
46 changed files with 5195 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
import { Kpi } from "./components/kpi";
import { EquityChart, type EquityPoint } from "./components/equity-chart";
import { PositionsTable } from "./components/positions-table";
import { OrdersTable } from "./components/orders-table";
import { IntentsTable } from "./components/intents-table";
import { dbStatus, recentIntents, recentTicks } from "./lib/db";
import {
credentialsPresent,
getAccount,
getClock,
getOrders,
getPositions,
} from "./lib/alpaca";
import { money, pct } from "./lib/format";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export default async function Page() {
const [account, positions, orders, clock] = await Promise.all([
getAccount(),
getPositions(),
getOrders(),
getClock(),
]);
const ticks = recentTicks(300).reverse();
const intents = recentIntents(200);
const db = dbStatus();
const chartData: EquityPoint[] = ticks
.filter((t) => t.equity !== null)
.map((t) => ({
ts: String(t.ts),
equity: Number(t.equity || 0),
cash: Number(t.cash || 0),
}));
const equity = Number(account?.equity || 0);
const lastEquity = Number(account?.last_equity || equity);
const dayPL = equity - lastEquity;
const dayPct = lastEquity ? dayPL / lastEquity : 0;
const creds = credentialsPresent();
return (
<main className="min-h-screen p-6 max-w-[1400px] mx-auto">
<header className="flex flex-wrap items-baseline justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
alpaclaudia<span className="text-brand">.</span>
</h1>
<p className="text-sm text-mute mt-1">
Alpaca paper-trading bot Wheel + trailing stops.
</p>
</div>
<div className="flex items-center gap-2 text-xs">
{creds ? (
<span className="chip text-up border-up/30">
Alpaca connected
</span>
) : (
<span className="chip text-down border-down/30">
No Alpaca creds
</span>
)}
{clock && (
<span className="chip">
market {clock.is_open ? "open" : "closed"}
</span>
)}
{db.ok ? (
<span className="chip text-mute">db ok</span>
) : (
<span className="chip text-down">db missing</span>
)}
</div>
</header>
<section className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<Kpi
label="Equity"
value={account ? money(account.equity) : "—"}
sub={account ? `PV ${money(account.portfolio_value)}` : "not connected"}
/>
<Kpi
label="Cash"
value={account ? money(account.cash) : "—"}
sub={account ? `BP ${money(account.buying_power)}` : undefined}
/>
<Kpi
label="Day P&L"
value={money(dayPL, { sign: true })}
sub={pct(dayPct)}
tone={dayPL > 0 ? "up" : dayPL < 0 ? "down" : "mute"}
/>
<Kpi
label="Positions"
value={positions?.length ?? 0}
sub={`${(positions || []).filter((p) => p.asset_class?.toLowerCase().includes("option")).length} option · ${
(positions || []).filter((p) => !p.asset_class?.toLowerCase().includes("option")).length
} equity`}
/>
</section>
<section className="mb-6">
<EquityChart data={chartData} />
</section>
<section className="grid grid-cols-1 gap-6">
<PositionsTable positions={positions || []} />
<IntentsTable intents={intents as any} />
<OrdersTable orders={orders || []} />
</section>
<footer className="mt-8 text-xs text-mute">
db: <code>{db.path}</code>
{db.error && <span className="text-down"> {db.error}</span>}
</footer>
</main>
);
}