feat(exchanges): add Ondo Perps exchange adapter

This commit is contained in:
discountry
2026-07-11 15:10:40 +08:00
parent 6b5f2542a4
commit 1f2c2150e0
20 changed files with 2181 additions and 4 deletions
+2
View File
@@ -88,6 +88,8 @@ function assignExchange(options: CliOptions, raw: string): void {
options.exchange = normalized as CliOptions["exchange"];
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
options.exchange = "grvt";
} else if (normalized === "ondo" || normalized === "ondoperp") {
options.exchange = "ondoperps";
}
}
+9
View File
@@ -117,6 +117,15 @@ const STATIC_CAPABILITIES: Record<
changeMarginMode: true,
forceCancelAllOrders: true,
},
ondoperps: {
trailingStops: false,
fundingRate: true,
precision: true,
queryOpenOrders: true,
queryAccountSnapshot: true,
changeMarginMode: false,
forceCancelAllOrders: true,
},
};
export interface CommandExecutorDependencies {
+1
View File
@@ -470,6 +470,7 @@ function normalizeExchange(value: string | undefined): SupportedExchangeId | und
const normalized = value.trim().toLowerCase();
if (normalized === "gravity" || normalized === "grav" || normalized === "grv") return "grvt";
if (normalized === "bnb") return "binance";
if (normalized === "ondo" || normalized === "ondoperp") return "ondoperps";
if (isSupportedExchangeId(normalized)) return normalized;
throw new CommandParseError(`Unsupported exchange '${value}'`);
}
+1
View File
@@ -98,6 +98,7 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
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" },
ondoperps: { envKeys: ["ONDOPERPS_SYMBOL", "ONDOPERP_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD.P" },
};
export function resolveSymbolFromEnv(explicitExchangeId?: SupportedExchangeId | string | null): string {
+13
View File
@@ -7,6 +7,7 @@ import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapt
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
import { StandxExchangeAdapter, type StandxCredentials } from "./standx/adapter";
import { BinanceExchangeAdapter, type BinanceCredentials } from "./binance/adapter";
import { OndoperpsExchangeAdapter, type OndoperpsCredentials } from "./ondoperps/adapter";
export const SUPPORTED_EXCHANGE_IDS = [
"aster",
@@ -17,6 +18,7 @@ export const SUPPORTED_EXCHANGE_IDS = [
"nado",
"standx",
"binance",
"ondoperps",
] as const;
export const BASIS_SUPPORTED_EXCHANGE_IDS = [
@@ -37,6 +39,8 @@ export interface ExchangeFactoryOptions {
nado?: NadoCredentials;
standx?: StandxCredentials;
binance?: BinanceCredentials;
ondoperps?: OndoperpsCredentials;
ondoperp?: OndoperpsCredentials;
}
export type SupportedExchangeId = (typeof SUPPORTED_EXCHANGE_IDS)[number];
@@ -51,6 +55,7 @@ const EXCHANGE_DISPLAY_NAME: Record<SupportedExchangeId, string> = {
nado: "Nado",
standx: "StandX",
binance: "Binance",
ondoperps: "Ondo Perps",
};
const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
@@ -63,6 +68,9 @@ const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
standx: "standx",
binance: "binance",
bnb: "binance",
ondoperps: "ondoperps",
ondoperp: "ondoperps",
ondo: "ondoperps",
};
export function isSupportedExchangeId(value: string): value is SupportedExchangeId {
@@ -104,5 +112,10 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
case "binance":
return new BinanceExchangeAdapter({ ...options.binance, symbol: options.symbol });
case "ondoperps":
return new OndoperpsExchangeAdapter({
...(options.ondoperps ?? options.ondoperp),
symbol: options.symbol,
});
}
}
+150
View File
@@ -0,0 +1,150 @@
import type {
AccountListener,
ConnectionEventListener,
DepthListener,
ExchangeAdapter,
ExchangePrecision,
FundingRateListener,
KlineListener,
OrderListener,
TickerListener,
} from "../adapter";
import { createInitManager, createSafeInvoke } from "../adapter-utils";
import type { CreateOrderParams, Order } from "../types";
import { OndoperpsGateway, type OndoperpsGatewayOptions } from "./gateway";
export interface OndoperpsCredentials {
apiKeyId?: string;
apiSecret?: string;
symbol?: string;
baseUrl?: string;
wsUrl?: string;
builderCode?: string;
builderFeeRateBps?: number;
logger?: OndoperpsGatewayOptions["logger"];
}
export class OndoperpsExchangeAdapter implements ExchangeAdapter {
readonly id = "ondoperps";
private readonly gateway: OndoperpsGateway;
private readonly symbol: string;
private readonly safeInvoke = createSafeInvoke("OndoperpsExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
constructor(credentials: OndoperpsCredentials = {}) {
const apiKeyId = credentials.apiKeyId ?? process.env.ONDOPERPS_API_KEY_ID ?? process.env.ONDOPERP_API_KEY_ID ?? process.env.ONDO_KEY_ID;
const apiSecret = credentials.apiSecret ?? process.env.ONDOPERPS_API_SECRET ?? process.env.ONDOPERP_API_SECRET ?? process.env.ONDO_API_SECRET;
if (!apiKeyId || !apiSecret) {
throw new Error("Missing ONDOPERPS_API_KEY_ID or ONDOPERPS_API_SECRET environment variable");
}
this.symbol = credentials.symbol ?? process.env.ONDOPERPS_SYMBOL ?? process.env.ONDOPERP_SYMBOL ?? process.env.TRADE_SYMBOL ?? "BTC-USD.P";
const sandbox = parseBoolean(process.env.ONDOPERPS_SANDBOX ?? process.env.ONDOPERP_SANDBOX);
this.gateway = new OndoperpsGateway({
apiKeyId,
apiSecret,
symbol: this.symbol,
baseUrl: credentials.baseUrl ?? process.env.ONDOPERPS_BASE_URL ?? process.env.ONDOPERP_BASE_URL ?? (sandbox ? "https://api.ondoperps-sandbox.xyz" : undefined),
wsUrl: credentials.wsUrl ?? process.env.ONDOPERPS_WS_URL ?? process.env.ONDOPERP_WS_URL ?? (sandbox ? "wss://api.ondoperps-sandbox.xyz/ws" : undefined),
builderCode: credentials.builderCode ?? process.env.ONDOPERPS_BUILDER_CODE ?? process.env.ONDOPERP_BUILDER_CODE,
builderFeeRateBps: credentials.builderFeeRateBps ?? parseNumber(
process.env.ONDOPERPS_BUILDER_FEE_RATE_BPS ?? process.env.ONDOPERP_BUILDER_FEE_RATE_BPS,
),
logger: credentials.logger,
});
this.init = createInitManager("OndoperpsExchangeAdapter", () => this.gateway.ensureInitialized());
}
supportsTrailingStops(): boolean {
return false;
}
watchAccount(cb: AccountListener): void {
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
}
watchFundingRate(symbol: string, cb: FundingRateListener): void {
void this.init.ensureInitialized("watchFundingRate");
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
}
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
await this.init.ensureInitialized("getPrecision");
return this.gateway.getPrecision(this.symbol);
}
onConnectionEvent(listener: ConnectionEventListener): void {
this.gateway.onConnectionEvent(listener);
}
offConnectionEvent(listener: ConnectionEventListener): void {
this.gateway.offConnectionEvent(listener);
}
async queryOpenOrders(): Promise<Order[]> {
await this.init.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders();
}
async queryAccountSnapshot() {
await this.init.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot();
}
async forceCancelAllOrders(): Promise<boolean> {
await this.init.ensureInitialized("forceCancelAllOrders");
return this.gateway.forceCancelAllOrders();
}
}
function parseBoolean(value: string | undefined): boolean {
if (!value) return false;
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
}
function parseNumber(value: string | undefined): number | undefined {
if (!value) return undefined;
const number = Number(value);
return Number.isFinite(number) ? number : undefined;
}
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
import { createOrderHandlers } from "../order-handlers";
const handlers = createOrderHandlers({
exchangeName: "Ondo Perps",
defaultLimitTimeInForce: "GTX",
defaultMarketTimeInForce: "IOC",
defaultCloseTimeInForce: "IOC",
defaultStopTimeInForce: "GTC",
defaultStopTriggerType: "STOP_LOSS",
supportsTrailingStop: false,
supportsTriggerType: true,
stopDefaultReduceOnly: true,
stopDefaultClosePosition: true,
closeDefaultClosePosition: true,
});
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+173
View File
@@ -0,0 +1,173 @@
export interface OndoperpsApiResponse<T> {
success: boolean;
result?: T;
error?: string;
error_code?: string;
}
export interface OndoperpsOrder {
orderId: string;
clientOrderId?: string;
parentOrderId?: string;
side: "buy" | "sell";
price?: string;
size: string;
market: string;
filledSize?: string;
lastFillSize?: string;
filledCost?: string;
realizedPnl?: string;
fee?: string;
feeRebate?: string;
status: "open" | "fullyfilled" | "canceled" | "pending" | "untriggered" | string;
createdAt: string;
filledAt?: string;
canceledAt?: string;
cancelReason?: string;
type: "limit" | "market" | "stopMarket" | "takeProfitMarket" | string;
timeInForce?: "GTC" | "IOC" | "FOK";
reduceOnly?: boolean;
closePosition?: boolean;
stopOrderType?: "stopLoss" | "takeProfit";
triggerPrice?: string;
}
export interface OndoperpsPosition {
market: string;
direction: "long" | "short" | "neutral";
netQuantity: string;
averageEntryPrice: string;
usedMargin: string;
unrealizedPnl: string;
markPrice: string;
liquidationPrice: string;
bankruptcyPrice: string;
maintenanceMargin: string;
notionalValue: string;
leverage: string;
netFundingSinceNeutral: string;
returnOnEquity: string;
stopLossTriggerPrice?: string;
takeProfitTriggerPrice?: string;
}
export interface OndoperpsBalance {
walletBalance: string;
realizedPnl: string;
unrealizedPnl: string;
marginBalance: string;
usedMargin: string;
availableMargin: string;
withdrawableMargin: string;
maintenanceMarginRequirement: string;
totalMaintenanceMargin: string;
marginRatio: string;
leverage: string;
underLiquidation: boolean;
totalFundingPayments: string;
totalTradingFees: string;
totalPnL: string;
netInvested?: string;
}
export interface OndoperpsStopOrders {
market: string;
positionDirection: "long" | "short" | "neutral";
stopLoss?: string | null;
takeProfit?: string | null;
}
export type OndoperpsBookLevel = [string, string];
export interface OndoperpsBookSnapshot {
market: string;
time: string;
bids: OndoperpsBookLevel[];
asks: OndoperpsBookLevel[];
depthLevels?: string;
}
export interface OndoperpsCandle {
startTime: string;
open: string;
high: string;
low: string;
close: string;
volume: string;
}
export interface OndoperpsWsKline {
m: string;
t: number;
s: number;
e: number;
o: number;
h: number;
l: number;
c: number;
v: number;
x?: boolean;
}
export interface OndoperpsContract {
market: string;
displayName?: string;
productType: string;
contractType: string;
baseCurrency: string;
quoteCurrency: string;
disabled: boolean;
lastPrice?: string;
baseVolume?: string;
quoteVolume?: string;
usdVolume?: string;
bid?: string;
ask?: string;
high?: string;
low?: string;
openInterest?: string;
openInterestUsd?: string;
indexPrice?: string;
fundingRate?: string;
nextFundingRate?: string;
nextFundingRateTimestamp?: string;
makerFee?: string;
takerFee?: string;
priceChangePercent?: string;
isClosed?: boolean;
}
export interface OndoperpsMarkPrice {
market: string;
price: string;
markPrice: string;
oraclePrice?: string;
lastExternalPrice?: string;
lastUpdatedTime?: string;
}
export interface OndoperpsFundingRate {
market: string;
rate: string;
intervalEnds?: string;
}
export interface OndoperpsTradingPair {
market: string;
baseIncrement: string;
quoteIncrement: string;
}
export interface OndoperpsMarketsResult {
perps?: {
tradingPairs?: OndoperpsTradingPair[];
};
}
export interface OndoperpsWsMessage {
type: "pong" | "loggedIn" | "subscribed" | "unsubscribed" | "update" | "error" | string;
channel?: string;
code?: number;
msg?: string;
data?: unknown;
}
+10 -1
View File
@@ -17,6 +17,7 @@ import * as paradexOrders from "./paradex/order";
import * as nadoOrders from "./nado/order";
import * as standxOrders from "./standx/order";
import * as binanceOrders from "./binance/order";
import * as ondoperpsOrders from "./ondoperps/order";
type ExchangeKey = SupportedExchangeId;
@@ -85,13 +86,21 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
trailingStop: binanceOrders.createTrailingStopOrder,
close: binanceOrders.createClosePositionOrder,
},
ondoperps: {
limit: ondoperpsOrders.createLimitOrder,
market: ondoperpsOrders.createMarketOrder,
stop: ondoperpsOrders.createStopOrder,
trailingStop: ondoperpsOrders.createTrailingStopOrder,
close: ondoperpsOrders.createClosePositionOrder,
},
};
const knownExchanges: ExchangeKey[] = [...SUPPORTED_EXCHANGE_IDS];
function normalizeExchangeId(value: string | undefined | null): string | undefined {
if (!value) return undefined;
return value.trim().toLowerCase();
const normalized = value.trim().toLowerCase();
return normalized === "ondoperp" ? "ondoperps" : normalized;
}
function resolveExchangeKey(adapter: ExchangeAdapter): ExchangeKey {
+25
View File
@@ -7,6 +7,7 @@ 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 type { OndoperpsCredentials } from "./ondoperps/adapter";
import { t } from "../i18n";
import type { Address } from "viem";
@@ -50,6 +51,10 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
const credentials = resolveBinanceCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
}
case "ondoperps": {
const credentials = resolveOndoperpsCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, ondoperps: credentials });
}
}
}
@@ -200,6 +205,26 @@ function resolveBinanceCredentials(symbol: string): BinanceCredentials {
};
}
function resolveOndoperpsCredentials(symbol: string): OndoperpsCredentials {
const apiKeyId = process.env.ONDOPERPS_API_KEY_ID ?? process.env.ONDOPERP_API_KEY_ID ?? process.env.ONDO_KEY_ID;
const apiSecret = process.env.ONDOPERPS_API_SECRET ?? process.env.ONDOPERP_API_SECRET ?? process.env.ONDO_API_SECRET;
if (!apiKeyId || !apiSecret) {
throw new Error(t("env.missingOndoperps"));
}
const sandbox = parseOptionalBoolean(process.env.ONDOPERPS_SANDBOX ?? process.env.ONDOPERP_SANDBOX) === true;
return {
apiKeyId,
apiSecret,
symbol: process.env.ONDOPERPS_SYMBOL ?? process.env.ONDOPERP_SYMBOL ?? symbol,
baseUrl: process.env.ONDOPERPS_BASE_URL ?? process.env.ONDOPERP_BASE_URL ?? (sandbox ? "https://api.ondoperps-sandbox.xyz" : undefined),
wsUrl: process.env.ONDOPERPS_WS_URL ?? process.env.ONDOPERP_WS_URL ?? (sandbox ? "wss://api.ondoperps-sandbox.xyz/ws" : undefined),
builderCode: process.env.ONDOPERPS_BUILDER_CODE ?? process.env.ONDOPERP_BUILDER_CODE ?? undefined,
builderFeeRateBps: parseOptionalNumber(
process.env.ONDOPERPS_BUILDER_FEE_RATE_BPS ?? process.env.ONDOPERP_BUILDER_FEE_RATE_BPS,
),
};
}
function isHex32(value: string): boolean {
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
}
+4
View File
@@ -426,6 +426,10 @@ const translations: Record<string, TranslationEntry> = {
zh: "StandX 需要配置 STANDX_TOKEN",
en: "StandX requires STANDX_TOKEN",
},
"env.missingOndoperps": {
zh: "Ondo Perps 需要配置 ONDOPERPS_API_KEY_ID 与 ONDOPERPS_API_SECRET(兼容旧 ONDOPERP_ 前缀)",
en: "Ondo Perps requires ONDOPERPS_API_KEY_ID and ONDOPERPS_API_SECRET (legacy ONDOPERP_ prefix is supported)",
},
"log.subscribe.accountFail": {
zh: "订阅账户失败: {error}",
en: "Failed to subscribe account: {error}",