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
+73
View File
@@ -0,0 +1,73 @@
"use client";
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
export type EquityPoint = { ts: string; equity: number; cash: number };
export function EquityChart({ data }: { data: EquityPoint[] }) {
if (!data.length) {
return (
<div className="panel p-6 text-mute text-sm h-[300px] flex items-center justify-center">
No ticks recorded yet the bot hasn't run.
</div>
);
}
return (
<div className="panel p-4 h-[320px]">
<div className="text-xs text-mute mb-2 uppercase tracking-wider">Equity (ticks)</div>
<ResponsiveContainer width="100%" height="90%">
<AreaChart data={data} margin={{ top: 8, right: 12, bottom: 0, left: 0 }}>
<defs>
<linearGradient id="g-equity" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#5eead4" stopOpacity={0.55} />
<stop offset="95%" stopColor="#5eead4" stopOpacity={0.0} />
</linearGradient>
</defs>
<CartesianGrid stroke="#252a35" strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="ts"
stroke="#8a94a6"
fontSize={11}
tickFormatter={(v: string) => v.slice(11, 16)}
minTickGap={32}
/>
<YAxis
stroke="#8a94a6"
fontSize={11}
width={64}
tickFormatter={(v: number) => `$${(v / 1000).toFixed(1)}k`}
domain={["auto", "auto"]}
/>
<Tooltip
contentStyle={{
background: "#13161d",
border: "1px solid #252a35",
borderRadius: 8,
fontSize: 12,
}}
labelStyle={{ color: "#8a94a6" }}
formatter={(v: unknown) => {
const n = Number(v);
return [`$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`, "Equity"];
}}
/>
<Area
type="monotone"
dataKey="equity"
stroke="#5eead4"
strokeWidth={2}
fill="url(#g-equity)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}