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>
This commit is contained in:
Mauricio Barragan
2026-06-08 21:03:33 -06:00
parent 908aefb2d8
commit 2e8744ccf0
87 changed files with 10414 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
import type { ExchangeId, Level, OrderBook } from "../domain/entities/index.js";
import type { MarketDataFeed, MarketDataFeedFactory } from "../domain/ports/ports.js";
/** NDJSON line shape written by FeedRecorder (replay-compatible). */
export interface NdjsonBookLine {
ts: number;
exchange: ExchangeId;
bids: Level[];
asks: Level[];
}
export function lineToOrderBook(line: NdjsonBookLine): OrderBook {
return {
exchange: line.exchange,
bids: line.bids,
asks: line.asks,
recvTs: line.ts,
exchangeTs: line.ts,
};
}
/** In-memory feed for integration tests — no network or WebSocket. */
export class FakeMarketDataFeed implements MarketDataFeed {
private listeners: Array<(book: OrderBook) => void> = [];
constructor(private readonly fixtures: OrderBook[]) {}
onBook(listener: (book: OrderBook) => void): void {
this.listeners.push(listener);
}
start(): void {
for (const fixture of this.fixtures) {
for (const listener of this.listeners) listener(fixture);
}
}
stop(): void {
/* no-op */
}
}
export class FakeMarketDataFeedFactory implements MarketDataFeedFactory {
constructor(private readonly fixtures: OrderBook[]) {}
create(id: ExchangeId): MarketDataFeed {
return new FakeMarketDataFeed(this.fixtures.filter((f) => f.exchange === id));
}
}
+236
View File
@@ -0,0 +1,236 @@
import {
EXCHANGE_IDS,
type CircuitState,
type ExchangeId,
type Level,
type Opportunity,
type OrderBook,
type RebalanceEvent,
type Trade,
type Wallet,
} from "../domain/entities/index.js";
import type {
IClock,
IIdGenerator,
IInventory,
IQuoteBook,
IRiskGate,
IStateStore,
ITradeExecutor,
TradingPolicy,
} from "../domain/ports/ports.js";
export class FakePolicy implements TradingPolicy {
takerFees: Record<ExchangeId, number> = { kraken: 0.0026, bybit: 0.001, okx: 0.001, binance: 0.001 };
withdrawalFeesBtc: Record<ExchangeId, number> = {
kraken: 0.00002,
bybit: 0.00005,
okx: 0.00004,
binance: 0.0005,
};
minNet = 0.0005;
maxTrade = 0.25;
flickerMs = 150;
latencyBps = 2;
breakerLosses = 5;
breakerCooldownMs = 15000;
rebalanceIntervalMsValue = 20000;
rebalanceMinBtcValue = 0.075;
rebalanceMinUsdtValue = 7500;
demo = false;
takerFee(exchange: ExchangeId): number {
return this.takerFees[exchange];
}
withdrawalFeeBtc(exchange: ExchangeId): number {
return this.withdrawalFeesBtc[exchange];
}
minNetProfitPct(): number {
return this.minNet;
}
maxTradeBtc(): number {
return this.maxTrade;
}
flickerConfirmMs(): number {
return this.flickerMs;
}
latencySlippageBps(): number {
return this.latencyBps;
}
circuitBreakerLosses(): number {
return this.breakerLosses;
}
circuitBreakerCooldownMs(): number {
return this.breakerCooldownMs;
}
rebalanceIntervalMs(): number {
return this.rebalanceIntervalMsValue;
}
rebalanceMinBtc(): number {
return this.rebalanceMinBtcValue;
}
rebalanceMinUsdt(): number {
return this.rebalanceMinUsdtValue;
}
isDemo(): boolean {
return this.demo;
}
}
export class FixedClock implements IClock {
constructor(public t: number) {}
now(): number {
return this.t;
}
}
export class SeqIds implements IIdGenerator {
private n = 0;
next(prefix: string): string {
this.n += 1;
return `${prefix}_${this.n}`;
}
}
export class FakeStore implements IStateStore {
ticksProcessed = 0;
tradesRejected = 0;
circuit: CircuitState = "running";
consecutiveLosses = 0;
tickTimes: number[] = [];
opportunities: Opportunity[] = [];
trades: Trade[] = [];
rebalances: RebalanceEvent[] = [];
recordTickTime(ms: number): void {
this.tickTimes.push(ms);
}
addOpportunity(op: Opportunity): void {
this.opportunities.push(op);
}
addTrade(trade: Trade): void {
this.trades.push(trade);
}
addRebalance(event: RebalanceEvent): void {
this.rebalances.push(event);
}
}
export class FakeRiskGate implements IRiskGate {
evaluations: number[] = [];
ticks: number[] = [];
constructor(public allow = true) {}
canExecute(): boolean {
return this.allow;
}
evaluate(now: number): void {
this.evaluations.push(now);
}
tick(now: number): void {
this.ticks.push(now);
}
pause(): void {
this.allow = false;
}
resume(): void {
this.allow = true;
}
}
export class FakeExecutor implements ITradeExecutor {
calls: { op: Opportunity; now: number }[] = [];
constructor(public netProfit = 1) {}
execute(op: Opportunity, now: number): Trade {
this.calls.push({ op, now });
return {
id: `trade_${this.calls.length}`,
ts: now,
buyExchange: op.buyExchange,
sellExchange: op.sellExchange,
volumeBtc: op.volumeBtc,
requestedBtc: op.volumeBtc,
buyVwap: op.buyVwap,
sellVwap: op.sellVwap,
execBuyVwap: op.buyVwap,
execSellVwap: op.sellVwap,
feeBuy: op.feeBuy,
feeSell: op.feeSell,
netProfit: this.netProfit,
netProfitPct: 0,
partial: op.status === "executed_partial",
demo: op.demo,
};
}
}
export class FakeInventory implements IInventory {
wallets = new Map<ExchangeId, Wallet>();
transfers: { from: ExchangeId; to: ExchangeId; asset: "BTC" | "USDT"; amount: number; fee: number }[] = [];
constructor(init: Partial<Record<ExchangeId, { usdt: number; btc: number }>> = {}) {
for (const e of EXCHANGE_IDS) {
const w = init[e] ?? { usdt: 50000, btc: 0.5 };
this.wallets.set(e, { exchange: e, usdt: w.usdt, btc: w.btc });
}
}
get(exchange: ExchangeId): Wallet {
const w = this.wallets.get(exchange);
if (!w) throw new Error(`unknown wallet ${exchange}`);
return w;
}
maxBuyableBtc(exchange: ExchangeId, vwapWithFee: number): number {
if (vwapWithFee <= 0) return 0;
return this.get(exchange).usdt / vwapWithFee;
}
sellableBtc(exchange: ExchangeId): number {
return this.get(exchange).btc;
}
applyBuy(exchange: ExchangeId, btc: number, quoteCost: number): void {
const w = this.get(exchange);
w.btc += btc;
w.usdt -= quoteCost;
}
applySell(exchange: ExchangeId, btc: number, quoteProceeds: number): void {
const w = this.get(exchange);
w.btc -= btc;
w.usdt += quoteProceeds;
}
applyTransfer(from: ExchangeId, to: ExchangeId, asset: "BTC" | "USDT", amount: number, fee: number): void {
this.transfers.push({ from, to, asset, amount, fee });
const src = this.get(from);
const dst = this.get(to);
if (asset === "BTC") {
src.btc -= amount;
dst.btc += amount - fee;
} else {
src.usdt -= amount;
dst.usdt += amount - fee;
}
}
}
export class FakeQuoteBook implements IQuoteBook {
books = new Map<ExchangeId, OrderBook>();
constructor(public staleMs = 3000) {}
update(b: OrderBook): void {
this.books.set(b.exchange, b);
}
getBook(exchange: ExchangeId): OrderBook | undefined {
return this.books.get(exchange);
}
isFresh(exchange: ExchangeId, now: number): boolean {
const b = this.books.get(exchange);
if (!b) return false;
return now - b.recvTs <= this.staleMs;
}
}
export function book(
exchange: ExchangeId,
bids: Level[],
asks: Level[],
recvTs: number,
): OrderBook {
return { exchange, bids, asks, recvTs, exchangeTs: recvTs };
}