Files
arbpulse_github/web/src/components/PriceMatrix.tsx
T
Mauricio Barragan 2e8744ccf0 Initial commit: Arb Pulse monolith with CI and optional Fly deploy.
Real-time BTC cross-exchange arbitrage detection (Kraken, Bybit, OKX, Binance)
with React dashboard, GitHub Actions CI, and documented Fly.io deploy workflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 21:04:53 -06:00

104 lines
3.8 KiB
TypeScript

import type { BestQuote, ExchangeId } from "../types";
import { Card } from "./Card";
import { clsx, usd } from "../format";
interface Props {
quotes: BestQuote[];
}
const LABELS: Record<ExchangeId, string> = {
kraken: "Kraken",
bybit: "Bybit",
okx: "OKX",
binance: "Binance",
};
function statusDot(status: BestQuote["status"]): string {
switch (status) {
case "live":
return "bg-profit";
case "stale":
return "bg-warn";
case "down":
return "bg-loss";
case "connecting":
return "bg-slate-500";
default: {
const _exhaustive: never = status;
return _exhaustive;
}
}
}
export function PriceMatrix({ quotes }: Props): JSX.Element {
// Highlight the best (lowest) ask and best (highest) bid across venues.
const asks = quotes.map((q) => q.ask).filter((v): v is number => v !== null);
const bids = quotes.map((q) => q.bid).filter((v): v is number => v !== null);
const minAsk = asks.length ? Math.min(...asks) : null;
const maxBid = bids.length ? Math.max(...bids) : null;
const crossed = minAsk !== null && maxBid !== null && minAsk < maxBid;
return (
<Card
title="Order Book — Best Bid / Ask"
subtitle="BTC/USDT across venues"
right={
crossed ? (
<span className="rounded-full border border-profit/40 bg-profit/10 px-3 py-1 text-xs font-semibold text-profit">
DIVERGENCE
</span>
) : undefined
}
>
<div className="overflow-hidden rounded-xl border border-ink-600/50">
<table className="w-full text-sm">
<thead>
<tr className="bg-ink-700/50 text-left text-xs uppercase tracking-wider text-slate-400">
<th className="px-4 py-2 font-medium">Exchange</th>
<th className="px-4 py-2 text-right font-medium">Bid</th>
<th className="px-4 py-2 text-right font-medium">Bid Qty</th>
<th className="px-4 py-2 text-right font-medium">Ask</th>
<th className="px-4 py-2 text-right font-medium">Ask Qty</th>
<th className="px-4 py-2 text-right font-medium">Spread</th>
</tr>
</thead>
<tbody className="font-mono tabular-nums">
{quotes.map((q) => {
const spread = q.ask !== null && q.bid !== null ? q.ask - q.bid : null;
return (
<tr key={q.exchange} className="border-t border-ink-600/40">
<td className="px-4 py-2.5">
<span className="flex items-center gap-2 font-display text-sm">
<span className={clsx("h-2 w-2 rounded-full", statusDot(q.status))} />
{LABELS[q.exchange]}
</span>
</td>
<td
className={clsx(
"px-4 py-2.5 text-right",
q.bid !== null && q.bid === maxBid ? "font-semibold text-profit" : "text-slate-300",
)}
>
{q.bid !== null ? usd(q.bid) : "—"}
</td>
<td className="px-4 py-2.5 text-right text-slate-500">{q.bidQty?.toFixed(3) ?? "—"}</td>
<td
className={clsx(
"px-4 py-2.5 text-right",
q.ask !== null && q.ask === minAsk ? "font-semibold text-accent" : "text-slate-300",
)}
>
{q.ask !== null ? usd(q.ask) : "—"}
</td>
<td className="px-4 py-2.5 text-right text-slate-500">{q.askQty?.toFixed(3) ?? "—"}</td>
<td className="px-4 py-2.5 text-right text-slate-400">{spread !== null ? usd(spread) : "—"}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</Card>
);
}