Feat/support binance (#22)

* add docs

* Add Binance exchange support

- Updated the environment configuration to include Binance as a selectable exchange option.
- Enhanced the README documentation to reflect the addition of Binance.
- Implemented the Binance exchange adapter and integrated it into the existing exchange framework.
- Modified the basis arbitrage strategy to support Binance alongside existing exchanges.
- Added tests to ensure proper functionality and integration of Binance within the trading system.

* Enhance README with detailed Binance exchange configuration

- Added comprehensive instructions for setting up Binance as an exchange option.
- Included environment variable specifications for API keys, market types, and trading symbols.
- Provided examples for both perpetual and spot trading strategies.
- Clarified the use of WebSocket and REST for the Binance adapter.

* Enhance exchange support and testing framework

- Added a new test suite for exchange contracts to ensure consistency and functionality across supported exchanges.
- Refactored exchange ID handling to utilize a centralized list of supported exchanges, improving maintainability.
- Updated CLI argument parsing and help documentation to reflect the new exchange structure.
- Introduced utility functions for validating supported exchanges and their display names.
- Enhanced the BasisApp and strategy runner to leverage the new exchange validation logic.
- Added a new test command for running exchange-related tests.

* Refactor exchange contract tests and update CLI commands

- Removed the trailing supported exchanges set and simplified the logic for trailing stop support in the exchange contract tests.
- Updated the test command for exchange contracts to exclude unnecessary tests, streamlining the testing process.
- Enhanced test descriptions for clarity and improved understanding of the functionality being tested.
This commit is contained in:
Disney
2026-02-27 11:37:44 +08:00
committed by GitHub
parent 422ee6f465
commit d6399b92aa
588 changed files with 96879 additions and 106 deletions
+10 -1
View File
@@ -71,6 +71,13 @@ describe("BasisArbEngine", () => {
time: 2_000,
}),
};
const futuresClient = {
getPremiumIndex: vi.fn().mockResolvedValue({
fundingRate: "0.0001",
nextFundingTime: 3_600_000,
time: 2_000,
}),
};
const engine = new BasisArbEngine(
{
@@ -79,10 +86,12 @@ describe("BasisArbEngine", () => {
refreshIntervalMs: 1_000,
maxLogEntries: 10,
takerFeeRate: 0.0004,
arbAmount: 1,
},
adapter,
{
spotClient,
futuresClient,
now: () => 1_000,
}
);
@@ -111,7 +120,7 @@ describe("BasisArbEngine", () => {
const expectedNet = 1.04 * (1 - effectiveFee) - 1.05 * (1 + effectiveFee);
expect(snapshot.netSpread).toBeCloseTo(expectedNet, 6);
expect(snapshot.netSpreadBps).toBeCloseTo((expectedNet / 1.05) * 10_000, 6);
expect(snapshot.feedStatus).toEqual({ futures: true, spot: true });
expect(snapshot.feedStatus).toEqual({ futures: true, spot: true, funding: true });
expect(snapshot.opportunity).toBe(expectedNet >= 0);
engine.stop();
+7
View File
@@ -49,4 +49,11 @@ describe("resolveSymbolFromEnv", () => {
expect(resolveSymbolFromEnv("standx")).toBe("ETH-USD");
});
it("supports binance symbol defaults when explicit exchange id is provided", () => {
delete process.env.EXCHANGE;
process.env.BINANCE_SYMBOL = "ETHUSDT";
expect(resolveSymbolFromEnv("binance")).toBe("ETHUSDT");
});
});
+261
View File
@@ -0,0 +1,261 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
BASIS_SUPPORTED_EXCHANGE_IDS,
SUPPORTED_EXCHANGE_IDS,
getExchangeDisplayName,
resolveExchangeId,
type SupportedExchangeId,
} from "../src/exchanges/create-adapter";
import { parseCliArgs, printCliHelp } from "../src/cli/args";
import { resolveSymbolFromEnv } from "../src/config";
import {
routeCloseOrder,
routeLimitOrder,
routeMarketOrder,
routeStopOrder,
routeTrailingStopOrder,
} from "../src/exchanges/order-router";
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type {
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
CreateOrderParams,
} from "../src/exchanges/types";
const ORIGINAL_ENV = { ...process.env };
const REQUIRED_ENV_BY_EXCHANGE: Record<SupportedExchangeId, Record<string, string>> = {
aster: {
ASTER_API_KEY: "aster-key",
ASTER_API_SECRET: "aster-secret",
},
grvt: {
GRVT_API_KEY: "grvt-key",
GRVT_API_SECRET: `0x${"1".repeat(64)}`,
GRVT_SUB_ACCOUNT_ID: "sub-account",
GRVT_INSTRUMENT: "BTC_USDT_Perp",
GRVT_SYMBOL: "BTCUSDT",
},
lighter: {
LIGHTER_ACCOUNT_INDEX: "1",
LIGHTER_API_PRIVATE_KEY: "lighter-private-key",
LIGHTER_API_KEY_INDEX: "0",
},
backpack: {
BACKPACK_API_KEY: "backpack-key",
BACKPACK_API_SECRET: "backpack-secret",
},
paradex: {
PARADEX_PRIVATE_KEY: `0x${"2".repeat(64)}`,
PARADEX_WALLET_ADDRESS: `0x${"3".repeat(40)}`,
},
nado: {
NADO_SIGNER_PRIVATE_KEY: `0x${"4".repeat(64)}`,
NADO_SUBACCOUNT_OWNER: `0x${"5".repeat(40)}`,
},
standx: {
STANDX_TOKEN: "standx-token",
},
binance: {
BINANCE_API_KEY: "binance-key",
BINANCE_API_SECRET: "binance-secret",
},
};
class RecorderAdapter implements ExchangeAdapter {
readonly id: SupportedExchangeId;
public lastCreateOrderParams: CreateOrderParams | null = null;
constructor(id: SupportedExchangeId) {
this.id = id;
}
supportsTrailingStops(): boolean {
return false;
}
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
this.lastCreateOrderParams = params;
return {
orderId: 1,
clientOrderId: "test-client-order",
symbol: params.symbol,
side: params.side,
type: params.type,
status: "NEW",
price: String(params.price ?? 0),
origQty: String(params.quantity ?? 0),
executedQty: "0",
stopPrice: String(params.stopPrice ?? 0),
time: Date.now(),
updateTime: Date.now(),
reduceOnly: params.reduceOnly === "true",
closePosition: params.closePosition === "true",
timeInForce: params.timeInForce,
};
}
async cancelOrder(_params: { symbol: string; orderId: number | string }): Promise<void> {}
async cancelOrders(_params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {}
async cancelAllOrders(_params: { symbol: string }): Promise<void> {}
}
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
});
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});
describe("exchange contract suite", () => {
it("keeps exchange registry consistent and case-insensitive", () => {
expect(new Set(SUPPORTED_EXCHANGE_IDS).size).toBe(SUPPORTED_EXCHANGE_IDS.length);
expect(new Set(BASIS_SUPPORTED_EXCHANGE_IDS).size).toBe(BASIS_SUPPORTED_EXCHANGE_IDS.length);
for (const id of BASIS_SUPPORTED_EXCHANGE_IDS) {
expect(SUPPORTED_EXCHANGE_IDS).toContain(id);
}
for (const id of SUPPORTED_EXCHANGE_IDS) {
expect(resolveExchangeId(id.toUpperCase())).toBe(id);
expect(getExchangeDisplayName(id)).toBeTruthy();
}
});
it("accepts every supported exchange from CLI and documents them in help output", () => {
for (const id of SUPPORTED_EXCHANGE_IDS) {
expect(parseCliArgs(["--exchange", id]).exchange).toBe(id);
expect(parseCliArgs(["--exchange", id.toUpperCase()]).exchange).toBe(id);
}
const spy = vi.spyOn(console, "log").mockImplementation(() => undefined);
printCliHelp();
const output = spy.mock.calls.map((entry) => String(entry[0] ?? "")).join("\n");
for (const id of SUPPORTED_EXCHANGE_IDS) {
expect(output).toContain(id);
}
spy.mockRestore();
});
it("provides a default symbol fallback for every supported exchange", () => {
for (const id of SUPPORTED_EXCHANGE_IDS) {
const symbol = resolveSymbolFromEnv(id);
expect(typeof symbol).toBe("string");
expect(symbol.length).toBeGreaterThan(0);
}
});
it("builds the requested adapter id for every supported exchange", () => {
for (const id of SUPPORTED_EXCHANGE_IDS) {
process.env = { ...ORIGINAL_ENV, ...REQUIRED_ENV_BY_EXCHANGE[id] };
const adapter = buildAdapterFromEnv({ exchangeId: id, symbol: "BTCUSDT" });
expect(adapter.id).toBe(id);
expect(typeof adapter.supportsTrailingStops()).toBe("boolean");
expect(typeof adapter.watchAccount).toBe("function");
expect(typeof adapter.watchOrders).toBe("function");
expect(typeof adapter.watchDepth).toBe("function");
expect(typeof adapter.watchTicker).toBe("function");
expect(typeof adapter.watchKlines).toBe("function");
expect(typeof adapter.createOrder).toBe("function");
expect(typeof adapter.cancelOrder).toBe("function");
expect(typeof adapter.cancelOrders).toBe("function");
expect(typeof adapter.cancelAllOrders).toBe("function");
}
});
it("fails fast when required credentials are missing", () => {
for (const id of SUPPORTED_EXCHANGE_IDS) {
process.env = { ...ORIGINAL_ENV };
expect(() => buildAdapterFromEnv({ exchangeId: id, symbol: "BTCUSDT" })).toThrow();
}
});
it("routes core order intents for every supported exchange", async () => {
delete process.env.EXCHANGE;
delete process.env.TRADE_EXCHANGE;
for (const id of SUPPORTED_EXCHANGE_IDS) {
const adapter = new RecorderAdapter(id);
await routeLimitOrder({
adapter,
symbol: "BTCUSDT",
side: "BUY",
quantity: 0.01,
price: 100_000,
});
expect(adapter.lastCreateOrderParams?.type).toBe("LIMIT");
await routeMarketOrder({
adapter,
symbol: "BTCUSDT",
side: "SELL",
quantity: 0.01,
});
expect(adapter.lastCreateOrderParams?.type).toBe("MARKET");
await routeStopOrder({
adapter,
symbol: "BTCUSDT",
side: "SELL",
quantity: 0.01,
stopPrice: 99_000,
});
expect(adapter.lastCreateOrderParams?.type).toBe("STOP_MARKET");
await routeCloseOrder({
adapter,
symbol: "BTCUSDT",
side: "SELL",
quantity: 0.01,
reduceOnly: true,
closePosition: true,
});
expect(adapter.lastCreateOrderParams?.type).toBe("MARKET");
expect(adapter.lastCreateOrderParams?.reduceOnly).toBe("true");
}
});
it("routes trailing-stop intent by exchange capability (supported or explicit rejection)", async () => {
delete process.env.EXCHANGE;
delete process.env.TRADE_EXCHANGE;
for (const id of SUPPORTED_EXCHANGE_IDS) {
const adapter = new RecorderAdapter(id);
const intent = {
adapter,
symbol: "BTCUSDT",
side: "SELL" as const,
quantity: 0.01,
activationPrice: 101_000,
callbackRate: 0.2,
};
try {
const order = await routeTrailingStopOrder(intent);
expect(order.type).toBe("TRAILING_STOP_MARKET");
expect(adapter.lastCreateOrderParams?.type).toBe("TRAILING_STOP_MARKET");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
expect(message).toMatch(/does not support trailing stop/i);
}
}
});
});
+13
View File
@@ -5,6 +5,7 @@ import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
import { StandxExchangeAdapter } from "../src/exchanges/standx/adapter";
import { BinanceExchangeAdapter } from "../src/exchanges/binance/adapter";
const ORIGINAL_ENV = { ...process.env };
@@ -32,6 +33,7 @@ describe("exchange factory", () => {
expect(resolveExchangeId("BACKPACK")).toBe("backpack");
expect(resolveExchangeId("PaRaDeX")).toBe("paradex");
expect(resolveExchangeId("StandX")).toBe("standx");
expect(resolveExchangeId("BiNaNcE")).toBe("binance");
});
it("creates grvt adapter when EXCHANGE=grvt", () => {
@@ -79,4 +81,15 @@ describe("exchange factory", () => {
expect(adapter).toBeInstanceOf(StandxExchangeAdapter);
expect(adapter.id).toBe("standx");
});
it("creates binance adapter when EXCHANGE=binance", () => {
process.env.EXCHANGE = "binance";
process.env.BINANCE_API_KEY = "api-key";
process.env.BINANCE_API_SECRET = "api-secret";
process.env.BINANCE_SYMBOL = "BTCUSDT";
const adapter = createExchangeAdapter({ symbol: "BTCUSDT" });
expect(adapter).toBeInstanceOf(BinanceExchangeAdapter);
expect(adapter.id).toBe("binance");
});
});