Integrate StandX exchange support by updating configuration files, adding environment variables, and enhancing documentation. Include new API endpoints and authentication details for StandX in README and dedicated documentation files. Update CLI and adapter logic to accommodate StandX functionalities.

This commit is contained in:
discountry
2025-12-21 15:37:03 +08:00
parent 84d5e1f3d7
commit 93c6409688
23 changed files with 3295 additions and 18 deletions
+15 -1
View File
@@ -5,6 +5,7 @@ import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapt
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
import { StandxExchangeAdapter, type StandxCredentials } from "./standx/adapter";
export interface ExchangeFactoryOptions {
symbol: string;
@@ -15,9 +16,17 @@ export interface ExchangeFactoryOptions {
backpack?: BackpackCredentials;
paradex?: ParadexCredentials;
nado?: NadoCredentials;
standx?: StandxCredentials;
}
export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado";
export type SupportedExchangeId =
| "aster"
| "grvt"
| "lighter"
| "backpack"
| "paradex"
| "nado"
| "standx";
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
@@ -29,6 +38,7 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId {
if (fallback === "backpack") return "backpack";
if (fallback === "paradex") return "paradex";
if (fallback === "nado") return "nado";
if (fallback === "standx") return "standx";
return "aster";
}
@@ -38,6 +48,7 @@ export function getExchangeDisplayName(id: SupportedExchangeId): string {
if (id === "backpack") return "Backpack";
if (id === "paradex") return "Paradex";
if (id === "nado") return "Nado";
if (id === "standx") return "StandX";
return "AsterDex";
}
@@ -58,5 +69,8 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
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 });
}
+18 -2
View File
@@ -14,8 +14,9 @@ import * as grvtOrders from "./grvt/order";
import * as lighterOrders from "./lighter/order";
import * as paradexOrders from "./paradex/order";
import * as nadoOrders from "./nado/order";
import * as standxOrders from "./standx/order";
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado";
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado" | "standx";
interface ExchangeOrderHandlers {
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
@@ -68,9 +69,24 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
trailingStop: nadoOrders.createTrailingStopOrder,
close: nadoOrders.createClosePositionOrder,
},
standx: {
limit: standxOrders.createLimitOrder,
market: standxOrders.createMarketOrder,
stop: standxOrders.createStopOrder,
trailingStop: standxOrders.createTrailingStopOrder,
close: standxOrders.createClosePositionOrder,
},
};
const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex", "nado"];
const knownExchanges: ExchangeKey[] = [
"aster",
"backpack",
"grvt",
"lighter",
"paradex",
"nado",
"standx",
];
function normalizeExchangeId(value: string | undefined | null): string | undefined {
if (!value) return undefined;
+21
View File
@@ -5,6 +5,7 @@ import type { LighterCredentials } from "./lighter/adapter";
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 { t } from "../i18n";
import type { Address } from "viem";
@@ -42,6 +43,11 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
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 } });
}
@@ -153,6 +159,21 @@ function resolveNadoCredentials(symbol: string): NadoCredentials {
return credentials;
}
function resolveStandxCredentials(symbol: string): StandxCredentials {
const token = process.env.STANDX_TOKEN;
if (!token) {
throw new Error(t("env.missingStandx"));
}
return {
token,
symbol: process.env.STANDX_SYMBOL ?? symbol,
baseUrl: process.env.STANDX_BASE_URL ?? undefined,
wsUrl: process.env.STANDX_WS_URL ?? undefined,
sessionId: process.env.STANDX_SESSION_ID ?? undefined,
signingKey: process.env.STANDX_REQUEST_PRIVATE_KEY ?? undefined,
};
}
function isHex32(value: string): boolean {
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
}
+186
View File
@@ -0,0 +1,186 @@
import { setTimeout, clearTimeout } 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 { StandxGateway, type StandxGatewayOptions } from "./gateway";
export interface StandxCredentials {
token?: string;
symbol?: string;
baseUrl?: string;
wsUrl?: string;
sessionId?: string;
signingKey?: string;
logger?: StandxGatewayOptions["logger"];
}
export class StandxExchangeAdapter implements ExchangeAdapter {
readonly id = "standx";
private readonly gateway: StandxGateway;
private readonly symbol: string;
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: StandxCredentials = {}) {
const token = credentials.token ?? process.env.STANDX_TOKEN;
if (!token) {
throw new Error("Missing STANDX_TOKEN environment variable");
}
this.symbol = credentials.symbol ?? process.env.STANDX_SYMBOL ?? process.env.TRADE_SYMBOL ?? "BTC-USD";
this.gateway = new StandxGateway({
token,
symbol: this.symbol,
baseUrl: credentials.baseUrl,
wsUrl: credentials.wsUrl,
sessionId: credentials.sessionId,
signingKey: credentials.signingKey,
logger: credentials.logger,
});
}
supportsTrailingStops(): boolean {
return false;
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
}
watchFundingRate(symbol: string, cb: FundingRateListener): void {
void this.ensureInitialized("watchFundingRate");
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
}
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> {
try {
const precision = await this.gateway.getPrecision(this.symbol);
if (!precision) return null;
return {
priceTick: precision.priceTick,
qtyStep: precision.qtyStep,
priceDecimals: precision.priceDecimals,
sizeDecimals: precision.sizeDecimals,
minBaseAmount: precision.minBaseAmount,
};
} catch (error) {
console.error("[StandxExchangeAdapter] getPrecision failed", error);
return null;
}
}
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[StandxExchangeAdapter] ${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(`[StandxExchangeAdapter] ${context} failed`, error);
}
}
File diff suppressed because it is too large Load Diff
+92
View File
@@ -0,0 +1,92 @@
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,
timeInForce: intent.timeInForce ?? "IOC",
},
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",
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
closePosition: toStringBoolean(intent.closePosition ?? true),
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
throw new Error("StandX exchange does not support trailing stop orders");
}
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",
closePosition: toStringBoolean(intent.closePosition ?? true),
timeInForce: intent.timeInForce ?? "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
+90
View File
@@ -0,0 +1,90 @@
export interface StandxOrder {
id?: number;
cl_ord_id?: string;
symbol: string;
side: string;
order_type: string;
qty: string;
price?: string;
fill_qty?: string;
fill_avg_price?: string;
reduce_only?: boolean;
time_in_force?: string;
status?: string;
created_at?: string;
updated_at?: string;
}
export interface StandxPosition {
symbol: string;
qty: string;
entry_price?: string;
mark_price?: string;
upnl?: string;
leverage?: string;
liq_price?: string;
margin_mode?: string;
updated_at?: string;
}
export interface StandxBalance {
token: string;
free?: string;
locked?: string;
total?: string;
updated_at?: string;
}
export interface StandxDepthBook {
symbol: string;
bids: [string, string][];
asks: [string, string][];
}
export interface StandxPrice {
symbol: string;
last_price?: string;
mark_price?: string;
index_price?: string;
mid_price?: string;
spread_bid?: string;
spread_ask?: string;
spread?: [string, string];
time?: string;
}
export interface StandxSymbolInfo {
symbol: string;
price_tick_decimals?: number;
qty_tick_decimals?: number;
min_order_qty?: string;
max_order_qty?: string;
depth_ticks?: string;
}
export interface StandxSymbolMarket {
symbol: string;
funding_rate?: string;
next_funding_time?: string;
}
export interface StandxBalanceSnapshot {
balance?: string;
upnl?: string;
cross_available?: string;
cross_balance?: string;
isolated_balance?: string;
cross_upnl?: string;
isolated_upnl?: string;
locked?: string;
}
export interface StandxKlineHistory {
s?: string;
t?: number[];
o?: number[];
h?: number[];
l?: number[];
c?: number[];
v?: number[];
}