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.
123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|