mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +00:00
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.
This commit is contained in:
+12
-1
@@ -2,12 +2,23 @@
|
|||||||
LANG=zh
|
LANG=zh
|
||||||
|
|
||||||
# Exchange selection
|
# Exchange selection
|
||||||
EXCHANGE=aster # Pick aster (default) or standx/grvt/lighter/backpack/paradex/nado
|
EXCHANGE=aster # Pick aster (default) or binance/standx/grvt/lighter/backpack/paradex/nado
|
||||||
|
|
||||||
# Aster API credentials
|
# Aster API credentials
|
||||||
ASTER_API_KEY=
|
ASTER_API_KEY=
|
||||||
ASTER_API_SECRET=
|
ASTER_API_SECRET=
|
||||||
|
|
||||||
|
# Binance API credentials (set when EXCHANGE=binance)
|
||||||
|
BINANCE_API_KEY=
|
||||||
|
BINANCE_API_SECRET=
|
||||||
|
BINANCE_SYMBOL=BTCUSDT # Trading symbol. Use BTCUSDT_PERP to force perpetual when ambiguous.
|
||||||
|
BINANCE_MARKET_TYPE=perp # perp | spot | auto
|
||||||
|
# BINANCE_SANDBOX=false
|
||||||
|
# BINANCE_SPOT_REST_URL=https://api.binance.com
|
||||||
|
# BINANCE_FUTURES_REST_URL=https://fapi.binance.com
|
||||||
|
# BINANCE_SPOT_WS_URL=wss://stream.binance.com:9443/ws
|
||||||
|
# BINANCE_FUTURES_WS_URL=wss://fstream.binance.com/ws
|
||||||
|
|
||||||
# StandX authentication (set when EXCHANGE=standx)
|
# StandX authentication (set when EXCHANGE=standx)
|
||||||
STANDX_TOKEN=
|
STANDX_TOKEN=
|
||||||
STANDX_SYMBOL=BTC-USD
|
STANDX_SYMBOL=BTC-USD
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
|
|||||||
|
|
||||||
| 变量 | 说明 |
|
| 变量 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `EXCHANGE` | 选择交易所(`aster`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado`) |
|
| `EXCHANGE` | 选择交易所(`aster`/`binance`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado`) |
|
||||||
| `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT`) |
|
| `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT`) |
|
||||||
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
|
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
|
||||||
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
|
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
|
|||||||
|
|
||||||
| Variable | Purpose |
|
| Variable | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `EXCHANGE` | Choose the venue (`aster` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) |
|
| `EXCHANGE` | Choose the venue (`aster` / `binance` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) |
|
||||||
| `TRADE_SYMBOL` | Contract symbol (defaults to `BTCUSDT`) |
|
| `TRADE_SYMBOL` | Contract symbol (defaults to `BTCUSDT`) |
|
||||||
| `TRADE_AMOUNT` | Order size in base asset units |
|
| `TRADE_AMOUNT` | Order size in base asset units |
|
||||||
| `LOSS_LIMIT` | Max per-trade loss in USDT before forced close |
|
| `LOSS_LIMIT` | Max per-trade loss in USDT before forced close |
|
||||||
|
|||||||
+4
-3
@@ -4,7 +4,7 @@ export interface CliOptions {
|
|||||||
strategy?: StrategyId;
|
strategy?: StrategyId;
|
||||||
silent: boolean;
|
silent: boolean;
|
||||||
help: boolean;
|
help: boolean;
|
||||||
exchange?: "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado" | "standx";
|
exchange?: "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado" | "standx" | "binance";
|
||||||
}
|
}
|
||||||
|
|
||||||
const STRATEGY_VALUES = new Set<StrategyId>([
|
const STRATEGY_VALUES = new Set<StrategyId>([
|
||||||
@@ -89,7 +89,8 @@ function assignExchange(options: CliOptions, raw: string): void {
|
|||||||
normalized === "backpack" ||
|
normalized === "backpack" ||
|
||||||
normalized === "paradex" ||
|
normalized === "paradex" ||
|
||||||
normalized === "nado" ||
|
normalized === "nado" ||
|
||||||
normalized === "standx"
|
normalized === "standx" ||
|
||||||
|
normalized === "binance"
|
||||||
) {
|
) {
|
||||||
options.exchange = normalized as CliOptions["exchange"];
|
options.exchange = normalized as CliOptions["exchange"];
|
||||||
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
|
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
|
||||||
@@ -99,7 +100,7 @@ function assignExchange(options: CliOptions, raw: string): void {
|
|||||||
|
|
||||||
export function printCliHelp(): void {
|
export function printCliHelp(): void {
|
||||||
// eslint-disable-next-line no-console
|
// 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 <aster|grvt|lighter|backpack|paradex|nado|standx|binance>] [--silent]\n\n` +
|
||||||
`Options:\n` +
|
`Options:\n` +
|
||||||
` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` +
|
` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` +
|
||||||
` Aliases: offset, offset-maker for the offset maker engine.\n` +
|
` Aliases: offset, offset-maker for the offset maker engine.\n` +
|
||||||
|
|||||||
@@ -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.");
|
throw new Error("Basis arbitrage strategy is disabled. Set ENABLE_BASIS_STRATEGY=true to enable it.");
|
||||||
}
|
}
|
||||||
const exchangeId = resolveExchangeId();
|
const exchangeId = resolveExchangeId();
|
||||||
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx") {
|
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx" && exchangeId !== "binance") {
|
||||||
throw new Error("Basis arbitrage strategy currently only supports the Aster, Nado, and StandX exchanges");
|
throw new Error("Basis arbitrage strategy currently only supports the Aster, Nado, StandX, and Binance exchanges");
|
||||||
}
|
}
|
||||||
const adapter = createAdapterOrThrow(basisConfig.futuresSymbol);
|
const adapter = createAdapterOrThrow(basisConfig.futuresSymbol);
|
||||||
const engine = new BasisArbEngine(basisConfig, adapter);
|
const engine = new BasisArbEngine(basisConfig, adapter);
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
|
|||||||
paradex: { envKeys: ["PARADEX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC/USDC" },
|
paradex: { envKeys: ["PARADEX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC/USDC" },
|
||||||
nado: { envKeys: ["NADO_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-PERP" },
|
nado: { envKeys: ["NADO_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-PERP" },
|
||||||
standx: { envKeys: ["STANDX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD" },
|
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 {
|
export function resolveSymbolFromEnv(explicitExchangeId?: SupportedExchangeId | string | null): string {
|
||||||
@@ -312,6 +313,7 @@ export const basisConfig: BasisArbConfig = {
|
|||||||
const exchange = (process.env.EXCHANGE ?? "").trim().toLowerCase();
|
const exchange = (process.env.EXCHANGE ?? "").trim().toLowerCase();
|
||||||
if (exchange === "nado") return "BTC-PERP";
|
if (exchange === "nado") return "BTC-PERP";
|
||||||
if (exchange === "standx") return "BTC-USD";
|
if (exchange === "standx") return "BTC-USD";
|
||||||
|
if (exchange === "binance") return "BTCUSDT_PERP";
|
||||||
return "ASTERUSDT";
|
return "ASTERUSDT";
|
||||||
})()
|
})()
|
||||||
),
|
),
|
||||||
@@ -321,6 +323,7 @@ export const basisConfig: BasisArbConfig = {
|
|||||||
const exchange = (process.env.EXCHANGE ?? "").trim().toLowerCase();
|
const exchange = (process.env.EXCHANGE ?? "").trim().toLowerCase();
|
||||||
if (exchange === "nado") return "KBTC";
|
if (exchange === "nado") return "KBTC";
|
||||||
if (exchange === "standx") return "BTC-USD";
|
if (exchange === "standx") return "BTC-USD";
|
||||||
|
if (exchange === "binance") return "BTCUSDT";
|
||||||
return "ASTERUSDT";
|
return "ASTERUSDT";
|
||||||
})()
|
})()
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/ad
|
|||||||
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
|
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
|
||||||
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
|
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
|
||||||
import { StandxExchangeAdapter, type StandxCredentials } from "./standx/adapter";
|
import { StandxExchangeAdapter, type StandxCredentials } from "./standx/adapter";
|
||||||
|
import { BinanceExchangeAdapter, type BinanceCredentials } from "./binance/adapter";
|
||||||
|
|
||||||
export interface ExchangeFactoryOptions {
|
export interface ExchangeFactoryOptions {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
@@ -17,6 +18,7 @@ export interface ExchangeFactoryOptions {
|
|||||||
paradex?: ParadexCredentials;
|
paradex?: ParadexCredentials;
|
||||||
nado?: NadoCredentials;
|
nado?: NadoCredentials;
|
||||||
standx?: StandxCredentials;
|
standx?: StandxCredentials;
|
||||||
|
binance?: BinanceCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SupportedExchangeId =
|
export type SupportedExchangeId =
|
||||||
@@ -26,7 +28,8 @@ export type SupportedExchangeId =
|
|||||||
| "backpack"
|
| "backpack"
|
||||||
| "paradex"
|
| "paradex"
|
||||||
| "nado"
|
| "nado"
|
||||||
| "standx";
|
| "standx"
|
||||||
|
| "binance";
|
||||||
|
|
||||||
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
||||||
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
|
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
|
||||||
@@ -39,6 +42,7 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId {
|
|||||||
if (fallback === "paradex") return "paradex";
|
if (fallback === "paradex") return "paradex";
|
||||||
if (fallback === "nado") return "nado";
|
if (fallback === "nado") return "nado";
|
||||||
if (fallback === "standx") return "standx";
|
if (fallback === "standx") return "standx";
|
||||||
|
if (fallback === "binance" || fallback === "bnb") return "binance";
|
||||||
return "aster";
|
return "aster";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +53,7 @@ export function getExchangeDisplayName(id: SupportedExchangeId): string {
|
|||||||
if (id === "paradex") return "Paradex";
|
if (id === "paradex") return "Paradex";
|
||||||
if (id === "nado") return "Nado";
|
if (id === "nado") return "Nado";
|
||||||
if (id === "standx") return "StandX";
|
if (id === "standx") return "StandX";
|
||||||
|
if (id === "binance") return "Binance";
|
||||||
return "AsterDex";
|
return "AsterDex";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,5 +77,8 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
|
|||||||
if (id === "standx") {
|
if (id === "standx") {
|
||||||
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
|
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
|
||||||
}
|
}
|
||||||
|
if (id === "binance") {
|
||||||
|
return new BinanceExchangeAdapter({ ...options.binance, symbol: options.symbol });
|
||||||
|
}
|
||||||
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
|
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import * as lighterOrders from "./lighter/order";
|
|||||||
import * as paradexOrders from "./paradex/order";
|
import * as paradexOrders from "./paradex/order";
|
||||||
import * as nadoOrders from "./nado/order";
|
import * as nadoOrders from "./nado/order";
|
||||||
import * as standxOrders from "./standx/order";
|
import * as standxOrders from "./standx/order";
|
||||||
|
import * as binanceOrders from "./binance/order";
|
||||||
|
|
||||||
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado" | "standx";
|
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado" | "standx" | "binance";
|
||||||
|
|
||||||
interface ExchangeOrderHandlers {
|
interface ExchangeOrderHandlers {
|
||||||
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
|
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
|
||||||
@@ -76,6 +77,13 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
|||||||
trailingStop: standxOrders.createTrailingStopOrder,
|
trailingStop: standxOrders.createTrailingStopOrder,
|
||||||
close: standxOrders.createClosePositionOrder,
|
close: standxOrders.createClosePositionOrder,
|
||||||
},
|
},
|
||||||
|
binance: {
|
||||||
|
limit: binanceOrders.createLimitOrder,
|
||||||
|
market: binanceOrders.createMarketOrder,
|
||||||
|
stop: binanceOrders.createStopOrder,
|
||||||
|
trailingStop: binanceOrders.createTrailingStopOrder,
|
||||||
|
close: binanceOrders.createClosePositionOrder,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const knownExchanges: ExchangeKey[] = [
|
const knownExchanges: ExchangeKey[] = [
|
||||||
@@ -86,6 +94,7 @@ const knownExchanges: ExchangeKey[] = [
|
|||||||
"paradex",
|
"paradex",
|
||||||
"nado",
|
"nado",
|
||||||
"standx",
|
"standx",
|
||||||
|
"binance",
|
||||||
];
|
];
|
||||||
|
|
||||||
function normalizeExchangeId(value: string | undefined | null): string | undefined {
|
function normalizeExchangeId(value: string | undefined | null): string | undefined {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { BackpackCredentials } from "./backpack/adapter";
|
|||||||
import type { ParadexCredentials } from "./paradex/adapter";
|
import type { ParadexCredentials } from "./paradex/adapter";
|
||||||
import type { NadoCredentials } from "./nado/adapter";
|
import type { NadoCredentials } from "./nado/adapter";
|
||||||
import type { StandxCredentials } from "./standx/adapter";
|
import type { StandxCredentials } from "./standx/adapter";
|
||||||
|
import type { BinanceCredentials } from "./binance/adapter";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import type { Address } from "viem";
|
import type { Address } from "viem";
|
||||||
|
|
||||||
@@ -48,6 +49,11 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
|
|||||||
return createExchangeAdapter({ exchange: id, symbol, standx: credentials });
|
return createExchangeAdapter({ exchange: id, symbol, standx: credentials });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (id === "binance") {
|
||||||
|
const credentials = resolveBinanceCredentials(symbol);
|
||||||
|
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
|
||||||
|
}
|
||||||
|
|
||||||
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
|
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +180,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 {
|
function isHex32(value: string): boolean {
|
||||||
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
|
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -313,8 +313,8 @@ const translations: Record<string, TranslationEntry> = {
|
|||||||
"grid.direction.long": { zh: "多", en: "Long" },
|
"grid.direction.long": { zh: "多", en: "Long" },
|
||||||
"grid.direction.short": { zh: "空", en: "Short" },
|
"grid.direction.short": { zh: "空", en: "Short" },
|
||||||
"basis.onlyAster": {
|
"basis.onlyAster": {
|
||||||
zh: "期现套利策略目前仅支持 Aster / Nado / StandX 交易所。请设置 EXCHANGE=aster 或 EXCHANGE=nado 或 EXCHANGE=standx 后重试。",
|
zh: "期现套利策略目前仅支持 Aster / Nado / StandX / Binance。请设置 EXCHANGE=aster、EXCHANGE=nado、EXCHANGE=standx 或 EXCHANGE=binance 后重试。",
|
||||||
en: "Basis arbitrage currently supports only Aster, Nado, and StandX. Set EXCHANGE=aster, EXCHANGE=nado, or EXCHANGE=standx and retry.",
|
en: "Basis arbitrage currently supports Aster, Nado, StandX, and Binance. Set EXCHANGE=aster, EXCHANGE=nado, EXCHANGE=standx, or EXCHANGE=binance and retry.",
|
||||||
},
|
},
|
||||||
"basis.startFailed": {
|
"basis.startFailed": {
|
||||||
zh: "无法启动期现套利策略: {message}",
|
zh: "无法启动期现套利策略: {message}",
|
||||||
|
|||||||
@@ -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>(
|
safeSubscribe<AsterDepth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
@@ -409,8 +409,9 @@ export class BasisArbEngine {
|
|||||||
if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue;
|
if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue;
|
||||||
if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue;
|
if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue;
|
||||||
|
|
||||||
if (name === "USDT0") {
|
const isTaggedFuturesAsset = /0$/.test(name);
|
||||||
futuresBalances.push({ asset: name, wallet, available });
|
if (isTaggedFuturesAsset) {
|
||||||
|
futuresBalances.push({ asset: name.replace(/0$/, ""), wallet, available });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const locked = Math.max(wallet - available, 0);
|
const locked = Math.max(wallet - available, 0);
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx") {
|
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx" && exchangeId !== "binance") {
|
||||||
setError(new Error(t("basis.onlyAster")));
|
setError(new Error(t("basis.onlyAster")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ describe("BasisArbEngine", () => {
|
|||||||
time: 2_000,
|
time: 2_000,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
const futuresClient = {
|
||||||
|
getPremiumIndex: vi.fn().mockResolvedValue({
|
||||||
|
fundingRate: "0.0001",
|
||||||
|
nextFundingTime: 3_600_000,
|
||||||
|
time: 2_000,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
const engine = new BasisArbEngine(
|
const engine = new BasisArbEngine(
|
||||||
{
|
{
|
||||||
@@ -79,10 +86,12 @@ describe("BasisArbEngine", () => {
|
|||||||
refreshIntervalMs: 1_000,
|
refreshIntervalMs: 1_000,
|
||||||
maxLogEntries: 10,
|
maxLogEntries: 10,
|
||||||
takerFeeRate: 0.0004,
|
takerFeeRate: 0.0004,
|
||||||
|
arbAmount: 1,
|
||||||
},
|
},
|
||||||
adapter,
|
adapter,
|
||||||
{
|
{
|
||||||
spotClient,
|
spotClient,
|
||||||
|
futuresClient,
|
||||||
now: () => 1_000,
|
now: () => 1_000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -111,7 +120,7 @@ describe("BasisArbEngine", () => {
|
|||||||
const expectedNet = 1.04 * (1 - effectiveFee) - 1.05 * (1 + effectiveFee);
|
const expectedNet = 1.04 * (1 - effectiveFee) - 1.05 * (1 + effectiveFee);
|
||||||
expect(snapshot.netSpread).toBeCloseTo(expectedNet, 6);
|
expect(snapshot.netSpread).toBeCloseTo(expectedNet, 6);
|
||||||
expect(snapshot.netSpreadBps).toBeCloseTo((expectedNet / 1.05) * 10_000, 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);
|
expect(snapshot.opportunity).toBe(expectedNet >= 0);
|
||||||
|
|
||||||
engine.stop();
|
engine.stop();
|
||||||
|
|||||||
@@ -49,4 +49,11 @@ describe("resolveSymbolFromEnv", () => {
|
|||||||
|
|
||||||
expect(resolveSymbolFromEnv("standx")).toBe("ETH-USD");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
|
|||||||
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
|
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
|
||||||
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
|
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
|
||||||
import { StandxExchangeAdapter } from "../src/exchanges/standx/adapter";
|
import { StandxExchangeAdapter } from "../src/exchanges/standx/adapter";
|
||||||
|
import { BinanceExchangeAdapter } from "../src/exchanges/binance/adapter";
|
||||||
|
|
||||||
const ORIGINAL_ENV = { ...process.env };
|
const ORIGINAL_ENV = { ...process.env };
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ describe("exchange factory", () => {
|
|||||||
expect(resolveExchangeId("BACKPACK")).toBe("backpack");
|
expect(resolveExchangeId("BACKPACK")).toBe("backpack");
|
||||||
expect(resolveExchangeId("PaRaDeX")).toBe("paradex");
|
expect(resolveExchangeId("PaRaDeX")).toBe("paradex");
|
||||||
expect(resolveExchangeId("StandX")).toBe("standx");
|
expect(resolveExchangeId("StandX")).toBe("standx");
|
||||||
|
expect(resolveExchangeId("BiNaNcE")).toBe("binance");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates grvt adapter when EXCHANGE=grvt", () => {
|
it("creates grvt adapter when EXCHANGE=grvt", () => {
|
||||||
@@ -79,4 +81,15 @@ describe("exchange factory", () => {
|
|||||||
expect(adapter).toBeInstanceOf(StandxExchangeAdapter);
|
expect(adapter).toBeInstanceOf(StandxExchangeAdapter);
|
||||||
expect(adapter.id).toBe("standx");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user