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:04:53 -06:00
co-authored by Cursor
parent 908aefb2d8
commit 2e8744ccf0
87 changed files with 10414 additions and 0 deletions
@@ -0,0 +1,58 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { bootstrap } from "../../composition/bootstrap.js";
import { runtime } from "../../infrastructure/config/runtime.js";
import { FakeMarketDataFeedFactory } from "../../test-support/fake-market-data-feed.js";
import { book, FixedClock } from "../../test-support/test-fakes.js";
import { SyntheticFeed } from "../../infrastructure/demo/synthetic-feed.js";
const NOW = 2_000_000;
test("fixture feed drives detection through bootstrap without network", () => {
runtime.demoMode = false;
runtime.flickerConfirmMs = 0;
runtime.minNetProfitPct = 0.0001;
const fixtures = [
book("bybit", [{ price: 99_990, qty: 2 }], [{ price: 100_000, qty: 2 }], NOW),
book("okx", [{ price: 100_600, qty: 2 }], [{ price: 100_610, qty: 2 }], NOW),
book("kraken", [{ price: 100_200, qty: 2 }], [{ price: 100_210, qty: 2 }], NOW),
];
const ctx = bootstrap({
feedFactory: new FakeMarketDataFeedFactory(fixtures),
demoFeed: new SyntheticFeed(),
clock: new FixedClock(NOW),
});
ctx.start();
const snap = ctx.application.getSnapshot();
ctx.stop();
assert.ok(snap.stats.ticksProcessed >= 2, `ticks ${snap.stats.ticksProcessed}`);
const executed = snap.recentOpportunities.filter((o) => o.status === "executed" || o.status === "executed_partial");
assert.ok(executed.length >= 1, "expected at least one executed opportunity from fixture cross");
assert.ok(snap.stats.tradesExecuted >= 1, `trades ${snap.stats.tradesExecuted}`);
});
test("NDJSON-shaped fixture lines replay as order books", () => {
const line = {
ts: NOW,
exchange: "bybit" as const,
bids: [{ price: 99_000, qty: 1 }],
asks: [{ price: 99_010, qty: 1 }],
};
const fixtures = [book(line.exchange, line.bids, line.asks, line.ts)];
runtime.demoMode = false;
runtime.flickerConfirmMs = 0;
const ctx = bootstrap({
feedFactory: new FakeMarketDataFeedFactory(fixtures),
clock: new FixedClock(NOW),
});
ctx.start();
assert.equal(ctx.application.getSnapshot().stats.ticksProcessed, 1);
ctx.stop();
});
@@ -0,0 +1,127 @@
import { EXCHANGE_IDS, type ConfigPatch, type ExchangeId } from "../../domain/entities/index.js";
import type { IQuoteBook, IRiskGate } from "../../domain/ports/ports.js";
import type { StartMarketData } from "./start-market-data.js";
const MIN_PROFIT_PCT = 0.0001;
const MAX_PROFIT_PCT = 0.01;
const MIN_TRADE_BTC = 0.01;
const MAX_TRADE_BTC = 1.0;
const MAX_FLICKER_MS = 500;
export interface RuntimePort {
demoMode: boolean;
recordFeed: boolean;
minNetProfitPct: number;
maxTradeBtc: number;
flickerConfirmMs: number;
activeExchanges: Record<ExchangeId, boolean>;
}
export interface FeedRecorderControl {
close(): void;
}
/** Operator controls — mutates runtime via injected port, never reads process.env. */
export class ControlService {
constructor(
private readonly runtime: RuntimePort,
private readonly risk: IRiskGate,
private readonly quotes: IQuoteBook,
private readonly resetState: () => void,
private readonly marketData: StartMarketData,
private readonly recorder: FeedRecorderControl,
) {}
pause(): void {
this.risk.pause();
}
resume(): void {
this.risk.resume();
}
reset(): void {
this.resetState();
}
setDemoMode(enabled: boolean): void {
if (this.runtime.demoMode === enabled) return;
this.runtime.demoMode = enabled;
this.marketData.switchDemo(enabled);
}
setRecordFeed(enabled: boolean): void {
this.runtime.recordFeed = enabled;
if (!enabled) this.recorder.close();
}
setThreshold(pct: number): string | null {
if (!Number.isFinite(pct) || pct < MIN_PROFIT_PCT || pct > MAX_PROFIT_PCT) {
return `minNetProfitPct must be between ${MIN_PROFIT_PCT} and ${MAX_PROFIT_PCT}`;
}
this.runtime.minNetProfitPct = pct;
return null;
}
setMaxTradeBtc(btc: number): string | null {
if (!Number.isFinite(btc) || btc < MIN_TRADE_BTC || btc > MAX_TRADE_BTC) {
return `maxTradeBtc must be between ${MIN_TRADE_BTC} and ${MAX_TRADE_BTC}`;
}
this.runtime.maxTradeBtc = btc;
return null;
}
patchConfig(patch: ConfigPatch): string | null {
if (patch.minNetProfitPct !== undefined) {
const pct = patch.minNetProfitPct;
if (!Number.isFinite(pct) || pct < MIN_PROFIT_PCT || pct > MAX_PROFIT_PCT) {
return `minNetProfitPct must be between ${MIN_PROFIT_PCT} and ${MAX_PROFIT_PCT}`;
}
this.runtime.minNetProfitPct = pct;
}
if (patch.maxTradeBtc !== undefined) {
const btc = patch.maxTradeBtc;
if (!Number.isFinite(btc) || btc < MIN_TRADE_BTC || btc > MAX_TRADE_BTC) {
return `maxTradeBtc must be between ${MIN_TRADE_BTC} and ${MAX_TRADE_BTC}`;
}
this.runtime.maxTradeBtc = btc;
}
if (patch.flickerConfirmMs !== undefined) {
const ms = patch.flickerConfirmMs;
if (!Number.isFinite(ms) || ms < 0 || ms > MAX_FLICKER_MS) {
return `flickerConfirmMs must be between 0 and ${MAX_FLICKER_MS}`;
}
this.runtime.flickerConfirmMs = ms;
}
if (patch.activeExchanges !== undefined) {
const next = { ...this.runtime.activeExchanges };
for (const id of EXCHANGE_IDS) {
const enabled = patch.activeExchanges[id];
if (enabled !== undefined) next[id] = enabled;
}
if (!EXCHANGE_IDS.some((id) => next[id])) {
return "at least one exchange must remain active";
}
for (const id of EXCHANGE_IDS) {
if (this.runtime.activeExchanges[id] === next[id]) continue;
this.runtime.activeExchanges[id] = next[id];
this.marketData.applyExchangeToggle(id, next[id], (ex) => this.clearExchangeBook(ex));
}
}
return null;
}
private clearExchangeBook(exchange: ExchangeId): void {
this.quotes.update({
exchange,
bids: [],
asks: [],
recvTs: 0,
exchangeTs: null,
});
}
}
@@ -0,0 +1,17 @@
import type { Opportunity } from "../../domain/entities/index.js";
import type { IOpportunityExecutor, IRiskGate, IStateStore, ITradeExecutor } from "../../domain/ports/ports.js";
/** Risk gate + simulated execution + store update after a confirmed opportunity. */
export class ExecuteArbitrage implements IOpportunityExecutor {
constructor(
private readonly executor: ITradeExecutor,
private readonly store: IStateStore,
private readonly risk: IRiskGate,
) {}
execute(op: Opportunity, now: number): void {
const trade = this.executor.execute(op, now);
this.store.addTrade(trade);
this.risk.evaluate(now);
}
}
@@ -0,0 +1,21 @@
import type { OrderBook } from "../../domain/entities/index.js";
import type { ArbitrageEngine } from "../../domain/services/arbitrage-engine.js";
export interface FeedRecorderPort {
record(book: OrderBook): void;
}
/** Hot path: record optional NDJSON, then drive detection on the updated book. */
export class ProcessOrderBookUpdate {
constructor(
private readonly engine: ArbitrageEngine,
private readonly recorder: FeedRecorderPort,
private readonly isExchangeActive: (exchange: OrderBook["exchange"]) => boolean,
) {}
run(book: OrderBook): void {
if (!this.isExchangeActive(book.exchange)) return;
this.recorder.record(book);
this.engine.onBook(book);
}
}
@@ -0,0 +1,10 @@
import type { IRebalancer } from "../../domain/ports/ports.js";
/** Periodic inventory correction (withdrawal fee only here, not per trade). */
export class RebalanceInventory {
constructor(private readonly rebalancer: IRebalancer) {}
tick(now: number): void {
this.rebalancer.tick(now);
}
}
@@ -0,0 +1,90 @@
import { EXCHANGE_IDS, type ExchangeId } from "../../domain/entities/index.js";
import type { MarketDataFeed, MarketDataFeedFactory } from "../../domain/ports/ports.js";
export type FeedMode = "real" | "demo" | "stopped";
export interface DemoFeedPort {
start(): void;
stop(): void;
}
/**
* Starts real WS connectors or the synthetic demo feed. Listeners are wired
* once in composition; this class only controls lifecycle.
*/
export class StartMarketData {
private connectors = new Map<ExchangeId, MarketDataFeed>();
private mode: FeedMode = "stopped";
constructor(
private readonly feedFactory: MarketDataFeedFactory,
private readonly demoFeed: DemoFeedPort,
private readonly onBook: (book: import("../../domain/entities/index.js").OrderBook) => void,
private readonly isDemoMode: () => boolean,
private readonly isExchangeActive: (id: ExchangeId) => boolean,
) {}
getMode(): FeedMode {
return this.mode;
}
start(): void {
if (this.isDemoMode()) {
this.mode = "demo";
this.demoFeed.start();
} else {
this.mode = "real";
for (const id of EXCHANGE_IDS) {
if (this.isExchangeActive(id)) this.startConnector(id);
}
}
}
startConnector(id: ExchangeId): void {
if (this.connectors.has(id)) return;
const connector = this.feedFactory.create(id);
connector.onBook((book) => this.onBook(book));
connector.start();
this.connectors.set(id, connector);
}
stopConnector(id: ExchangeId): void {
const connector = this.connectors.get(id);
if (!connector) return;
connector.stop();
this.connectors.delete(id);
}
switchDemo(enabled: boolean): void {
this.stop();
if (enabled) {
this.mode = "demo";
this.demoFeed.start();
} else {
this.mode = "real";
for (const id of EXCHANGE_IDS) {
if (this.isExchangeActive(id)) this.startConnector(id);
}
}
}
applyExchangeToggle(id: ExchangeId, enabled: boolean, clearBook: (exchange: ExchangeId) => void): void {
if (this.mode === "demo") {
if (!enabled) clearBook(id);
return;
}
if (enabled) {
this.startConnector(id);
} else {
this.stopConnector(id);
clearBook(id);
}
}
stop(): void {
this.demoFeed.stop();
for (const c of this.connectors.values()) c.stop();
this.connectors.clear();
this.mode = "stopped";
}
}
@@ -0,0 +1,9 @@
import type { StartMarketData } from "./start-market-data.js";
export class StopMarketData {
constructor(private readonly marketData: StartMarketData) {}
run(): void {
this.marketData.stop();
}
}
@@ -0,0 +1,15 @@
import type { ArbitrageEngine } from "../../domain/services/arbitrage-engine.js";
import type { RebalanceInventory } from "./rebalance-inventory.js";
/** Periodic risk cooldown reset + rebalancer tick (independent of feed). */
export class TickRiskAndRebalance {
constructor(
private readonly engine: ArbitrageEngine,
private readonly rebalance: RebalanceInventory,
) {}
tick(now: number): void {
this.engine.tick(now);
this.rebalance.tick(now);
}
}