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.
92 lines
3.5 KiB
TypeScript
92 lines
3.5 KiB
TypeScript
import { num, timeAgo } from "../lib/format";
|
|
|
|
type Intent = {
|
|
id: number;
|
|
ts: string;
|
|
strategy: string;
|
|
symbol: string;
|
|
side: string;
|
|
qty: number;
|
|
order_type: string;
|
|
limit_price: number | null;
|
|
trail_percent: number | null;
|
|
details_json: string;
|
|
submitted: number;
|
|
alpaca_order_id: string | null;
|
|
status: string | null;
|
|
};
|
|
|
|
function statusTone(status: string | null, submitted: number): string {
|
|
if (status === "blocked") return "text-down";
|
|
if (status === "error") return "text-down";
|
|
if (submitted) return "text-up";
|
|
if (status === "dry_run") return "text-mute";
|
|
return "text-warn";
|
|
}
|
|
|
|
export function IntentsTable({ intents }: { intents: Intent[] }) {
|
|
if (!intents.length)
|
|
return (
|
|
<div className="panel p-6 text-mute text-sm">
|
|
No strategy intents recorded yet. Run the bot once: <code>alpaclaudia tick</code>.
|
|
</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">Bot intents (latest 200)</div>
|
|
<div className="text-xs text-mute">{intents.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">When</th>
|
|
<th className="text-left px-4 py-2 font-normal">Strategy</th>
|
|
<th className="text-left px-4 py-2 font-normal">Symbol</th>
|
|
<th className="text-left px-4 py-2 font-normal">Side</th>
|
|
<th className="text-left px-4 py-2 font-normal">Type</th>
|
|
<th className="text-right px-4 py-2 font-normal">Qty</th>
|
|
<th className="text-left px-4 py-2 font-normal">Outcome</th>
|
|
<th className="text-left px-4 py-2 font-normal">Rationale</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{intents.map((i) => {
|
|
let rationale = "";
|
|
let blockedReason = "";
|
|
try {
|
|
const d = JSON.parse(i.details_json || "{}");
|
|
rationale = d.rationale || "";
|
|
blockedReason = d.blocked_reason || d.error || "";
|
|
} catch {}
|
|
const tone = statusTone(i.status, i.submitted);
|
|
return (
|
|
<tr key={i.id} className="row-hover border-b border-border/60 align-top">
|
|
<td className="px-4 py-2 text-mute text-xs whitespace-nowrap">
|
|
{timeAgo(i.ts)}
|
|
</td>
|
|
<td className="px-4 py-2 text-xs">{i.strategy}</td>
|
|
<td className="px-4 py-2 font-mono text-xs">{i.symbol}</td>
|
|
<td className="px-4 py-2 uppercase text-xs">{i.side}</td>
|
|
<td className="px-4 py-2 text-xs">{i.order_type}</td>
|
|
<td className="px-4 py-2 text-right num">{num(i.qty, 0)}</td>
|
|
<td className={`px-4 py-2 text-xs ${tone}`}>
|
|
{i.status || (i.submitted ? "submitted" : "pending")}
|
|
</td>
|
|
<td className="px-4 py-2 text-xs text-mute max-w-[520px]">
|
|
{rationale}
|
|
{blockedReason && (
|
|
<div className="text-down mt-0.5">blocked: {blockedReason}</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|