Files
arbpulse/src/domain/services/arbitrage-engine.test.ts
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

194 lines
6.4 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { ArbitrageEngine } from "./arbitrage-engine.js";
import { ExecuteArbitrage } from "../../application/use-cases/execute-arbitrage.js";
import {
FakeExecutor,
FakeInventory,
FakePolicy,
FakeQuoteBook,
FakeRiskGate,
FakeStore,
FixedClock,
SeqIds,
book,
} from "../../test-support/test-fakes.js";
import type { ExchangeId, OpportunityStatus } from "../entities/index.js";
const NOW = 1_000_000;
interface Harness {
engine: ArbitrageEngine;
quotes: FakeQuoteBook;
store: FakeStore;
risk: FakeRiskGate;
executor: FakeExecutor;
policy: FakePolicy;
}
function harness(opts: { recvTs?: number; buyAsk?: number; sellBid?: number; flickerMs?: number } = {}): Harness {
const { recvTs = NOW, buyAsk = 100000, sellBid = 100600, flickerMs = 0 } = opts;
const quotes = new FakeQuoteBook();
const store = new FakeStore();
const risk = new FakeRiskGate(true);
const executor = new FakeExecutor(1);
const policy = new FakePolicy();
policy.maxTrade = 0.1;
policy.flickerMs = flickerMs;
quotes.update(book("bybit", [{ price: buyAsk - 10, qty: 1 }], [{ price: buyAsk, qty: 1 }], recvTs));
quotes.update(book("okx", [{ price: sellBid, qty: 1 }], [{ price: sellBid + 10, qty: 1 }], recvTs));
const opportunityExecutor = new ExecuteArbitrage(executor, store, risk);
const engine = new ArbitrageEngine({
quotes,
inventory: new FakeInventory(),
store,
risk,
opportunityExecutor,
policy,
clock: new FixedClock(NOW),
ids: new SeqIds(),
});
return { engine, quotes, store, risk, executor, policy };
}
function statusesFor(h: Harness, buy: ExchangeId, sell: ExchangeId): OpportunityStatus[] {
return h.store.opportunities
.filter((o) => o.buyExchange === buy && o.sellExchange === sell)
.map((o) => o.status);
}
function trigger(h: Harness): void {
h.engine.onBook(h.quotes.getBook("okx")!);
}
test("executes a clean, net-profitable, fresh, confirmed cross", () => {
const h = harness({ flickerMs: 0 });
trigger(h);
assert.deepEqual(statusesFor(h, "bybit", "okx"), ["executed"]);
assert.equal(h.executor.calls.length, 1);
assert.equal(h.store.trades.length, 1);
assert.equal(h.risk.evaluations.length, 1);
});
test("rejected_fees when the net edge is below the threshold", () => {
const h = harness({ buyAsk: 100000, sellBid: 100100, flickerMs: 0 });
trigger(h);
assert.deepEqual(statusesFor(h, "bybit", "okx"), ["rejected_fees"]);
assert.equal(h.executor.calls.length, 0);
assert.equal(h.store.trades.length, 0);
});
test("rejected_stale when a crossing quote is older than staleMs", () => {
const h = harness({ recvTs: NOW - 5000, flickerMs: 0 });
trigger(h);
assert.deepEqual(statusesFor(h, "bybit", "okx"), ["rejected_stale"]);
assert.equal(h.executor.calls.length, 0);
});
test("anti-flicker: first profitable tick is pending_confirm, not executed", () => {
const h = harness({ flickerMs: 150 });
trigger(h);
assert.deepEqual(statusesFor(h, "bybit", "okx"), ["pending_confirm"]);
assert.equal(h.executor.calls.length, 0);
});
test("does not execute while the risk gate is closed", () => {
const h = harness({ flickerMs: 0 });
h.risk.allow = false;
trigger(h);
assert.deepEqual(statusesFor(h, "bybit", "okx"), ["rejected_risk"]);
assert.equal(h.executor.calls.length, 0);
});
test("counts ticks processed", () => {
const h = harness({ flickerMs: 0 });
trigger(h);
assert.equal(h.store.ticksProcessed, 1);
});
test("executes only the highest netProfit opportunity when multiple pairs confirm in one tick", () => {
const quotes = new FakeQuoteBook();
const store = new FakeStore();
const risk = new FakeRiskGate(true);
const executor = new FakeExecutor(1);
const policy = new FakePolicy();
policy.maxTrade = 0.1;
policy.flickerMs = 0;
const recvTs = NOW;
// Small edge: buy bybit, sell okx
quotes.update(book("bybit", [{ price: 99990, qty: 1 }], [{ price: 100000, qty: 1 }], recvTs));
quotes.update(book("okx", [{ price: 100600, qty: 1 }], [{ price: 100610, qty: 1 }], recvTs));
// Large edge: buy kraken (cheaper ask), sell bybit
quotes.update(book("kraken", [{ price: 99400, qty: 1 }], [{ price: 99500, qty: 1 }], recvTs));
quotes.update(book("bybit", [{ price: 101500, qty: 1 }], [{ price: 101510, qty: 1 }], recvTs));
const opportunityExecutor = new ExecuteArbitrage(executor, store, risk);
const engine = new ArbitrageEngine({
quotes,
inventory: new FakeInventory(),
store,
risk,
opportunityExecutor,
policy,
clock: new FixedClock(NOW),
ids: new SeqIds(),
});
engine.onBook(quotes.getBook("kraken")!);
assert.equal(executor.calls.length, 1);
assert.equal(executor.calls[0]!.op.buyExchange, "kraken");
assert.equal(executor.calls[0]!.op.sellExchange, "bybit");
assert.equal(statusesFor({ engine, quotes, store, risk, executor, policy }, "bybit", "okx").length, 0);
assert.equal(store.trades.length, 1);
});
test("defers lower-profit pair to a later tick after the winner executes", () => {
const quotes = new FakeQuoteBook();
const store = new FakeStore();
const risk = new FakeRiskGate(true);
const executor = new FakeExecutor(1);
const policy = new FakePolicy();
policy.maxTrade = 0.1;
policy.flickerMs = 0;
const recvTs = NOW;
quotes.update(book("okx", [{ price: 100600, qty: 1 }], [{ price: 100610, qty: 1 }], recvTs));
quotes.update(book("kraken", [{ price: 99400, qty: 1 }], [{ price: 99500, qty: 1 }], recvTs));
quotes.update(book("bybit", [{ price: 101500, qty: 1 }], [{ price: 101510, qty: 1 }], recvTs));
const clock = new FixedClock(NOW);
const opportunityExecutor = new ExecuteArbitrage(executor, store, risk);
const engine = new ArbitrageEngine({
quotes,
inventory: new FakeInventory(),
store,
risk,
opportunityExecutor,
policy,
clock,
ids: new SeqIds(),
});
engine.onBook(quotes.getBook("kraken")!);
assert.equal(executor.calls.length, 1);
// Remove other crosses; bybit→okx should execute (anti-flicker already confirmed).
quotes.update(book("kraken", [{ price: 99400, qty: 1 }], [{ price: 101600, qty: 1 }], recvTs));
quotes.update(book("bybit", [{ price: 99990, qty: 1 }], [{ price: 100000, qty: 1 }], recvTs));
clock.t = NOW + 100;
engine.onBook(quotes.getBook("okx")!);
assert.equal(executor.calls.length, 2);
assert.equal(executor.calls[1]!.op.buyExchange, "bybit");
assert.equal(executor.calls[1]!.op.sellExchange, "okx");
});