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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user