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
+6 -11
View File
@@ -1,10 +1,12 @@
import { SUPPORTED_EXCHANGE_IDS, type SupportedExchangeId } from "../exchanges/create-adapter";
export type StrategyId = "trend" | "swing" | "guardian" | "maker" | "maker-points" | "offset-maker" | "liquidity-maker" | "basis" | "grid";
export interface CliOptions {
strategy?: StrategyId;
silent: boolean;
help: boolean;
exchange?: "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado" | "standx";
exchange?: SupportedExchangeId;
}
const STRATEGY_VALUES = new Set<StrategyId>([
@@ -82,15 +84,7 @@ function assignStrategy(options: CliOptions, raw: string): void {
function assignExchange(options: CliOptions, raw: string): void {
const normalized = raw.trim().toLowerCase();
if (!normalized) return;
if (
normalized === "aster" ||
normalized === "grvt" ||
normalized === "lighter" ||
normalized === "backpack" ||
normalized === "paradex" ||
normalized === "nado" ||
normalized === "standx"
) {
if (SUPPORTED_EXCHANGE_IDS.includes(normalized as SupportedExchangeId)) {
options.exchange = normalized as CliOptions["exchange"];
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
options.exchange = "grvt";
@@ -98,8 +92,9 @@ function assignExchange(options: CliOptions, raw: string): void {
}
export function printCliHelp(): void {
const exchangeList = SUPPORTED_EXCHANGE_IDS.join("|");
// eslint-disable-next-line no-console
console.log(`Usage: bun run index.ts [--strategy <trend|swing|guardian|maker|maker-points|offset-maker|liquidity-maker|basis|grid>] [--exchange <aster|grvt|lighter|backpack|paradex|nado|standx>] [--silent]\n\n` +
console.log(`Usage: bun run index.ts [--strategy <trend|swing|guardian|maker|maker-points|offset-maker|liquidity-maker|basis|grid>] [--exchange <${exchangeList}>] [--silent]\n\n` +
`Options:\n` +
` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` +
` Aliases: offset, offset-maker for the offset maker engine.\n` +
+3 -3
View File
@@ -1,5 +1,5 @@
import { basisConfig, gridConfig, isBasisStrategyEnabled, liquidityMakerConfig, makerConfig, makerPointsConfig, swingConfig, tradingConfig } from "../config";
import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-adapter";
import { getExchangeDisplayName, isBasisSupportedExchangeId, resolveExchangeId } from "../exchanges/create-adapter";
import type { ExchangeAdapter } from "../exchanges/adapter";
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
import { MakerEngine, type MakerEngineSnapshot } from "../strategy/maker-engine";
@@ -141,8 +141,8 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
throw new Error("Basis arbitrage strategy is disabled. Set ENABLE_BASIS_STRATEGY=true to enable it.");
}
const exchangeId = resolveExchangeId();
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx") {
throw new Error("Basis arbitrage strategy currently only supports the Aster, Nado, and StandX exchanges");
if (!isBasisSupportedExchangeId(exchangeId)) {
throw new Error("Basis arbitrage strategy currently only supports the Aster, Nado, StandX, and Binance exchanges");
}
const adapter = createAdapterOrThrow(basisConfig.futuresSymbol);
const engine = new BasisArbEngine(basisConfig, adapter);
+3
View File
@@ -97,6 +97,7 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
paradex: { envKeys: ["PARADEX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC/USDC" },
nado: { envKeys: ["NADO_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-PERP" },
standx: { envKeys: ["STANDX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD" },
binance: { envKeys: ["BINANCE_SYMBOL", "TRADE_SYMBOL"], fallback: "BTCUSDT" },
};
export function resolveSymbolFromEnv(explicitExchangeId?: SupportedExchangeId | string | null): string {
@@ -312,6 +313,7 @@ export const basisConfig: BasisArbConfig = {
const exchange = (process.env.EXCHANGE ?? "").trim().toLowerCase();
if (exchange === "nado") return "BTC-PERP";
if (exchange === "standx") return "BTC-USD";
if (exchange === "binance") return "BTCUSDT_PERP";
return "ASTERUSDT";
})()
),
@@ -321,6 +323,7 @@ export const basisConfig: BasisArbConfig = {
const exchange = (process.env.EXCHANGE ?? "").trim().toLowerCase();
if (exchange === "nado") return "KBTC";
if (exchange === "standx") return "BTC-USD";
if (exchange === "binance") return "BTCUSDT";
return "ASTERUSDT";
})()
),
+231
View File
@@ -0,0 +1,231 @@
import { clearTimeout, setTimeout } from "timers";
import type {
AccountListener,
DepthListener,
ExchangeAdapter,
ExchangePrecision,
FundingRateListener,
KlineListener,
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import { BinanceGateway, type BinanceGatewayOptions } from "./gateway";
export interface BinanceCredentials {
apiKey?: string;
apiSecret?: string;
symbol?: string;
marketType?: "spot" | "perp" | "auto";
sandbox?: boolean;
spotRestUrl?: string;
futuresRestUrl?: string;
spotWsUrl?: string;
futuresWsUrl?: string;
logger?: BinanceGatewayOptions["logger"];
}
export class BinanceExchangeAdapter implements ExchangeAdapter {
readonly id = "binance";
private readonly gateway: BinanceGateway;
private readonly symbol: string;
private readonly marketType: "spot" | "perp" | "auto";
private initPromise: Promise<void> | null = null;
private readonly initContexts = new Set<string>();
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: BinanceCredentials = {}) {
const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY;
const apiSecret = credentials.apiSecret ?? process.env.BINANCE_API_SECRET;
if (!apiKey || !apiSecret) {
throw new Error("Missing BINANCE_API_KEY or BINANCE_API_SECRET environment variable");
}
this.symbol = (credentials.symbol ?? process.env.BINANCE_SYMBOL ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").trim().toUpperCase();
const modeRaw = (credentials.marketType ?? process.env.BINANCE_MARKET_TYPE ?? "perp").trim().toLowerCase();
this.marketType = modeRaw === "spot" ? "spot" : modeRaw === "auto" ? "auto" : "perp";
this.gateway = new BinanceGateway({
apiKey,
apiSecret,
symbol: this.symbol,
marketType: this.marketType,
sandbox: credentials.sandbox,
spotRestUrl: credentials.spotRestUrl,
futuresRestUrl: credentials.futuresRestUrl,
spotWsUrl: credentials.spotWsUrl,
futuresWsUrl: credentials.futuresWsUrl,
logger: credentials.logger,
});
}
supportsTrailingStops(): boolean {
return this.marketType !== "spot";
}
watchAccount(cb: AccountListener): void {
const safe = this.safeInvoke("watchAccount", cb);
void this.ensureInitialized("watchAccount")
.then(() => {
this.gateway.onAccount(safe);
})
.catch((error) => this.handleInitError("watchAccount", error));
}
watchOrders(cb: OrderListener): void {
const safe = this.safeInvoke("watchOrders", cb);
void this.ensureInitialized("watchOrders")
.then(() => {
this.gateway.onOrders(safe);
})
.catch((error) => this.handleInitError("watchOrders", error));
}
watchDepth(symbol: string, cb: DepthListener): void {
const safe = this.safeInvoke("watchDepth", cb);
void this.ensureInitialized(`watchDepth:${symbol}`)
.then(() => {
this.gateway.onDepth(symbol, safe);
})
.catch((error) => this.handleInitError("watchDepth", error));
}
watchTicker(symbol: string, cb: TickerListener): void {
const safe = this.safeInvoke("watchTicker", cb);
void this.ensureInitialized(`watchTicker:${symbol}`)
.then(() => {
this.gateway.onTicker(symbol, safe);
})
.catch((error) => this.handleInitError("watchTicker", error));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
const safe = this.safeInvoke("watchKlines", cb);
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`)
.then(() => {
this.gateway.onKlines(symbol, interval, safe);
})
.catch((error) => this.handleInitError("watchKlines", error));
}
watchFundingRate(symbol: string, cb: FundingRateListener): void {
const safe = this.safeInvoke("watchFundingRate", cb);
void this.ensureInitialized(`watchFundingRate:${symbol}`)
.then(() => {
this.gateway.onFundingRate(symbol, safe);
})
.catch((error) => this.handleInitError("watchFundingRate", error));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
await this.ensureInitialized("getPrecision");
return this.gateway.getPrecision(this.symbol);
}
async queryOpenOrders(): Promise<AsterOrder[]> {
await this.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders();
}
async queryAccountSnapshot() {
await this.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot();
}
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
await this.ensureInitialized("changeMarginMode");
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
}
async forceCancelAllOrders(): Promise<boolean> {
await this.ensureInitialized("forceCancelAllOrders");
return this.gateway.forceCancelAllOrders();
}
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[BinanceExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[BinanceExchangeAdapter] ${context} failed`, error);
}
}
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
if (params.quantity === undefined) {
params.quantity = intent.quantity;
}
if (params.timeInForce === undefined && intent.timeInForce) {
params.timeInForce = intent.timeInForce;
}
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "LIMIT",
quantity: intent.quantity,
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
triggerType: intent.triggerType,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "TRAILING_STOP_MARKET",
quantity: intent.quantity,
activationPrice: intent.activationPrice,
callbackRate: intent.callbackRate,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
},
intent
);
return intent.adapter.createOrder(params);
}
+72 -40
View File
@@ -6,6 +6,25 @@ import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/ad
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
import { StandxExchangeAdapter, type StandxCredentials } from "./standx/adapter";
import { BinanceExchangeAdapter, type BinanceCredentials } from "./binance/adapter";
export const SUPPORTED_EXCHANGE_IDS = [
"aster",
"grvt",
"lighter",
"backpack",
"paradex",
"nado",
"standx",
"binance",
] as const;
export const BASIS_SUPPORTED_EXCHANGE_IDS = [
"aster",
"nado",
"standx",
"binance",
] as const;
export interface ExchangeFactoryOptions {
symbol: string;
@@ -17,60 +36,73 @@ export interface ExchangeFactoryOptions {
paradex?: ParadexCredentials;
nado?: NadoCredentials;
standx?: StandxCredentials;
binance?: BinanceCredentials;
}
export type SupportedExchangeId =
| "aster"
| "grvt"
| "lighter"
| "backpack"
| "paradex"
| "nado"
| "standx";
export type SupportedExchangeId = (typeof SUPPORTED_EXCHANGE_IDS)[number];
export type BasisSupportedExchangeId = (typeof BASIS_SUPPORTED_EXCHANGE_IDS)[number];
const EXCHANGE_DISPLAY_NAME: Record<SupportedExchangeId, string> = {
aster: "AsterDex",
grvt: "GRVT",
lighter: "Lighter",
backpack: "Backpack",
paradex: "Paradex",
nado: "Nado",
standx: "StandX",
binance: "Binance",
};
const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
aster: "aster",
grvt: "grvt",
lighter: "lighter",
backpack: "backpack",
paradex: "paradex",
nado: "nado",
standx: "standx",
binance: "binance",
bnb: "binance",
};
export function isSupportedExchangeId(value: string): value is SupportedExchangeId {
return SUPPORTED_EXCHANGE_IDS.includes(value as SupportedExchangeId);
}
export function isBasisSupportedExchangeId(value: string): value is BasisSupportedExchangeId {
return BASIS_SUPPORTED_EXCHANGE_IDS.includes(value as BasisSupportedExchangeId);
}
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
.toString()
.trim()
.toLowerCase();
if (fallback === "grvt") return "grvt";
if (fallback === "lighter") return "lighter";
if (fallback === "backpack") return "backpack";
if (fallback === "paradex") return "paradex";
if (fallback === "nado") return "nado";
if (fallback === "standx") return "standx";
return "aster";
return EXCHANGE_ALIAS_MAP[fallback] ?? "aster";
}
export function getExchangeDisplayName(id: SupportedExchangeId): string {
if (id === "grvt") return "GRVT";
if (id === "lighter") return "Lighter";
if (id === "backpack") return "Backpack";
if (id === "paradex") return "Paradex";
if (id === "nado") return "Nado";
if (id === "standx") return "StandX";
return "AsterDex";
return EXCHANGE_DISPLAY_NAME[id];
}
export function createExchangeAdapter(options: ExchangeFactoryOptions): ExchangeAdapter {
const id = resolveExchangeId(options.exchange);
if (id === "grvt") {
return new GrvtExchangeAdapter({ ...options.grvt, symbol: options.symbol });
switch (id) {
case "aster":
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
case "grvt":
return new GrvtExchangeAdapter({ ...options.grvt, symbol: options.symbol });
case "lighter":
return new LighterExchangeAdapter({ ...options.lighter, displaySymbol: options.symbol });
case "backpack":
return new BackpackExchangeAdapter({ ...options.backpack, symbol: options.symbol });
case "paradex":
return new ParadexExchangeAdapter({ ...options.paradex, symbol: options.symbol });
case "nado":
return new NadoExchangeAdapter({ ...options.nado, symbol: options.symbol });
case "standx":
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
case "binance":
return new BinanceExchangeAdapter({ ...options.binance, symbol: options.symbol });
}
if (id === "lighter") {
return new LighterExchangeAdapter({ ...options.lighter, displaySymbol: options.symbol });
}
if (id === "backpack") {
return new BackpackExchangeAdapter({ ...options.backpack, symbol: options.symbol });
}
if (id === "paradex") {
return new ParadexExchangeAdapter({ ...options.paradex, symbol: options.symbol });
}
if (id === "nado") {
return new NadoExchangeAdapter({ ...options.nado, symbol: options.symbol });
}
if (id === "standx") {
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
}
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
}
+12 -11
View File
@@ -1,5 +1,6 @@
import type { ExchangeAdapter } from "./adapter";
import type { AsterOrder } from "./types";
import { SUPPORTED_EXCHANGE_IDS, type SupportedExchangeId } from "./create-adapter";
import type {
BaseOrderIntent,
ClosePositionIntent,
@@ -15,8 +16,9 @@ import * as lighterOrders from "./lighter/order";
import * as paradexOrders from "./paradex/order";
import * as nadoOrders from "./nado/order";
import * as standxOrders from "./standx/order";
import * as binanceOrders from "./binance/order";
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado" | "standx";
type ExchangeKey = SupportedExchangeId;
interface ExchangeOrderHandlers {
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
@@ -76,17 +78,16 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
trailingStop: standxOrders.createTrailingStopOrder,
close: standxOrders.createClosePositionOrder,
},
binance: {
limit: binanceOrders.createLimitOrder,
market: binanceOrders.createMarketOrder,
stop: binanceOrders.createStopOrder,
trailingStop: binanceOrders.createTrailingStopOrder,
close: binanceOrders.createClosePositionOrder,
},
};
const knownExchanges: ExchangeKey[] = [
"aster",
"backpack",
"grvt",
"lighter",
"paradex",
"nado",
"standx",
];
const knownExchanges: ExchangeKey[] = [...SUPPORTED_EXCHANGE_IDS];
function normalizeExchangeId(value: string | undefined | null): string | undefined {
if (!value) return undefined;
@@ -98,7 +99,7 @@ function resolveExchangeKey(adapter: ExchangeAdapter): ExchangeKey {
const candidates = [fromEnv, normalizeExchangeId(adapter.id)];
for (const candidate of candidates) {
if (!candidate) continue;
if ((knownExchanges as string[]).includes(candidate)) {
if (knownExchanges.includes(candidate as ExchangeKey)) {
return candidate as ExchangeKey;
}
}
+56 -30
View File
@@ -6,6 +6,7 @@ import type { BackpackCredentials } from "./backpack/adapter";
import type { ParadexCredentials } from "./paradex/adapter";
import type { NadoCredentials } from "./nado/adapter";
import type { StandxCredentials } from "./standx/adapter";
import type { BinanceCredentials } from "./binance/adapter";
import { t } from "../i18n";
import type { Address } from "viem";
@@ -18,37 +19,38 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
const id = resolveExchangeId(options.exchangeId);
const symbol = options.symbol;
if (id === "aster") {
const credentials = resolveAsterCredentials();
return createExchangeAdapter({ exchange: id, symbol, aster: credentials });
switch (id) {
case "aster": {
const credentials = resolveAsterCredentials();
return createExchangeAdapter({ exchange: id, symbol, aster: credentials });
}
case "grvt":
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
case "lighter": {
const credentials = resolveLighterCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, lighter: credentials });
}
case "backpack": {
const credentials = resolveBackpackCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, backpack: credentials });
}
case "paradex": {
const credentials = resolveParadexCredentials();
return createExchangeAdapter({ exchange: id, symbol, paradex: credentials });
}
case "nado": {
const credentials = resolveNadoCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, nado: credentials });
}
case "standx": {
const credentials = resolveStandxCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, standx: credentials });
}
case "binance": {
const credentials = resolveBinanceCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
}
}
if (id === "lighter") {
const credentials = resolveLighterCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, lighter: credentials });
}
if (id === "backpack") {
const credentials = resolveBackpackCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, backpack: credentials });
}
if (id === "paradex") {
const credentials = resolveParadexCredentials();
return createExchangeAdapter({ exchange: id, symbol, paradex: credentials });
}
if (id === "nado") {
const credentials = resolveNadoCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, nado: credentials });
}
if (id === "standx") {
const credentials = resolveStandxCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, standx: credentials });
}
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
}
function resolveAsterCredentials(): AsterCredentials {
@@ -174,6 +176,30 @@ function resolveStandxCredentials(symbol: string): StandxCredentials {
};
}
function resolveBinanceCredentials(symbol: string): BinanceCredentials {
const apiKey = process.env.BINANCE_API_KEY;
const apiSecret = process.env.BINANCE_API_SECRET;
if (!apiKey || !apiSecret) {
throw new Error("Missing BINANCE_API_KEY or BINANCE_API_SECRET environment variable");
}
const marketTypeRaw = process.env.BINANCE_MARKET_TYPE?.trim().toLowerCase();
const marketType: BinanceCredentials["marketType"] =
marketTypeRaw === "spot" ? "spot" : marketTypeRaw === "auto" ? "auto" : "perp";
return {
apiKey,
apiSecret,
symbol: process.env.BINANCE_SYMBOL ?? symbol,
marketType,
sandbox: parseOptionalBoolean(process.env.BINANCE_SANDBOX),
spotRestUrl: process.env.BINANCE_SPOT_REST_URL ?? undefined,
futuresRestUrl: process.env.BINANCE_FUTURES_REST_URL ?? undefined,
spotWsUrl: process.env.BINANCE_SPOT_WS_URL ?? undefined,
futuresWsUrl: process.env.BINANCE_FUTURES_WS_URL ?? undefined,
};
}
function isHex32(value: string): boolean {
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
}
+2 -2
View File
@@ -313,8 +313,8 @@ const translations: Record<string, TranslationEntry> = {
"grid.direction.long": { zh: "多", en: "Long" },
"grid.direction.short": { zh: "空", en: "Short" },
"basis.onlyAster": {
zh: "期现套利策略目前仅支持 Aster / Nado / StandX 交易所。请设置 EXCHANGE=asterEXCHANGE=nadoEXCHANGE=standx 后重试。",
en: "Basis arbitrage currently supports only Aster, Nado, and StandX. Set EXCHANGE=aster, EXCHANGE=nado, or EXCHANGE=standx and retry.",
zh: "期现套利策略目前仅支持 Aster / Nado / StandX / Binance。请设置 EXCHANGE=asterEXCHANGE=nadoEXCHANGE=standx 或 EXCHANGE=binance 后重试。",
en: "Basis arbitrage currently supports Aster, Nado, StandX, and Binance. Set EXCHANGE=aster, EXCHANGE=nado, EXCHANGE=standx, or EXCHANGE=binance and retry.",
},
"basis.startFailed": {
zh: "无法启动期现套利策略: {message}",
+4 -3
View File
@@ -167,7 +167,7 @@ export class BasisArbEngine {
}
);
if (this.exchange.id === "nado" || this.exchange.id === "standx") {
if (this.exchange.id === "nado" || this.exchange.id === "standx" || this.exchange.id === "binance") {
safeSubscribe<AsterDepth>(
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
(depth) => {
@@ -409,8 +409,9 @@ export class BasisArbEngine {
if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue;
if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue;
if (name === "USDT0") {
futuresBalances.push({ asset: name, wallet, available });
const isTaggedFuturesAsset = /0$/.test(name);
if (isTaggedFuturesAsset) {
futuresBalances.push({ asset: name.replace(/0$/, ""), wallet, available });
continue;
}
const locked = Math.max(wallet - available, 0);
+2 -2
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Box, Text, useInput } from "ink";
import { basisConfig } from "../config";
import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-adapter";
import { getExchangeDisplayName, isBasisSupportedExchangeId, resolveExchangeId } from "../exchanges/create-adapter";
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
import { BasisArbEngine, type BasisArbSnapshot } from "../strategy/basis-arb-engine";
import { formatNumber } from "../utils/format";
@@ -31,7 +31,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
);
useEffect(() => {
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx") {
if (!isBasisSupportedExchangeId(exchangeId)) {
setError(new Error(t("basis.onlyAster")));
return;
}