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
+75
View File
@@ -0,0 +1,75 @@
const BASE = process.env.ALPACA_BASE_URL || "https://paper-api.alpaca.markets";
function headers(): HeadersInit {
const key = process.env.ALPACA_API_KEY || "";
const sec = process.env.ALPACA_API_SECRET || "";
return {
"APCA-API-KEY-ID": key,
"APCA-API-SECRET-KEY": sec,
Accept: "application/json",
};
}
export function credentialsPresent(): boolean {
return Boolean(process.env.ALPACA_API_KEY && process.env.ALPACA_API_SECRET);
}
async function alpacaGet<T>(pathname: string): Promise<T | null> {
if (!credentialsPresent()) return null;
try {
const res = await fetch(`${BASE}${pathname}`, {
headers: headers(),
cache: "no-store",
});
if (!res.ok) return null;
return (await res.json()) as T;
} catch {
return null;
}
}
export type Account = {
equity: string;
cash: string;
buying_power: string;
portfolio_value: string;
status: string;
pattern_day_trader?: boolean;
last_equity?: string;
};
export type Position = {
symbol: string;
qty: string;
avg_entry_price: string;
current_price: string;
market_value: string;
unrealized_pl: string;
unrealized_plpc: string;
side: string;
asset_class: string;
};
export type Order = {
id: string;
symbol: string;
side: string;
qty: string | null;
filled_qty: string | null;
order_type: string;
status: string;
submitted_at: string;
filled_at: string | null;
filled_avg_price: string | null;
trail_percent?: string | null;
limit_price?: string | null;
stop_price?: string | null;
asset_class?: string;
};
export type Clock = { is_open: boolean; next_open: string; next_close: string };
export const getAccount = () => alpacaGet<Account>("/v2/account");
export const getPositions = () => alpacaGet<Position[]>("/v2/positions");
export const getOrders = () => alpacaGet<Order[]>("/v2/orders?status=all&limit=50&nested=true");
export const getClock = () => alpacaGet<Clock>("/v2/clock");