mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
feat(exchanges): add Ondo Perps exchange adapter
This commit is contained in:
@@ -11,6 +11,7 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
|
||||
如果您希望获取优惠并支持本项目,请考虑使用以下注册链接:
|
||||
|
||||
* [Lighter 手续费优惠注册链接](https://app.lighter.xyz/?referral=111909FA)
|
||||
* [Ondo Perps 邀请注册链接](https://app.ondoperps.xyz/?ref=4A3ACQ)
|
||||
* [Aster 手续费优惠注册链接](https://www.asterdex.com/zh-CN/referral/4665f3)
|
||||
* [StandX 手续费优惠注册链接](https://standx.com/referral?code=xingxingjun)
|
||||
* [Binance 手续费优惠注册链接](https://www.binance.com/join?ref=KNKCA9XC)
|
||||
@@ -41,6 +42,7 @@ npx skills add https://github.com/discountry/ritmex-bot --skill use-ritmex-bot
|
||||
- [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
|
||||
- [简明上手指南(零基础)](simple-readme.md)
|
||||
- [基础网格策略使用教程](grid-trading.md)
|
||||
- [Ondo Perps 接入说明](docs/ondoperps/README.md)
|
||||
|
||||
## 核心特性
|
||||
- **实时行情与风控**:Websocket + REST 自动同步账户、挂单与仓位,断线后自动恢复。
|
||||
@@ -59,6 +61,7 @@ npx skills add https://github.com/discountry/ritmex-bot --skill use-ritmex-bot
|
||||
| Backpack | USDC 永续 | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, `BACKPACK_PASSWORD` | `BACKPACK_SANDBOX=true` 启用沙盒
|
||||
| Paradex | StarkEx 永续 | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | `PARADEX_SANDBOX=true` 使用测试网
|
||||
| Nado | USDC 永续 | `NADO_SIGNER_PRIVATE_KEY`, `NADO_SUBACCOUNT_OWNER` | `NADO_ENV` 可切换 `inkMainnet`/`inkTestnet`
|
||||
| Ondo Perps | USD 加密资产/股票/商品永续 | `ONDOPERPS_API_KEY_ID`, `ONDOPERPS_API_SECRET` | API Key HMAC 鉴权;支持生产与沙盒环境
|
||||
|
||||
## 系统要求
|
||||
- Bun ≥ 1.2(需同时包含 `bun`、`bunx` 命令)
|
||||
@@ -103,7 +106,7 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
|
||||
|
||||
| 变量 | 说明 |
|
||||
| --- | --- |
|
||||
| `EXCHANGE` | 选择交易所(`aster`/`binance`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado`) |
|
||||
| `EXCHANGE` | 选择交易所(`aster`/`binance`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado`/`ondoperps`) |
|
||||
| `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT`) |
|
||||
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
|
||||
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
|
||||
@@ -160,6 +163,17 @@ EXCHANGE=binance BINANCE_MARKET_TYPE=perp BINANCE_SYMBOL=BTCUSDT_PERP bun run in
|
||||
EXCHANGE=binance BINANCE_MARKET_TYPE=spot BINANCE_SYMBOL=BTCUSDT bun run index.ts --strategy grid
|
||||
```
|
||||
|
||||
### Ondo Perps
|
||||
1. 使用[邀请链接](https://app.ondoperps.xyz/?ref=4A3ACQ)注册并在账户页面创建具备交易权限的 API Key。
|
||||
2. 设置 `EXCHANGE=ondoperps`,填写 `ONDOPERPS_API_KEY_ID` 与 `ONDOPERPS_API_SECRET`。
|
||||
3. 设置官方市场格式的 `ONDOPERPS_SYMBOL`,默认 `BTC-USD.P`;其他示例包括 `XAU-USD.P` 与 `NVDA-USD.P`。
|
||||
4. 沙盒环境设置 `ONDOPERPS_SANDBOX=true`;自定义端点可使用 `ONDOPERPS_BASE_URL` 与 `ONDOPERPS_WS_URL`。
|
||||
5. Builder 接入可选配置 `ONDOPERPS_BUILDER_CODE` 与 `ONDOPERPS_BUILDER_FEE_RATE_BPS`,费率上限为 10 bps。
|
||||
|
||||
兼容配置:`ondoperp` 可作为 `ondoperps` 的交易所别名,旧 `ONDOPERP_*` 环境变量前缀继续生效。
|
||||
|
||||
适配器使用 REST HMAC 签名执行下单、撤单和账户查询,通过 WebSocket 接收深度、标记价、K 线、订单、仓位与余额更新。仓位级止损映射为统一的 `STOP_MARKET` 订单。官方接入索引:[Ondo Perps llms.txt](https://ondoperps.mintlify.app/llms.txt)。
|
||||
|
||||
### StandX
|
||||
|
||||
* [StandX 做市策略教程](docs/standx/maker-points-guide.md)
|
||||
|
||||
+15
-1
@@ -7,6 +7,7 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
|
||||
If you'd like to support this project and get fee discounts, please consider using these referral links:
|
||||
|
||||
* [Lighter referral link](https://app.lighter.xyz/?referral=111909FA)
|
||||
* [Ondo Perps invite link](https://app.ondoperps.xyz/?ref=4A3ACQ)
|
||||
* [Aster referral link](https://www.asterdex.com/en/referral/4665f3)
|
||||
* [StandX referral link](https://standx.com/referral?code=xingxingjun)
|
||||
* [Binance referral link](https://www.binance.com/join?ref=KNKCA9XC)
|
||||
@@ -37,6 +38,7 @@ Full guide: [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
|
||||
- [ritmex-bot CLI 使用手册(中文)](cli-guide.md)
|
||||
- [Beginner-friendly Quick Start](simple-readme.md)
|
||||
- [Grid Trading Strategy Guide](grid-trading.md)
|
||||
- [Ondo Perps Integration Guide](docs/ondoperps/README.md)
|
||||
|
||||
## Highlights
|
||||
- **Live data & risk sync** via websockets with REST fallbacks and full reconciliation on restart.
|
||||
@@ -55,6 +57,7 @@ Full guide: [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
|
||||
| Backpack | USDC perpetuals | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, `BACKPACK_PASSWORD` | Set `BACKPACK_SANDBOX=true` for the sandbox |
|
||||
| Paradex | StarkEx perpetuals | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | Toggle `PARADEX_SANDBOX=true` for the testnet |
|
||||
| Nado | USDC perpetuals | `NADO_SIGNER_PRIVATE_KEY`, `NADO_SUBACCOUNT_OWNER` | Switch `NADO_ENV` between `inkMainnet` and `inkTestnet` |
|
||||
| Ondo Perps | USD crypto/equity/commodity perpetuals | `ONDOPERPS_API_KEY_ID`, `ONDOPERPS_API_SECRET` | API-key HMAC authentication with production and sandbox endpoints |
|
||||
|
||||
## Requirements
|
||||
- Bun >= 1.2 (both `bun` and `bunx` on PATH)
|
||||
@@ -99,7 +102,7 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
|
||||
|
||||
| Variable | Purpose |
|
||||
| --- | --- |
|
||||
| `EXCHANGE` | Choose the venue (`aster` / `binance` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) |
|
||||
| `EXCHANGE` | Choose the venue (`aster` / `binance` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado` / `ondoperps`) |
|
||||
| `TRADE_SYMBOL` | Contract symbol (defaults to `BTCUSDT`) |
|
||||
| `TRADE_AMOUNT` | Order size in base asset units |
|
||||
| `LOSS_LIMIT` | Max per-trade loss in USDT before forced close |
|
||||
@@ -157,6 +160,17 @@ EXCHANGE=binance BINANCE_MARKET_TYPE=perp BINANCE_SYMBOL=BTCUSDT_PERP bun run in
|
||||
EXCHANGE=binance BINANCE_MARKET_TYPE=spot BINANCE_SYMBOL=BTCUSDT bun run index.ts --strategy grid
|
||||
```
|
||||
|
||||
### Ondo Perps
|
||||
1. Register through the [invite link](https://app.ondoperps.xyz/?ref=4A3ACQ) and create an API key with trading permissions from the account page.
|
||||
2. Set `EXCHANGE=ondoperps`, `ONDOPERPS_API_KEY_ID`, and `ONDOPERPS_API_SECRET`.
|
||||
3. Set `ONDOPERPS_SYMBOL` with the official market format. The default is `BTC-USD.P`; other examples include `XAU-USD.P` and `NVDA-USD.P`.
|
||||
4. Use `ONDOPERPS_SANDBOX=true` for sandbox endpoints. `ONDOPERPS_BASE_URL` and `ONDOPERPS_WS_URL` provide explicit endpoint overrides.
|
||||
5. Builder integrations can set `ONDOPERPS_BUILDER_CODE` and `ONDOPERPS_BUILDER_FEE_RATE_BPS`; the fee is capped at 10 bps.
|
||||
|
||||
Compatibility: `ondoperp` remains an exchange alias for `ondoperps`, and legacy `ONDOPERP_*` environment variables remain supported.
|
||||
|
||||
The adapter signs REST requests with the Ondo API-key HMAC scheme and consumes WebSocket depth, mark price, kline, order, position, and balance updates. Position-level stops map into the shared `STOP_MARKET` order contract. Official integration index: [Ondo Perps llms.txt](https://ondoperps.mintlify.app/llms.txt).
|
||||
|
||||
### StandX
|
||||
|
||||
* [StandX Maker Points Strategy Guide](docs/standx/maker-points-guide.md)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Ondo Perps 接入说明
|
||||
|
||||
## 链接
|
||||
|
||||
- 邀请注册:https://app.ondoperps.xyz/?ref=4A3ACQ
|
||||
- 官方文档索引:https://ondoperps.mintlify.app/llms.txt
|
||||
- REST OpenAPI:https://docs.ondoperps.xyz/api-reference/rest-spec.json
|
||||
- WebSocket OpenAPI:https://docs.ondoperps.xyz/api-reference/ws-spec.json
|
||||
- API Key 鉴权:https://docs.ondoperps.xyz/api-reference/api_key_authentication.md
|
||||
|
||||
## 配置
|
||||
|
||||
```bash
|
||||
EXCHANGE=ondoperps
|
||||
ONDOPERPS_API_KEY_ID=ondoKeyId_xxx
|
||||
ONDOPERPS_API_SECRET=ondoApiSecret_xxx
|
||||
ONDOPERPS_SYMBOL=BTC-USD.P
|
||||
```
|
||||
|
||||
Ondo Perps 市场使用 `{TICKER}-USD.P` 格式。默认值为 `BTC-USD.P`,其他示例包括 `ETH-USD.P`、`XAU-USD.P`、`NVDA-USD.P` 与 `AMD-USD.P`。
|
||||
|
||||
兼容入口:`EXCHANGE=ondoperp` 会解析为 `ondoperps`,旧 `ONDOPERP_*` 环境变量会在对应 `ONDOPERPS_*` 变量缺失时使用。
|
||||
|
||||
可选配置:
|
||||
|
||||
| 变量 | 说明 |
|
||||
| --- | --- |
|
||||
| `ONDOPERPS_SANDBOX` | `true` 时连接官方沙盒 REST 与 WebSocket 地址 |
|
||||
| `ONDOPERPS_BASE_URL` | 覆盖 REST API 地址 |
|
||||
| `ONDOPERPS_WS_URL` | 覆盖 WebSocket 地址 |
|
||||
| `ONDOPERPS_BUILDER_CODE` | Ondo 分配的 Builder Code |
|
||||
| `ONDOPERPS_BUILDER_FEE_RATE_BPS` | Builder 订单费率,适配器限制在 1–10 bps |
|
||||
|
||||
## 已接入能力
|
||||
|
||||
- API Key HMAC-SHA256 REST 鉴权
|
||||
- 限价单、Post Only 限价单、市价单、批量撤单与全部撤单
|
||||
- 仓位级止损与止盈,映射到统一订单类型
|
||||
- 账户余额、仓位、活动订单与市场精度查询
|
||||
- 深度、标记价、K 线、资金费率、订单、仓位与余额 WebSocket 订阅
|
||||
- REST 定时校准,覆盖 WebSocket 断线和私有频道鉴权异常场景
|
||||
- 生产与沙盒端点切换
|
||||
|
||||
## 鉴权规则
|
||||
|
||||
REST 请求发送以下请求头:
|
||||
|
||||
- `ONDO-KEY-ID`
|
||||
- `ONDO-TIMESTAMP`
|
||||
- `ONDO-SIGN`
|
||||
|
||||
签名内容为 `timestamp + uppercaseMethod + requestPathWithQuery + body`,使用 API Secret 进行 HMAC-SHA256 并输出十六进制字符串。
|
||||
|
||||
WebSocket 登录消息使用 API Key ID、毫秒时间戳与 `time + "ondo_perps_ws_login"` 的 HMAC-SHA256 签名。连接空闲限制为 180 秒,适配器每 30 秒发送一次 ping。
|
||||
|
||||
## 安全
|
||||
|
||||
API Secret 只通过运行环境传入。仓库文件、日志与错误信息均不保存请求签名头或凭证内容。建议在 Ondo Perps 后台配置固定 IPv4 白名单,并为 API Key 只授予策略需要的权限。
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}'`);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -81,6 +81,16 @@ describe("command parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses the Ondo Perps exchange alias", () => {
|
||||
for (const exchange of ["ondoperps", "ondoperp"]) {
|
||||
const command = parseCommandArgv(["exchange", "capabilities", "--exchange", exchange]);
|
||||
expect(command).toMatchObject({
|
||||
kind: "exchange-capabilities",
|
||||
exchange: "ondoperps",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("throws for unsupported option", () => {
|
||||
expect(() => parseCommandArgv(["doctor", "--unknown"])).toThrow(CommandParseError);
|
||||
});
|
||||
|
||||
@@ -56,4 +56,19 @@ describe("resolveSymbolFromEnv", () => {
|
||||
|
||||
expect(resolveSymbolFromEnv("binance")).toBe("ETHUSDT");
|
||||
});
|
||||
|
||||
it("supports Ondo Perps symbol defaults when explicit exchange id is provided", () => {
|
||||
delete process.env.EXCHANGE;
|
||||
process.env.ONDOPERPS_SYMBOL = "NVDA-USD.P";
|
||||
|
||||
expect(resolveSymbolFromEnv("ondoperps")).toBe("NVDA-USD.P");
|
||||
});
|
||||
|
||||
it("supports the legacy Ondo Perps exchange id and symbol prefix", () => {
|
||||
delete process.env.EXCHANGE;
|
||||
delete process.env.ONDOPERPS_SYMBOL;
|
||||
process.env.ONDOPERP_SYMBOL = "ETH-USD.P";
|
||||
|
||||
expect(resolveSymbolFromEnv("ondoperp")).toBe("ETH-USD.P");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ const REQUIRED_ENV_BY_EXCHANGE: Record<SupportedExchangeId, Record<string, strin
|
||||
},
|
||||
lighter: {
|
||||
LIGHTER_ACCOUNT_INDEX: "1",
|
||||
LIGHTER_API_PRIVATE_KEY: "lighter-private-key",
|
||||
LIGHTER_API_PRIVATE_KEY: "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b",
|
||||
LIGHTER_API_KEY_INDEX: "0",
|
||||
},
|
||||
backpack: {
|
||||
@@ -64,6 +64,10 @@ const REQUIRED_ENV_BY_EXCHANGE: Record<SupportedExchangeId, Record<string, strin
|
||||
BINANCE_API_KEY: "binance-key",
|
||||
BINANCE_API_SECRET: "binance-secret",
|
||||
},
|
||||
ondoperps: {
|
||||
ONDOPERPS_API_KEY_ID: "ondoKeyId_test",
|
||||
ONDOPERPS_API_SECRET: "ondoApiSecret_test",
|
||||
},
|
||||
};
|
||||
|
||||
class RecorderAdapter implements ExchangeAdapter {
|
||||
@@ -151,6 +155,7 @@ describe("exchange contract suite", () => {
|
||||
for (const id of SUPPORTED_EXCHANGE_IDS) {
|
||||
expect(output).toContain(id);
|
||||
}
|
||||
expect(parseCliArgs(["--exchange", "ondoperp"]).exchange).toBe("ondoperps");
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -258,4 +263,21 @@ describe("exchange contract suite", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("routes orders from adapters that still report the legacy ondoperp id", async () => {
|
||||
delete process.env.EXCHANGE;
|
||||
delete process.env.TRADE_EXCHANGE;
|
||||
const adapter = new RecorderAdapter("ondoperps");
|
||||
(adapter as unknown as { id: string }).id = "ondoperp";
|
||||
|
||||
await routeLimitOrder({
|
||||
adapter,
|
||||
symbol: "BTC-USD.P",
|
||||
side: "BUY",
|
||||
quantity: 0.01,
|
||||
price: 100_000,
|
||||
});
|
||||
|
||||
expect(adapter.lastCreateOrderParams?.type).toBe("LIMIT");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
import { createExchangeAdapter, resolveExchangeId } from "../src/exchanges/create-adapter";
|
||||
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
|
||||
import { AsterExchangeAdapter } from "../src/exchanges/aster/adapter";
|
||||
import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
|
||||
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
|
||||
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
|
||||
import { StandxExchangeAdapter } from "../src/exchanges/standx/adapter";
|
||||
import { BinanceExchangeAdapter } from "../src/exchanges/binance/adapter";
|
||||
import { OndoperpsExchangeAdapter } from "../src/exchanges/ondoperps/adapter";
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env };
|
||||
|
||||
@@ -34,6 +36,8 @@ describe("exchange factory", () => {
|
||||
expect(resolveExchangeId("PaRaDeX")).toBe("paradex");
|
||||
expect(resolveExchangeId("StandX")).toBe("standx");
|
||||
expect(resolveExchangeId("BiNaNcE")).toBe("binance");
|
||||
expect(resolveExchangeId("OndoPerps")).toBe("ondoperps");
|
||||
expect(resolveExchangeId("OndoPerp")).toBe("ondoperps");
|
||||
});
|
||||
|
||||
it("creates grvt adapter when EXCHANGE=grvt", () => {
|
||||
@@ -92,4 +96,41 @@ describe("exchange factory", () => {
|
||||
expect(adapter).toBeInstanceOf(BinanceExchangeAdapter);
|
||||
expect(adapter.id).toBe("binance");
|
||||
});
|
||||
|
||||
it("creates ondoperps adapter when EXCHANGE=ondoperps", () => {
|
||||
process.env.EXCHANGE = "ondoperps";
|
||||
process.env.ONDOPERPS_API_KEY_ID = "ondoKeyId_test";
|
||||
process.env.ONDOPERPS_API_SECRET = "ondoApiSecret_test";
|
||||
process.env.ONDOPERPS_SYMBOL = "XAU-USD.P";
|
||||
|
||||
const adapter = createExchangeAdapter({ symbol: "XAU-USD.P" });
|
||||
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
|
||||
expect(adapter.id).toBe("ondoperps");
|
||||
});
|
||||
|
||||
it("accepts the legacy ondoperp id and environment prefix", () => {
|
||||
delete process.env.ONDOPERPS_API_KEY_ID;
|
||||
delete process.env.ONDOPERPS_API_SECRET;
|
||||
delete process.env.ONDOPERPS_SYMBOL;
|
||||
process.env.ONDOPERP_API_KEY_ID = "ondoKeyId_legacy";
|
||||
process.env.ONDOPERP_API_SECRET = "ondoApiSecret_legacy";
|
||||
process.env.ONDOPERP_SYMBOL = "ETH-USD.P";
|
||||
|
||||
const adapter = buildAdapterFromEnv({ exchangeId: "ondoperp", symbol: "BTC-USD.P" });
|
||||
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
|
||||
expect(adapter.id).toBe("ondoperps");
|
||||
});
|
||||
|
||||
it("accepts the legacy ondoperp factory option", () => {
|
||||
const adapter = createExchangeAdapter({
|
||||
exchange: "ondoperp",
|
||||
symbol: "BTC-USD.P",
|
||||
ondoperp: {
|
||||
apiKeyId: "ondoKeyId_legacy",
|
||||
apiSecret: "ondoApiSecret_legacy",
|
||||
},
|
||||
});
|
||||
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
|
||||
expect(adapter.id).toBe("ondoperps");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildOndoperpsAuthHeaders,
|
||||
createOndoperpsWsSignature,
|
||||
mapOndoperpsAccountSnapshot,
|
||||
mapOndoperpsOrder,
|
||||
normalizeOndoperpsSymbol,
|
||||
OndoperpsGateway,
|
||||
} from "../src/exchanges/ondoperps/gateway";
|
||||
|
||||
describe("Ondo Perps gateway", () => {
|
||||
it("normalizes common symbols to the official market format", () => {
|
||||
expect(normalizeOndoperpsSymbol("")).toBe("BTC-USD.P");
|
||||
expect(normalizeOndoperpsSymbol("xau")).toBe("XAU-USD.P");
|
||||
expect(normalizeOndoperpsSymbol("NVDA/USD")).toBe("NVDA-USD.P");
|
||||
expect(normalizeOndoperpsSymbol("AAPL-USD.P")).toBe("AAPL-USD.P");
|
||||
});
|
||||
|
||||
it("builds REST and WebSocket HMAC signatures from the documented payload", () => {
|
||||
const body = JSON.stringify({ market: "XAU-USD.P", side: "buy", size: "1" });
|
||||
const headers = buildOndoperpsAuthHeaders({
|
||||
apiKeyId: "ondoKeyId_test",
|
||||
apiSecret: "ondoApiSecret_secret",
|
||||
timestamp: "1700000000000",
|
||||
method: "POST",
|
||||
requestPath: "/v1/perps/orders?market=XAU-USD.P",
|
||||
body,
|
||||
});
|
||||
|
||||
expect(headers).toEqual({
|
||||
"ONDO-KEY-ID": "ondoKeyId_test",
|
||||
"ONDO-TIMESTAMP": "1700000000000",
|
||||
"ONDO-SIGN": "3cf0628da79a19225d79fd17ab4767ee1ecae0219a52e1b1fc1b0c1a8bc7a880",
|
||||
});
|
||||
expect(createOndoperpsWsSignature("ondoApiSecret_secret", "1700000000000")).toBe(
|
||||
"eb87f259c41c7cba728b1ce5baf0a385e6e5c1b1031424d73206cacbaebea742",
|
||||
);
|
||||
expect(createOndoperpsWsSignature("ondoApiSecret_secret", "1700000000000", true)).toBe(
|
||||
"46ca7822ee8444eccd96feaa596e30072c8428aa8ec2cedbe0fdc6744bc21ec7",
|
||||
);
|
||||
});
|
||||
|
||||
it("maps Ondo order fields into the shared exchange contract", () => {
|
||||
const order = mapOndoperpsOrder(
|
||||
{
|
||||
orderId: "order-1",
|
||||
clientOrderId: "client-1",
|
||||
side: "sell",
|
||||
price: "201.25",
|
||||
size: "2",
|
||||
market: "NVDA-USD.P",
|
||||
filledSize: "0.5",
|
||||
filledCost: "100.625",
|
||||
status: "open",
|
||||
createdAt: "2026-07-11T00:00:00Z",
|
||||
type: "limit",
|
||||
timeInForce: "GTC",
|
||||
reduceOnly: true,
|
||||
},
|
||||
"NVDA-USD.P",
|
||||
);
|
||||
|
||||
expect(order).toMatchObject({
|
||||
orderId: "order-1",
|
||||
clientOrderId: "client-1",
|
||||
symbol: "NVDA-USD.P",
|
||||
side: "SELL",
|
||||
type: "LIMIT",
|
||||
status: "NEW",
|
||||
origQty: "2",
|
||||
executedQty: "0.5",
|
||||
avgPrice: "201.25",
|
||||
reduceOnly: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates post-only orders with signed REST headers", async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
||||
const fetchFn = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
requests.push({ url, init });
|
||||
if (url.endsWith("/v1/markets")) {
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
result: {
|
||||
perps: {
|
||||
tradingPairs: [
|
||||
{ market: "XAU-USD.P", baseIncrement: "0.01", quoteIncrement: "0.1" },
|
||||
],
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
result: {
|
||||
orderId: "order-2",
|
||||
clientOrderId: "client-2",
|
||||
side: "buy",
|
||||
price: "2500",
|
||||
size: "0.5",
|
||||
market: "XAU-USD.P",
|
||||
filledSize: "0",
|
||||
status: "open",
|
||||
createdAt: "2026-07-11T00:00:00Z",
|
||||
type: "limit",
|
||||
timeInForce: "GTC",
|
||||
reduceOnly: false,
|
||||
},
|
||||
}));
|
||||
});
|
||||
const fakeWebSocket = {
|
||||
readyState: 0,
|
||||
addEventListener: vi.fn(),
|
||||
send: vi.fn(),
|
||||
} as unknown as WebSocket;
|
||||
const gateway = new OndoperpsGateway({
|
||||
apiKeyId: "ondoKeyId_test",
|
||||
apiSecret: "ondoApiSecret_secret",
|
||||
symbol: "XAU-USD.P",
|
||||
fetchFn: fetchFn as unknown as typeof fetch,
|
||||
webSocketFactory: () => fakeWebSocket,
|
||||
now: () => 1_700_000_000_000,
|
||||
});
|
||||
|
||||
const order = await gateway.createOrder({
|
||||
symbol: "XAU-USD.P",
|
||||
side: "BUY",
|
||||
type: "LIMIT",
|
||||
quantity: 0.5,
|
||||
price: 2500,
|
||||
timeInForce: "GTX",
|
||||
clientOrderId: "client-2",
|
||||
});
|
||||
|
||||
const request = requests[1];
|
||||
expect(request?.url).toBe("https://api.ondoperps.xyz/v1/perps/orders");
|
||||
expect(JSON.parse(String(request?.init?.body))).toMatchObject({
|
||||
market: "XAU-USD.P",
|
||||
type: "limit",
|
||||
side: "buy",
|
||||
size: "0.5",
|
||||
price: "2500",
|
||||
timeInForce: "GTC",
|
||||
postOnly: true,
|
||||
clientOrderId: "client-2",
|
||||
});
|
||||
expect(request?.init?.headers).toMatchObject({
|
||||
"ONDO-KEY-ID": "ondoKeyId_test",
|
||||
"ONDO-TIMESTAMP": "1700000000000",
|
||||
});
|
||||
expect(order).toMatchObject({ orderId: "order-2", status: "NEW", type: "LIMIT" });
|
||||
});
|
||||
|
||||
it("maps short positions and USDC margin fields into an account snapshot", () => {
|
||||
const snapshot = mapOndoperpsAccountSnapshot(
|
||||
{
|
||||
walletBalance: "5000",
|
||||
realizedPnl: "10",
|
||||
unrealizedPnl: "-25",
|
||||
marginBalance: "4975",
|
||||
usedMargin: "1000",
|
||||
availableMargin: "3975",
|
||||
withdrawableMargin: "3975",
|
||||
maintenanceMarginRequirement: "50",
|
||||
totalMaintenanceMargin: "80",
|
||||
marginRatio: "0.016",
|
||||
leverage: "1.5",
|
||||
underLiquidation: false,
|
||||
totalFundingPayments: "-1",
|
||||
totalTradingFees: "2",
|
||||
totalPnL: "-18",
|
||||
},
|
||||
[
|
||||
{
|
||||
market: "NVDA-USD.P",
|
||||
direction: "short",
|
||||
netQuantity: "3",
|
||||
averageEntryPrice: "200",
|
||||
usedMargin: "1000",
|
||||
unrealizedPnl: "-25",
|
||||
markPrice: "208.33",
|
||||
liquidationPrice: "260",
|
||||
bankruptcyPrice: "275",
|
||||
maintenanceMargin: "50",
|
||||
notionalValue: "625",
|
||||
leverage: "1.5",
|
||||
netFundingSinceNeutral: "-1",
|
||||
returnOnEquity: "-0.025",
|
||||
},
|
||||
],
|
||||
"NVDA-USD.P",
|
||||
"NVDA-USD.P",
|
||||
1234,
|
||||
);
|
||||
|
||||
expect(snapshot.totalWalletBalance).toBe("5000");
|
||||
expect(snapshot.availableBalance).toBe("3975");
|
||||
expect(snapshot.assets[0]?.asset).toBe("USDC");
|
||||
expect(snapshot.positions[0]).toMatchObject({
|
||||
symbol: "NVDA-USD.P",
|
||||
positionAmt: "-3",
|
||||
entryPrice: "200",
|
||||
markPrice: "208.33",
|
||||
positionSide: "BOTH",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user