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
@@ -0,0 +1,60 @@
import type { Position } from "../lib/alpaca";
import { money, num, pct } from "../lib/format";
export function PositionsTable({ positions }: { positions: Position[] }) {
if (!positions.length) {
return (
<div className="panel p-6 text-mute text-sm">No open positions.</div>
);
}
return (
<div className="panel overflow-hidden">
<div className="px-4 py-3 border-b border-border flex items-center justify-between">
<div className="text-sm font-medium">Positions</div>
<div className="text-xs text-mute">{positions.length}</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="table-header border-b border-border">
<th className="text-left px-4 py-2 font-normal">Symbol</th>
<th className="text-left px-4 py-2 font-normal">Class</th>
<th className="text-right px-4 py-2 font-normal">Qty</th>
<th className="text-right px-4 py-2 font-normal">Entry</th>
<th className="text-right px-4 py-2 font-normal">Mark</th>
<th className="text-right px-4 py-2 font-normal">Market value</th>
<th className="text-right px-4 py-2 font-normal">P&amp;L</th>
<th className="text-right px-4 py-2 font-normal">%</th>
</tr>
</thead>
<tbody>
{positions.map((p) => {
const pl = Number(p.unrealized_pl);
const plpc = Number(p.unrealized_plpc);
const tone = pl > 0 ? "text-up" : pl < 0 ? "text-down" : "text-mute";
const isOpt = p.asset_class?.toLowerCase().includes("option");
return (
<tr key={p.symbol} className="row-hover border-b border-border/60">
<td className="px-4 py-2 font-mono">{p.symbol}</td>
<td className="px-4 py-2">
<span className="chip">
{isOpt ? "opt" : "eq"} · {p.side || "long"}
</span>
</td>
<td className="px-4 py-2 text-right num">{num(p.qty, 0)}</td>
<td className="px-4 py-2 text-right num">{money(p.avg_entry_price)}</td>
<td className="px-4 py-2 text-right num">{money(p.current_price)}</td>
<td className="px-4 py-2 text-right num">{money(p.market_value)}</td>
<td className={`px-4 py-2 text-right num ${tone}`}>
{money(pl, { sign: true })}
</td>
<td className={`px-4 py-2 text-right num ${tone}`}>{pct(plpc)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}