mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +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)
|
* [Lighter 手续费优惠注册链接](https://app.lighter.xyz/?referral=111909FA)
|
||||||
|
* [Ondo Perps 邀请注册链接](https://app.ondoperps.xyz/?ref=4A3ACQ)
|
||||||
* [Aster 手续费优惠注册链接](https://www.asterdex.com/zh-CN/referral/4665f3)
|
* [Aster 手续费优惠注册链接](https://www.asterdex.com/zh-CN/referral/4665f3)
|
||||||
* [StandX 手续费优惠注册链接](https://standx.com/referral?code=xingxingjun)
|
* [StandX 手续费优惠注册链接](https://standx.com/referral?code=xingxingjun)
|
||||||
* [Binance 手续费优惠注册链接](https://www.binance.com/join?ref=KNKCA9XC)
|
* [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)
|
- [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
|
||||||
- [简明上手指南(零基础)](simple-readme.md)
|
- [简明上手指南(零基础)](simple-readme.md)
|
||||||
- [基础网格策略使用教程](grid-trading.md)
|
- [基础网格策略使用教程](grid-trading.md)
|
||||||
|
- [Ondo Perps 接入说明](docs/ondoperps/README.md)
|
||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
- **实时行情与风控**:Websocket + REST 自动同步账户、挂单与仓位,断线后自动恢复。
|
- **实时行情与风控**: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` 启用沙盒
|
| Backpack | USDC 永续 | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, `BACKPACK_PASSWORD` | `BACKPACK_SANDBOX=true` 启用沙盒
|
||||||
| Paradex | StarkEx 永续 | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | `PARADEX_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`
|
| 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` 命令)
|
- 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_SYMBOL` | 交易对(默认 `BTCUSDT`) |
|
||||||
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
|
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
|
||||||
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
|
| `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
|
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
|
||||||
|
|
||||||
* [StandX 做市策略教程](docs/standx/maker-points-guide.md)
|
* [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:
|
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)
|
* [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)
|
* [Aster referral link](https://www.asterdex.com/en/referral/4665f3)
|
||||||
* [StandX referral link](https://standx.com/referral?code=xingxingjun)
|
* [StandX referral link](https://standx.com/referral?code=xingxingjun)
|
||||||
* [Binance referral link](https://www.binance.com/join?ref=KNKCA9XC)
|
* [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)
|
- [ritmex-bot CLI 使用手册(中文)](cli-guide.md)
|
||||||
- [Beginner-friendly Quick Start](simple-readme.md)
|
- [Beginner-friendly Quick Start](simple-readme.md)
|
||||||
- [Grid Trading Strategy Guide](grid-trading.md)
|
- [Grid Trading Strategy Guide](grid-trading.md)
|
||||||
|
- [Ondo Perps Integration Guide](docs/ondoperps/README.md)
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
- **Live data & risk sync** via websockets with REST fallbacks and full reconciliation on restart.
|
- **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 |
|
| 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 |
|
| 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` |
|
| 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
|
## Requirements
|
||||||
- Bun >= 1.2 (both `bun` and `bunx` on PATH)
|
- 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 |
|
| 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_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 |
|
||||||
@@ -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
|
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
|
||||||
|
|
||||||
* [StandX Maker Points Strategy Guide](docs/standx/maker-points-guide.md)
|
* [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"];
|
options.exchange = normalized as CliOptions["exchange"];
|
||||||
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
|
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
|
||||||
options.exchange = "grvt";
|
options.exchange = "grvt";
|
||||||
|
} else if (normalized === "ondo" || normalized === "ondoperp") {
|
||||||
|
options.exchange = "ondoperps";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,15 @@ const STATIC_CAPABILITIES: Record<
|
|||||||
changeMarginMode: true,
|
changeMarginMode: true,
|
||||||
forceCancelAllOrders: true,
|
forceCancelAllOrders: true,
|
||||||
},
|
},
|
||||||
|
ondoperps: {
|
||||||
|
trailingStops: false,
|
||||||
|
fundingRate: true,
|
||||||
|
precision: true,
|
||||||
|
queryOpenOrders: true,
|
||||||
|
queryAccountSnapshot: true,
|
||||||
|
changeMarginMode: false,
|
||||||
|
forceCancelAllOrders: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CommandExecutorDependencies {
|
export interface CommandExecutorDependencies {
|
||||||
|
|||||||
@@ -470,6 +470,7 @@ function normalizeExchange(value: string | undefined): SupportedExchangeId | und
|
|||||||
const normalized = value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
if (normalized === "gravity" || normalized === "grav" || normalized === "grv") return "grvt";
|
if (normalized === "gravity" || normalized === "grav" || normalized === "grv") return "grvt";
|
||||||
if (normalized === "bnb") return "binance";
|
if (normalized === "bnb") return "binance";
|
||||||
|
if (normalized === "ondo" || normalized === "ondoperp") return "ondoperps";
|
||||||
if (isSupportedExchangeId(normalized)) return normalized;
|
if (isSupportedExchangeId(normalized)) return normalized;
|
||||||
throw new CommandParseError(`Unsupported exchange '${value}'`);
|
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" },
|
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" },
|
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 {
|
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 { 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";
|
import { BinanceExchangeAdapter, type BinanceCredentials } from "./binance/adapter";
|
||||||
|
import { OndoperpsExchangeAdapter, type OndoperpsCredentials } from "./ondoperps/adapter";
|
||||||
|
|
||||||
export const SUPPORTED_EXCHANGE_IDS = [
|
export const SUPPORTED_EXCHANGE_IDS = [
|
||||||
"aster",
|
"aster",
|
||||||
@@ -17,6 +18,7 @@ export const SUPPORTED_EXCHANGE_IDS = [
|
|||||||
"nado",
|
"nado",
|
||||||
"standx",
|
"standx",
|
||||||
"binance",
|
"binance",
|
||||||
|
"ondoperps",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const BASIS_SUPPORTED_EXCHANGE_IDS = [
|
export const BASIS_SUPPORTED_EXCHANGE_IDS = [
|
||||||
@@ -37,6 +39,8 @@ export interface ExchangeFactoryOptions {
|
|||||||
nado?: NadoCredentials;
|
nado?: NadoCredentials;
|
||||||
standx?: StandxCredentials;
|
standx?: StandxCredentials;
|
||||||
binance?: BinanceCredentials;
|
binance?: BinanceCredentials;
|
||||||
|
ondoperps?: OndoperpsCredentials;
|
||||||
|
ondoperp?: OndoperpsCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SupportedExchangeId = (typeof SUPPORTED_EXCHANGE_IDS)[number];
|
export type SupportedExchangeId = (typeof SUPPORTED_EXCHANGE_IDS)[number];
|
||||||
@@ -51,6 +55,7 @@ const EXCHANGE_DISPLAY_NAME: Record<SupportedExchangeId, string> = {
|
|||||||
nado: "Nado",
|
nado: "Nado",
|
||||||
standx: "StandX",
|
standx: "StandX",
|
||||||
binance: "Binance",
|
binance: "Binance",
|
||||||
|
ondoperps: "Ondo Perps",
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
|
const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
|
||||||
@@ -63,6 +68,9 @@ const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
|
|||||||
standx: "standx",
|
standx: "standx",
|
||||||
binance: "binance",
|
binance: "binance",
|
||||||
bnb: "binance",
|
bnb: "binance",
|
||||||
|
ondoperps: "ondoperps",
|
||||||
|
ondoperp: "ondoperps",
|
||||||
|
ondo: "ondoperps",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isSupportedExchangeId(value: string): value is SupportedExchangeId {
|
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 });
|
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
|
||||||
case "binance":
|
case "binance":
|
||||||
return new BinanceExchangeAdapter({ ...options.binance, symbol: options.symbol });
|
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 nadoOrders from "./nado/order";
|
||||||
import * as standxOrders from "./standx/order";
|
import * as standxOrders from "./standx/order";
|
||||||
import * as binanceOrders from "./binance/order";
|
import * as binanceOrders from "./binance/order";
|
||||||
|
import * as ondoperpsOrders from "./ondoperps/order";
|
||||||
|
|
||||||
type ExchangeKey = SupportedExchangeId;
|
type ExchangeKey = SupportedExchangeId;
|
||||||
|
|
||||||
@@ -85,13 +86,21 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
|||||||
trailingStop: binanceOrders.createTrailingStopOrder,
|
trailingStop: binanceOrders.createTrailingStopOrder,
|
||||||
close: binanceOrders.createClosePositionOrder,
|
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];
|
const knownExchanges: ExchangeKey[] = [...SUPPORTED_EXCHANGE_IDS];
|
||||||
|
|
||||||
function normalizeExchangeId(value: string | undefined | null): string | undefined {
|
function normalizeExchangeId(value: string | undefined | null): string | undefined {
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
return value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
|
return normalized === "ondoperp" ? "ondoperps" : normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveExchangeKey(adapter: ExchangeAdapter): ExchangeKey {
|
function resolveExchangeKey(adapter: ExchangeAdapter): ExchangeKey {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 type { BinanceCredentials } from "./binance/adapter";
|
||||||
|
import type { OndoperpsCredentials } from "./ondoperps/adapter";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import type { Address } from "viem";
|
import type { Address } from "viem";
|
||||||
|
|
||||||
@@ -50,6 +51,10 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
|
|||||||
const credentials = resolveBinanceCredentials(symbol);
|
const credentials = resolveBinanceCredentials(symbol);
|
||||||
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
|
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 {
|
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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,6 +426,10 @@ const translations: Record<string, TranslationEntry> = {
|
|||||||
zh: "StandX 需要配置 STANDX_TOKEN",
|
zh: "StandX 需要配置 STANDX_TOKEN",
|
||||||
en: "StandX requires 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": {
|
"log.subscribe.accountFail": {
|
||||||
zh: "订阅账户失败: {error}",
|
zh: "订阅账户失败: {error}",
|
||||||
en: "Failed to subscribe account: {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", () => {
|
it("throws for unsupported option", () => {
|
||||||
expect(() => parseCommandArgv(["doctor", "--unknown"])).toThrow(CommandParseError);
|
expect(() => parseCommandArgv(["doctor", "--unknown"])).toThrow(CommandParseError);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,4 +56,19 @@ describe("resolveSymbolFromEnv", () => {
|
|||||||
|
|
||||||
expect(resolveSymbolFromEnv("binance")).toBe("ETHUSDT");
|
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: {
|
||||||
LIGHTER_ACCOUNT_INDEX: "1",
|
LIGHTER_ACCOUNT_INDEX: "1",
|
||||||
LIGHTER_API_PRIVATE_KEY: "lighter-private-key",
|
LIGHTER_API_PRIVATE_KEY: "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b",
|
||||||
LIGHTER_API_KEY_INDEX: "0",
|
LIGHTER_API_KEY_INDEX: "0",
|
||||||
},
|
},
|
||||||
backpack: {
|
backpack: {
|
||||||
@@ -64,6 +64,10 @@ const REQUIRED_ENV_BY_EXCHANGE: Record<SupportedExchangeId, Record<string, strin
|
|||||||
BINANCE_API_KEY: "binance-key",
|
BINANCE_API_KEY: "binance-key",
|
||||||
BINANCE_API_SECRET: "binance-secret",
|
BINANCE_API_SECRET: "binance-secret",
|
||||||
},
|
},
|
||||||
|
ondoperps: {
|
||||||
|
ONDOPERPS_API_KEY_ID: "ondoKeyId_test",
|
||||||
|
ONDOPERPS_API_SECRET: "ondoApiSecret_test",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class RecorderAdapter implements ExchangeAdapter {
|
class RecorderAdapter implements ExchangeAdapter {
|
||||||
@@ -151,6 +155,7 @@ describe("exchange contract suite", () => {
|
|||||||
for (const id of SUPPORTED_EXCHANGE_IDS) {
|
for (const id of SUPPORTED_EXCHANGE_IDS) {
|
||||||
expect(output).toContain(id);
|
expect(output).toContain(id);
|
||||||
}
|
}
|
||||||
|
expect(parseCliArgs(["--exchange", "ondoperp"]).exchange).toBe("ondoperps");
|
||||||
spy.mockRestore();
|
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 { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||||
import { createExchangeAdapter, resolveExchangeId } from "../src/exchanges/create-adapter";
|
import { createExchangeAdapter, resolveExchangeId } from "../src/exchanges/create-adapter";
|
||||||
|
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
|
||||||
import { AsterExchangeAdapter } from "../src/exchanges/aster/adapter";
|
import { AsterExchangeAdapter } from "../src/exchanges/aster/adapter";
|
||||||
import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
|
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";
|
import { BinanceExchangeAdapter } from "../src/exchanges/binance/adapter";
|
||||||
|
import { OndoperpsExchangeAdapter } from "../src/exchanges/ondoperps/adapter";
|
||||||
|
|
||||||
const ORIGINAL_ENV = { ...process.env };
|
const ORIGINAL_ENV = { ...process.env };
|
||||||
|
|
||||||
@@ -34,6 +36,8 @@ describe("exchange factory", () => {
|
|||||||
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");
|
expect(resolveExchangeId("BiNaNcE")).toBe("binance");
|
||||||
|
expect(resolveExchangeId("OndoPerps")).toBe("ondoperps");
|
||||||
|
expect(resolveExchangeId("OndoPerp")).toBe("ondoperps");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates grvt adapter when EXCHANGE=grvt", () => {
|
it("creates grvt adapter when EXCHANGE=grvt", () => {
|
||||||
@@ -92,4 +96,41 @@ describe("exchange factory", () => {
|
|||||||
expect(adapter).toBeInstanceOf(BinanceExchangeAdapter);
|
expect(adapter).toBeInstanceOf(BinanceExchangeAdapter);
|
||||||
expect(adapter.id).toBe("binance");
|
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