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.
This commit is contained in:
discountry
2026-02-27 11:32:20 +08:00
parent 98c58727c7
commit 27abeb9fa4
8 changed files with 376 additions and 109 deletions
+6 -12
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" | "binance";
exchange?: SupportedExchangeId;
}
const STRATEGY_VALUES = new Set<StrategyId>([
@@ -82,16 +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" ||
normalized === "binance"
) {
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";
@@ -99,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|binance>] [--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` +
+2 -2
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,7 +141,7 @@ 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" && exchangeId !== "binance") {
if (!isBasisSupportedExchangeId(exchangeId)) {
throw new Error("Basis arbitrage strategy currently only supports the Aster, Nado, StandX, and Binance exchanges");
}
const adapter = createAdapterOrThrow(basisConfig.futuresSymbol);
+70 -46
View File
@@ -8,6 +8,24 @@ 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;
exchange?: string;
@@ -21,64 +39,70 @@ export interface ExchangeFactoryOptions {
binance?: BinanceCredentials;
}
export type SupportedExchangeId =
| "aster"
| "grvt"
| "lighter"
| "backpack"
| "paradex"
| "nado"
| "standx"
| "binance";
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";
if (fallback === "binance" || fallback === "bnb") return "binance";
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";
if (id === "binance") return "Binance";
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 });
}
if (id === "binance") {
return new BinanceExchangeAdapter({ ...options.binance, symbol: options.symbol });
}
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
}
+4 -12
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,
@@ -17,7 +18,7 @@ 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" | "binance";
type ExchangeKey = SupportedExchangeId;
interface ExchangeOrderHandlers {
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
@@ -86,16 +87,7 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
},
};
const knownExchanges: ExchangeKey[] = [
"aster",
"backpack",
"grvt",
"lighter",
"paradex",
"nado",
"standx",
"binance",
];
const knownExchanges: ExchangeKey[] = [...SUPPORTED_EXCHANGE_IDS];
function normalizeExchangeId(value: string | undefined | null): string | undefined {
if (!value) return undefined;
@@ -107,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;
}
}
+31 -35
View File
@@ -19,42 +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 });
}
if (id === "binance") {
const credentials = resolveBinanceCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
}
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
}
function resolveAsterCredentials(): AsterCredentials {
+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" && exchangeId !== "binance") {
if (!isBasisSupportedExchangeId(exchangeId)) {
setError(new Error(t("basis.onlyAster")));
return;
}