Author SHA1 Message Date
discountry 35653c8beb docs: add Hyperliquid invite link to README files 2026-07-12 11:15:29 +08:00
discountry 1f2c2150e0 feat(exchanges): add Ondo Perps exchange adapter 2026-07-11 15:10:40 +08:00
discountry 6b5f2542a4 docs: update repository guidelines and project structure details 2026-04-06 20:18:54 +08:00
discountry 4e69b81da2 docs: update testing instructions for Bun in CLAUDE.md 2026-04-06 20:08:52 +08:00
discountry 345ad11ba3 refactor: enhance order handling and tracking in GridEngine
- Introduced a clientOrderId system for better order identification.
- Updated order creation logic to ensure unique clientOrderIds.
- Improved order cancellation methods to maintain accurate current orders.
- Added tests for new clientOrderId functionality and level state tracking.
- Ensured that desired orders have an intent field set for clarity.
- Enhanced snapshot functionality to include level states for grid lines.
2026-04-06 20:05:03 +08:00
discountry d925fd93ee refactor: update import paths for AsterSpotBookTicker type
- Moved AsterSpotBookTicker import to the correct module path from "../exchanges/aster/types".
- Cleaned up imports in basis-arb-engine.ts for better organization and clarity.
2026-04-06 18:54:56 +08:00
discountry e81d1396a2 Implement feature X to enhance user experience and fix bug Y in module Z 2026-04-06 18:40:12 +08:00
discountry 20855c2d1d refactor: extract shared adapter utilities (safeInvoke + initManager)
Created src/exchanges/adapter-utils.ts with createSafeInvoke() and
createInitManager() factories. Converted 7/8 adapters to use shared
utilities, eliminating ~350 lines of duplicated retry/init/error-handling
boilerplate. Lighter adapter kept as-is (no retry logic by design).
Also translated Chinese comments in adapter.ts and order-schema.ts.
2026-04-06 18:36:56 +08:00
discountry d799cf79c2 refactor: extract shared order handler factory, deduplicate 8 order.ts files
Created src/exchanges/order-handlers.ts with createOrderHandlers() factory.
All 8 exchange order.ts files now use config-driven delegation instead of
duplicated ~80-line implementations. Shared applyCommonFields() logic
consolidated into the factory module.
2026-04-06 18:24:42 +08:00
discountry bc29b23027 refactor: restructure aster/ to match standard exchange directory layout
- Move aster-adapter.ts → aster/adapter.ts (consistent with all other exchanges)
- Rename aster/client.ts → aster/gateway.ts (consistent naming)
- Update all imports across 5 files
- Delete old aster-adapter.ts
2026-04-06 18:17:50 +08:00
discountry fb906be27c refactor: split types.ts into per-exchange type modules
- Extract GRVT-specific types to grvt/types.ts
- Extract Aster-spot/futures types to aster/types.ts
- types.ts now contains only universal/platform-agnostic types
- Reduced from 632 to ~135 lines
2026-04-06 18:16:25 +08:00
discountry fda6bcad1d refactor: rename Aster-prefixed universal types to clean names
- AsterOrder → Order
- AsterAccountSnapshot → AccountSnapshot
- AsterAccountPosition → AccountPosition
- AsterAccountAsset → AccountAsset
- AsterDepthLevel → DepthLevel
- AsterDepth → Depth
- AsterTicker → Ticker
- AsterKline → Kline

These types are the platform-agnostic contract used by all 8 exchanges,
not Aster-specific. Renamed across 63 files.
2026-04-06 18:11:57 +08:00
discountry 230c0d5e18 Add .factory to .gitignore and ensure proper newline at end of file 2026-04-06 17:58:57 +08:00
discountry eb70bab8c5 Add Oxlint configuration and integrate linting commands
- Introduced a new `.oxlintrc.json` file to configure Oxlint for code quality checks.
- Updated `package.json` to include linting scripts (`lint` and `lint:fix`) for easier code maintenance.
- Enhanced documentation in `README` files to guide users on running Oxlint checks and applying fixes.
2026-03-12 22:40:18 +08:00
discountry 055bb445a9 fix denpendence 2026-03-01 23:47:52 +08:00
discountry 74d0a0a98b Update package.json and README files to include repository information and enhance CLI command mode documentation
- Added repository details to package.json for better project visibility.
- Revised README files in both English and Chinese to improve the clarity and accessibility of the `ritmex-bot` CLI command mode instructions.
- Included installation instructions for adding the project as a skill, enhancing user onboarding experience.
2026-02-27 12:55:28 +08:00
discountry 674a6fe0da Add CLI user guides and .npmignore file
- Introduced English and Chinese user guides for the `ritmex-bot` CLI, detailing command usage and options.
- Created a new `.npmignore` file to exclude documentation directories from npm package distribution.
- Updated README files to link to the new user guides directly, ensuring easy access for users.
2026-02-27 12:47:32 +08:00
DisneyandGitHub 4014a86223 Enhance CLI command mode for ritmex-bot (#23)
- Introduced a new command mode for `ritmex-bot`, allowing agent-friendly structured trading operations without entering the Ink interactive menu.
- Updated `package.json` to include versioning and set the project as public with a new CLI entry point.
- Enhanced documentation in both English and Chinese to provide comprehensive usage instructions for the new command mode.
- Added a new executable script for `ritmex-bot` to facilitate command execution.
- Improved error handling and command parsing for better user experience and clarity in command execution.
2026-02-27 12:42:20 +08:00
97 changed files with 5326 additions and 3314 deletions
+2
View File
@@ -36,3 +36,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.tmp .tmp
.tmp/* .tmp/*
.factory
+3
View File
@@ -0,0 +1,3 @@
docs/
.claude/
.cursor/
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [
"coverage/**",
"data/**",
"dist/**",
"docs/**",
"node_modules/**",
"out/**",
".tmp/**"
]
}
+132 -25
View File
@@ -1,35 +1,142 @@
# Repository Guidelines # Repository Guidelines
## Project Structure & Module Organization ## Package Manager
The Bun entry point (`index.ts`) lives at the repo root for quick CLI smoke checks. All production code is under `src/`:
- `src/strategy/` gathers every live trading engine (`maker`, `offset-maker`, `trend`). Shared helpers for strategy wiring sit in `src/strategy/common/`. **必须使用 Bun** — 本项目使用 Bun 作为包管理器和运行时。所有命令都必须用 bun 执行,**不要使用 npm、yarn 或 npx**。
- `src/core/` keeps order coordination plus shared libs used by multiple strategies.
- `src/exchanges/` exposes the adapters and REST/websocket clients.
- `src/ui/` implements the Ink dashboards for each strategy (they remain independent dashboards).
- `src/logging/` contains the trade log helper.
- `src/utils/` and `src/config.ts` hold cross-cutting utilities and runtime config.
- `docs/` stores reference material, while `tests/` contains Vitest suites.
## Build, Test, and Development Commands ## Project Structure
- `bun install` install dependencies.
- `bun run index.ts` launch the CLI menu.
- `bun x vitest run` execute the full test suite; `bun x vitest --watch` for incremental runs.
Strategy-specific scripts still execute through the CLI; there is no separate `legacy/` workspace. 入口 `index.ts` 在仓库根目录,用于 CLI 启动。所有生产代码在 `src/` 下:
## Coding Style & Naming Conventions ```
Use modern TypeScript with ES modules, two-space indentation, and sorted imports (external → internal). Favor `camelCase` for variables/functions and `PascalCase` for classes/enums. Place new strategies under `src/strategy/`, shared utilities under `src/utils/` or `src/strategy/common/` when they only apply to strategies. Keep comments focused on non-obvious trading logic. src/
├── config.ts # 运行时配置(GridConfig、MakerPointsConfig 等)
├── runtime-errors.ts # 全局错误处理
├── index.tsx # Ink 渲染入口
├── cli/ # CLI 参数解析与策略启动
│ ├── args.ts
│ ├── command-executor.ts
│ ├── command-parser.ts
│ ├── command-types.ts
│ └── strategy-runner.ts
├── core/ # 订单协调 + 共享库
│ ├── order-coordinator.ts # 限价单/市价单统一下单(锁、去重、速率控制)
│ └── lib/
│ ├── order-plan.ts
│ ├── orders.ts
│ └── rate-limit.ts
├── exchanges/ # 交易所适配器(每个子目录含 adapter/gateway/order
│ ├── adapter.ts # ExchangeAdapter 接口
│ ├── adapter-utils.ts # 适配器工具函数
│ ├── types.ts # CreateOrderParams、Order、AccountSnapshot 等公共类型
│ ├── order-schema.ts # BaseOrderIntent、LimitOrderIntent 等
│ ├── order-handlers.ts # 通用 order handler 工厂
│ ├── order-router.ts # 按交易所分发订单处理
│ ├── create-adapter.ts # 工厂函数
│ ├── resolve-from-env.ts
│ ├── dry-run-adapter.ts # 模拟适配器
│ ├── aster/ # Aster 交易所
│ ├── backpack/ # Backpack 交易所
│ ├── binance/ # Binance 交易所(CCXT
│ ├── grvt/ # GRVT 交易所
│ ├── lighter/ # Lighter 交易所(含签名/nonce/字节处理)
│ ├── nado/ # Nado 交易所
│ ├── paradex/ # Paradex 交易所
│ └── standx/ # StandX 交易所
├── strategy/ # 所有策略引擎
│ ├── maker-engine.ts # Maker 做市策略
│ ├── maker-points-engine.ts # MakerPoints 积分做市策略
│ ├── maker-points-logic.ts # MakerPoints 纯逻辑(可单独测试)
│ ├── offset-maker-engine.ts # 偏移做市策略
│ ├── trend-engine.ts # 趋势跟踪策略
│ ├── grid-engine.ts # 网格交易策略(LevelState + clientOrderId 恢复)
│ ├── basis-arb-engine.ts # 基差套利策略
│ ├── swing-engine.ts # 波段交易策略
│ ├── swing-logic.ts # 波段纯逻辑
│ ├── guardian-engine.ts # 监控守护策略
│ ├── liquidity-maker-engine.ts # 流动性做市策略
│ └── common/ # 策略共享辅助
│ ├── binance-depth.ts # Binance 深度分析
│ ├── binance-rsi.ts # Binance RSI 指标
│ ├── event-emitter.ts # 策略事件发射器
│ ├── grid-storage.ts # 网格状态磁盘持久化
│ ├── session-volume.ts # 会话成交量统计
│ └── subscriptions.ts # WebSocket 安全订阅
├── ui/ # Ink 仪表盘(每个策略独立 App)
│ ├── App.tsx
│ ├── GridApp.tsx
│ ├── MakerApp.tsx
│ ├── MakerPointsApp.tsx
│ ├── OffsetMakerApp.tsx
│ ├── TrendApp.tsx
│ ├── BasisApp.tsx
│ ├── SwingApp.tsx
│ ├── GuardianApp.tsx
│ ├── LiquidityMakerApp.tsx
│ └── components/
├── logging/ # 交易日志
│ └── trade-log.ts
├── notifications/ # Telegram 通知
│ ├── index.ts
│ ├── telegram.ts
│ └── types.ts
├── i18n/ # 国际化
│ └── index.ts
└── utils/ # 跨领域工具
├── math.ts, format.ts, price.ts, depth.ts
├── errors.ts, risk.ts, pnl.ts
├── strategy.ts, order-status.ts, security.ts
├── copyright.ts
└── standx-token-expiry.ts
```
## Testing Guidelines 辅助目录:
Vitest powers unit/integration tests. Co-locate new tests next to their subject using `<feature>.test.ts`. Strategies should have coverage for order lifecycle, risk guards, and websocket edge cases. Run `bun x vitest --watch` during development for fast feedback. - `tests/` — Vitest 测试套件(大部分测试在此,少量与逻辑共处于 `src/`
- `docs/` — 交易所 API 参考文档
- `scripts/` — 一次性脚本(密钥派生等)
- `skills/` — AI 助手技能配置
## Commit & Pull Request Guidelines ## Commands
Follow lightweight Conventional Commits (e.g. `feat: add hedging status panel`). Scope each commit to a single module or strategy. PRs should include:
- Summary of changes. ```bash
- Validation notes (commands run, environments touched). bun install # 安装依赖
- Relevant logs or screenshots for behavior changes. bun run dev # 启动 CLI 菜单(等同于 bun run index.ts
Link issues/tasks when available. bun run test # 执行全部测试(bun x vitest run
bun x vitest --watch # 增量测试
bun x tsc --noEmit # 类型检查(无 typecheck 脚本,直接用 tsc
bun run lint # oxlint 检查
bun run lint:fix # oxlint 自动修复
```
⚠️ **不要用 `bun test`** — 那是 Bun 内置 runner,不支持 `vi.resetModules()``vi.waitFor()` 等 Vitest API。必须用 `bun run test`
## Coding Style
- 现代 TypeScript + ES modules,严格模式(`strict: true`
- 两空格缩进,imports 按 external → internal 排序
- 变量/函数用 `camelCase`,类/枚举用 `PascalCase`
- 新策略放 `src/strategy/`,策略共享工具放 `src/strategy/common/`
- 跨领域工具放 `src/utils/`
- 注释只写非显而易见的交易逻辑
- JSX 用于 Ink UI 组件(`react-jsx`
## Testing
Vitest 驱动所有测试。测试文件两种位置:
- `tests/<feature>.test.ts` — 大部分测试
- `src/<module>/<feature>.test.ts` — 纯逻辑单元测试(如 `maker-points-logic.test.ts``swing-logic.test.ts`
策略测试应覆盖:订单生命周期、风控守卫、WebSocket 边界情况、状态恢复。
## Commit Guidelines
Conventional Commits`feat:` / `fix:` / `refactor:` / `test:`),每次提交作用域限单个模块或策略。
## Environment & Secrets ## Environment & Secrets
Duplicate `.env.example` to `.env` and populate API keys (Aster/GRVT) before running strategies. Do not commit secrets—use local `.env` or deployment secret managers. Rotate keys if they leak into logs or backups.
复制 `.env.example``.env` 填入 API 密钥。**不要读取、打印或提交 `.env` 和任何密钥。** 泄露立即轮换。
## Safety
- 永远不要读取、运行、打印或访问 `.env` 或任何 secrets
- 代码中遇到项目依赖时,先查版本再查文档,避免过时知识导致错误
-12
View File
@@ -1,12 +0,0 @@
# RitMEX Bot - Claude Instructions
## Package Manager
**必须使用 Bun** - 这个项目使用 Bun 作为包管理器和运行时。所有能用 bun 执行的命令都必须使用 bun:
- 安装依赖: `bun install`
- 运行脚本: `bun run <script>`
- 执行测试: `bun test`
- 类型检查: `bun run typecheck`
**不要使用 npm、yarn 或 npx**
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+37 -12
View File
@@ -8,18 +8,10 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
基于 Bun 的多交易所永续合约量化终端,内置趋势跟随(SMA30)、Guardian 防守与做市策略,支持快速恢复、实时行情订阅、日志追踪与 CLI 仪表盘。 基于 Bun 的多交易所永续合约量化终端,内置趋势跟随(SMA30)、Guardian 防守与做市策略,支持快速恢复、实时行情订阅、日志追踪与 CLI 仪表盘。
## CLI 命令模式(ritmex-bot
`ritmex-bot` 支持 Agent 友好的结构化命令调用,覆盖交易所能力查询、行情、账户、仓位、下单、撤单与策略启动。
- 保持现有环境变量体系,不新增也不改名,只读取当前执行环境中的变量。
- `--symbol` 原样透传,不对交易对做统一改写。
- 支持 `--dry-run` 模拟执行与 `--json` 结构化输出,便于自动化系统集成。
完整文档请见:[ritmex-bot CLI 使用手册(中文)](docs/cli-guide.md)
如果您希望获取优惠并支持本项目,请考虑使用以下注册链接: 如果您希望获取优惠并支持本项目,请考虑使用以下注册链接:
* [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)
@@ -29,12 +21,29 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
* [edgex 手续费优惠注册链接](https://pro.edgex.exchange/referral/BULL) * [edgex 手续费优惠注册链接](https://pro.edgex.exchange/referral/BULL)
* [Paradex 手续费优惠注册链接](https://paradex.io/ref/xingxingjun) * [Paradex 手续费优惠注册链接](https://paradex.io/ref/xingxingjun)
* [Apex 手续费优惠注册链接](https://join.omni.apex.exchange/SEA) * [Apex 手续费优惠注册链接](https://join.omni.apex.exchange/SEA)
* [Hyperliquid 邀请注册链接](https://app.hyperliquid.xyz/join/RITMEX)
## CLI 命令模式(ritmex-bot
`ritmex-bot` 支持 Agent 友好的结构化命令调用,覆盖交易所能力查询、行情、账户、仓位、下单、撤单与策略启动。
- 保持现有环境变量体系,不新增也不改名,只读取当前执行环境中的变量。
- `--symbol` 原样透传,不对交易对做统一改写。
- 支持 `--dry-run` 模拟执行与 `--json` 结构化输出,便于自动化系统集成。
### 安装当前项目 Skillskills add
```bash
npx skills add https://github.com/discountry/ritmex-bot --skill use-ritmex-bot
```
如需指定分支,可追加 `--ref <branch-or-tag>`
完整文档请见:[ritmex-bot CLI 使用手册(中文)](cli-guide.md)
## 文档索引 ## 文档索引
- [ritmex-bot CLI 使用手册(中文)](docs/cli-guide.md) - [ritmex-bot CLI 使用手册(中文)](cli-guide.md)
- [ritmex-bot CLI User Guide (English)](docs/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 自动同步账户、挂单与仓位,断线后自动恢复。
@@ -53,6 +62,7 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
| 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` 命令)
@@ -97,7 +107,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) |
@@ -154,6 +164,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)
@@ -220,6 +241,8 @@ EXCHANGE=binance BINANCE_MARKET_TYPE=spot BINANCE_SYMBOL=BTCUSDT bun run index.t
bun run index.ts # 启动 CLI(默认入口) bun run index.ts # 启动 CLI(默认入口)
bun run start # 等价于运行 index.ts bun run start # 等价于运行 index.ts
bun run dev # 调试模式 bun run dev # 调试模式
bun run lint # 执行 Oxlint 检查
bun run lint:fix # 自动修复可安全修复的问题
bun x vitest run # 执行全部测试 bun x vitest run # 执行全部测试
``` ```
@@ -286,6 +309,8 @@ bun run pm2:start:offset
## 测试 ## 测试
项目使用 Vitest 项目使用 Vitest
```bash ```bash
bun run lint
bun run lint:fix
bun run test bun run test
bun x vitest --watch bun x vitest --watch
``` ```
+37 -12
View File
@@ -4,18 +4,10 @@
A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend engine, a Guardian stop sentinel, and two market-making modes. It offers instant restarts, realtime market data, structured logging, and an Ink-based CLI dashboard. A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend engine, a Guardian stop sentinel, and two market-making modes. It offers instant restarts, realtime market data, structured logging, and an Ink-based CLI dashboard.
## CLI Command Mode (`ritmex-bot`)
`ritmex-bot` provides an agent-friendly command interface for exchange capability checks, market data, account/position queries, order operations, and strategy execution.
- It keeps the current environment-variable system intact: no renaming and no new required keys.
- `--symbol` is passed through exactly as provided (no symbol normalization).
- It supports `--dry-run` simulation and `--json` structured output for automation.
Full guide: [ritmex-bot CLI User Guide (English)](docs/cli-guide.en.md)
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)
@@ -25,12 +17,29 @@ If you'd like to support this project and get fee discounts, please consider usi
* [edgex referral link](https://pro.edgex.exchange/referral/BULL) * [edgex referral link](https://pro.edgex.exchange/referral/BULL)
* [Paradex referral link](https://paradex.io/ref/xingxingjun) * [Paradex referral link](https://paradex.io/ref/xingxingjun)
* [Apex referral link](https://join.omni.apex.exchange/SEA) * [Apex referral link](https://join.omni.apex.exchange/SEA)
* [Hyperliquid invite link](https://app.hyperliquid.xyz/join/RITMEX)
## CLI Command Mode (`ritmex-bot`)
`ritmex-bot` provides an agent-friendly command interface for exchange capability checks, market data, account/position queries, order operations, and strategy execution.
- It keeps the current environment-variable system intact: no renaming and no new required keys.
- `--symbol` is passed through exactly as provided (no symbol normalization).
- It supports `--dry-run` simulation and `--json` structured output for automation.
### Install This Project Skill (`skills add`)
```bash
npx skills add https://github.com/discountry/ritmex-bot --skill use-ritmex-bot
```
If you need a specific branch/tag, append `--ref <branch-or-tag>`.
Full guide: [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
## Documentation Map ## Documentation Map
- [ritmex-bot CLI User Guide (English)](docs/cli-guide.en.md) - [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
- [ritmex-bot CLI 使用手册(中文)](docs/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.
@@ -49,6 +58,7 @@ If you'd like to support this project and get fee discounts, please consider usi
| 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)
@@ -93,7 +103,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 |
@@ -151,6 +161,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)
@@ -216,6 +237,8 @@ Please keep these credentials safe and do not share them with anyone.
bun run index.ts # Launch the CLI (default entrypoint) bun run index.ts # Launch the CLI (default entrypoint)
bun run start # Alias for bun run index.ts bun run start # Alias for bun run index.ts
bun run dev # Development entrypoint bun run dev # Development entrypoint
bun run lint # Run Oxlint checks
bun run lint:fix # Apply safe Oxlint fixes
bun x vitest run # Execute the full Vitest suite bun x vitest run # Execute the full Vitest suite
``` ```
@@ -282,6 +305,8 @@ Run `pm2 save` afterwards if you want the process list to survive reboots.
## Testing ## Testing
Powered by Vitest: Powered by Vitest:
```bash ```bash
bun run lint
bun run lint:fix
bun run test bun run test
bun x vitest --watch bun x vitest --watch
``` ```
+106 -105
View File
@@ -5,26 +5,27 @@
"": { "": {
"name": "ritmex-bot", "name": "ritmex-bot",
"dependencies": { "dependencies": {
"@grvt/client": "^1.6.4", "@grvt/client": "^1.6.27",
"@nadohq/client": "^0.1.0-alpha.41", "@nadohq/client": "^0.1.0-alpha.51",
"@noble/ed25519": "^3.0.0", "@noble/ed25519": "^3.0.0",
"axios": "^1.12.2", "axios": "^1.13.6",
"bignumber.js": "^9.3.1", "bignumber.js": "^10.0.2",
"ccxt": "^4.5.12", "ccxt": "^4.5.40",
"dotenv": "^17.2.2", "dotenv": "^17.3.1",
"ethereum-cryptography": "^2.1.3", "ethereum-cryptography": "^3.2.0",
"ink": "^6.3.1", "ink": "^6.8.0",
"react": "^19.1.1", "react": "^19.2.4",
"trading-signals": "^7.4.3", "trading-signals": "^7.4.3",
"viem": "^2.43.1", "viem": "^2.46.3",
"ws": "^8.18.3", "ws": "^8.19.0",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "^1.3.9",
"vitest": "^3.2.4", "oxlint": "^1.54.0",
"vitest": "^4.0.18",
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5", "typescript": "^5.9.3",
}, },
}, },
}, },
@@ -85,27 +86,65 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
"@grvt/client": ["@grvt/client@1.6.25", "", { "dependencies": { "axios": "^1.13.2" } }, "sha512-LG3oZSJDq1Qz6mLiHDmg+v/Hp+Nd+yOp8GIvkBVyIdOcPVzuzC8UQD7IxvS+g5EfQQKLiKzXSjKLc39a11Uj1A=="], "@grvt/client": ["@grvt/client@1.6.27", "", { "dependencies": { "axios": "^1.13.5" } }, "sha512-YUb7bsPPRNhZs/T+RGDBp/AKbegRHvjB8KXpTEAy5Dbf44RI00KoX9S/R12HzxLtS5rSblmcpD7hmCtDnlcN5Q=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@nadohq/client": ["@nadohq/client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/indexer-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "@nadohq/trigger-client": "^0.1.0-alpha.45", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-63S7eT5xSk3hxEXRLGDI94mPeu/zd1Nu4oUOZrY8wzhaT7Gl7PVP2snb8JH/PduVQzgs7+qblr4aTBI/j6QvLQ=="], "@nadohq/client": ["@nadohq/client@0.1.0-alpha.51", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.51", "@nadohq/indexer-client": "0.1.0-alpha.51", "@nadohq/shared": "0.1.0-alpha.51", "@nadohq/trigger-client": "0.1.0-alpha.51", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-t94DGAbnGPjHu517NlYixtzhsBmavv+Hu4OgIKy4WEbOwSWT0NZJRLVqhzfmsZQ1fzg/z0PGss3Z59v9fVjfgg=="],
"@nadohq/engine-client": ["@nadohq/engine-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-GqaRrsB0Z2EgNY6wu4nG5OTc5hF+buNRMc0+jPLlOQonlPJAC4OiMCgCigm4nJecYwsdiQD05W3Z4YUqggxbyw=="], "@nadohq/engine-client": ["@nadohq/engine-client@0.1.0-alpha.51", "", { "dependencies": { "@nadohq/shared": "0.1.0-alpha.51", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-gP0IHFOWq+IXMOkURVXA+bb9yh4qc+rCXy210mwmETAVT7h0kvcIij+fU6/k9nMl2G4EaGRzfocKpn3OTb6Hlw=="],
"@nadohq/indexer-client": ["@nadohq/indexer-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-8PzG9taYh380lsKT8i4UvGfhZiPpt+zPisx+ErVqMifyhcJAhgMeYNcfijwMQFNJdgTaBvtI0R5JK98l4Y0YQw=="], "@nadohq/indexer-client": ["@nadohq/indexer-client@0.1.0-alpha.51", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.51", "@nadohq/shared": "0.1.0-alpha.51", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-4ZJTwUOxKpYKi8TGnMRscuX3tkzzOJR7Uaqs4Se+BrE48POinaMd8SnJWu7KEvWNc6JnVwb/p5LWHLEsPWtC7A=="],
"@nadohq/shared": ["@nadohq/shared@0.1.0-alpha.45", "", { "peerDependencies": { "bignumber.js": "^9.3.0", "viem": "*" } }, "sha512-4Ml8dO9mkwnIidflpunwVMLX2KJA79b53T9xTBkH1Yzy0iOnbEE27r25wvgJgyszU0aznbPBv2++FZ0d0B5Zhg=="], "@nadohq/shared": ["@nadohq/shared@0.1.0-alpha.51", "", { "dependencies": { "abitype": "^1.2.3" }, "peerDependencies": { "bignumber.js": "^9.3.0", "viem": "*" } }, "sha512-889WGX2WcSgYpXfLx1TvStzyo/kkx19pcgn0ZebBcUHnYM+xqXJH+ty+BuFaheP+IkPfWfuS9S3Q757j04R4rg=="],
"@nadohq/trigger-client": ["@nadohq/trigger-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-Xt8W9zxOZhMvoXSRnx9kUR26wWuHc3EgTVTBbUXt7SlI9v3IP9I3idS6SU3j2f08flcvNqRmC2EbXyYDC2XeyQ=="], "@nadohq/trigger-client": ["@nadohq/trigger-client@0.1.0-alpha.51", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.51", "@nadohq/shared": "0.1.0-alpha.51", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-OmM6opQy6E2lvZqP4IDf/BF1r0KogJIEZezQ8zRJROtuKYxHnSOcItnlS1vihxp9FR7Y25l/tnQ1JxoWmZnwSw=="],
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="], "@noble/curves": ["@noble/curves@1.9.0", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg=="],
"@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="], "@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="],
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-khWlVXUa4CPvp4eXnj7/TUNeyiarwvTEmZggylPIPUCSWgPqUBFGElIBa9xKNCQt4pb+WSGArCNEAy22ekQQxg=="],
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-p3qEVSDVmyducpI9ORTJNbaMyXfICidDXGaf3WwyDyiXPExyZdfc6UsPepGPxImlfFJs5kxOCPqP4ut12Ed9pg=="],
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tUQ+vn/AL2P2Rz6cl3gsFA+4pMBFMRR0e6AVePoezV9iGTSMZUXIf0YWHq203bwNDXcY04vSXDqPB6E1b9WULA=="],
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-9tzH/OLGZCzVGEXW0D3I0OrQOOWciHYWV9mcxjqqQE6f1DMOXT23mWj5/OQtqhW3E1VfHf0uf2MzMbM2gboPlA=="],
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XelGa+x7vC9epF5JzXWHaQtdxqcL5R+H2WcEkr+JDkfmY3dAXVLa+51qW3+MWt7CZanakaOyhraLzdM32pWFfg=="],
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t5FubAU899vMF+6rBtxDewkwSAeFn8OUCKb/+jJ1XqPU0FTR+RffnTfXafVE8WXw39eN4pzUK3rkxUWlcsFuaw=="],
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Du6cwkoMRi9DAgITzQpzS1QaEqE+5BP3Tfgr1yPy3C034n6IHISCwf4KMB+wojg8FE/kYvrJDibT0ENdcR3jUg=="],
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4PjJ1lfghsVBuAegW79jkvUWbN8P4F28KaKbM72dW6ZWTYMg6M0pkTilXFZ+q/mb8f0MEyR1rO16useoa6KTA=="],
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WBNFe1foIFg6ipgzjzJfDLn7C5nZpLr/UHlnlT7GI3scCD2xpJiJTGMTTpJqA8p0Q1l2NsEnAOj9PtreF0Tkzg=="],
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-M+UAW76rZHHiYKKy4e7Te5MNikANiFhYWl0qYF1MTIhQczYIqWVDQ+SX0SzW8ipOB/oK3+enOvlvJuhMoA968Q=="],
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-sGassljr8TR5F1WCqOaacrp3mYi107MnjkqlSXOMHACps7U2bL4v7M3RY7P7NMcGkNhzRHF3VO5+XyPWhTVM6w=="],
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-mW5Z6XTO8QWtlUjlf6yjntarjnbrmGVSK/V6XYy2rjH8xUfv9pn9G1vO92DBBBi0eUwnVpcOY3hMkP851WZNWg=="],
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-nfPmEGW9BfWEUD0PVXGaYa2RS4wVblofNih1gsyUoLSWSyliNisIWd7F+QXrDSL5SJ78BPZjDW6FainV8qRiTg=="],
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gybxMQx4NN1T+pa8TLwgVS4u9H9Hxwm7Sl0qvnQJF2WRhNN1oTOCzkmmCBbW/29DYLeV99WU76zh7srCamK9yw=="],
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EAXMh2w3pzSj/aiB67kXzcHIoKxAywC1i1IgxA1sLY7iffEaZowDTOJgirY7WxeR/WiiKwak89gPeh9wUdLnIw=="],
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Ih7CfITkbw86+LjgV5gDwNmNQlvz7X+51gdeLLUDwuA/9lojDxCmRQEzeMl44KStQlwzCuNIcgGMae0MllV5ww=="],
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qGQ8KIEfdgwwJd09Nc0d5hzlHQAM0EMoshMzHG/nNlMAHXrY7RFyat6VbzjflifAFb7LPAw+Yw9AB8cfzR7L8g=="],
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-GXos7N5oZIcIE8hlXgtUkI+y3frIqwiztx9p4+ZrpNgASbnIsxxy8bgLJqXE2SGCAdmtjBO1mdKhGTtVrb1jbg=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DVxSELcI72lmcODuVpO8uXkwuYPiA0hKMhBwNa8fO4+sIVxFvxugf/z6qrL+4PM8RjGWjdl8O/QqzuRWj9b5bA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="],
@@ -151,72 +190,64 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="],
"@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"@scure/bip32": ["@scure/bip32@1.4.0", "", { "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg=="], "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="],
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="], "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
"@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
"@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
"ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
"axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="], "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "bignumber.js": ["bignumber.js@10.0.2", "", {}, "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"ccxt": ["ccxt@4.5.35", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-aFn1jq/vR9wD3Vxu/2UFoS8snqlvfYn/iYrNhyZZfzB3N+kAHhP5+sAgO6ZwNHkHiUT9IPPahC1ZSFdPtjpIYA=="], "ccxt": ["ccxt@4.5.40", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-8nJhhNKft7+fELDNQuCV2AQZBxLJfYQaD2LpqxHrucSszosyVl0u6lCIFfcnDUbeUW925uhrsKevHf2fNbLjKw=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
@@ -229,13 +260,9 @@
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -261,7 +288,7 @@
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], "ethereum-cryptography": ["ethereum-cryptography@3.2.0", "", { "dependencies": { "@noble/ciphers": "1.3.0", "@noble/curves": "1.9.0", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0" } }, "sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg=="],
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
@@ -271,7 +298,7 @@
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -293,7 +320,7 @@
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"ink": ["ink@6.6.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ=="], "ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
@@ -301,11 +328,7 @@
"isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -315,20 +338,20 @@
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"ox": ["ox@0.11.3", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw=="], "ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="],
"oxlint": ["oxlint@1.54.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.54.0", "@oxlint/binding-android-arm64": "1.54.0", "@oxlint/binding-darwin-arm64": "1.54.0", "@oxlint/binding-darwin-x64": "1.54.0", "@oxlint/binding-freebsd-x64": "1.54.0", "@oxlint/binding-linux-arm-gnueabihf": "1.54.0", "@oxlint/binding-linux-arm-musleabihf": "1.54.0", "@oxlint/binding-linux-arm64-gnu": "1.54.0", "@oxlint/binding-linux-arm64-musl": "1.54.0", "@oxlint/binding-linux-ppc64-gnu": "1.54.0", "@oxlint/binding-linux-riscv64-gnu": "1.54.0", "@oxlint/binding-linux-riscv64-musl": "1.54.0", "@oxlint/binding-linux-s390x-gnu": "1.54.0", "@oxlint/binding-linux-x64-gnu": "1.54.0", "@oxlint/binding-linux-x64-musl": "1.54.0", "@oxlint/binding-openharmony-arm64": "1.54.0", "@oxlint/binding-win32-arm64-msvc": "1.54.0", "@oxlint/binding-win32-ia32-msvc": "1.54.0", "@oxlint/binding-win32-x64-msvc": "1.54.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-ObSjVwf0ZYA5U5Cmelj0PsCuqCJXsm2TxZ40tgUSAY7Wu0lKAsNjor6cgXHXSys8jOwv1ICjtzouoWHdKGHbZg=="],
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
@@ -351,7 +374,7 @@
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -359,47 +382,43 @@
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="], "string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
"trading-signals": ["trading-signals@7.4.3", "", {}, "sha512-kOyzd85qhuhU7yGkB5z74MVP6j30xBCfZy5+bgBnvfxY1ZdvaQsdD+C5j+CoIRtCAgSOuxB7jmPpBrqO6h17sQ=="], "trading-signals": ["trading-signals@7.4.3", "", {}, "sha512-kOyzd85qhuhU7yGkB5z74MVP6j30xBCfZy5+bgBnvfxY1ZdvaQsdD+C5j+CoIRtCAgSOuxB7jmPpBrqO6h17sQ=="],
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
"viem": ["viem@2.45.1", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.11.3", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA=="], "viem": ["viem@2.46.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.12.4", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg=="],
"vite": ["vite@7.1.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA=="], "vite": ["vite@7.1.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
@@ -407,34 +426,16 @@
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"@scure/bip32/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
"cli-truncate/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], "ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
"ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"ox/@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="],
"ox/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
"viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], "viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
"viem/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"viem/@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="],
"viem/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
"viem/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "viem/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"ox/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"ox/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"viem/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"viem/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
} }
} }
View File
+58
View File
@@ -0,0 +1,58 @@
# Ondo Perps 接入说明
## 链接
- 邀请注册:https://app.ondoperps.xyz/?ref=4A3ACQ
- 官方文档索引:https://ondoperps.mintlify.app/llms.txt
- REST OpenAPIhttps://docs.ondoperps.xyz/api-reference/rest-spec.json
- WebSocket OpenAPIhttps://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 订单费率,适配器限制在 110 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 只授予策略需要的权限。
+19 -12
View File
@@ -4,12 +4,18 @@
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"private": false, "private": false,
"repository": {
"type": "git",
"url": "https://github.com/discountry/ritmex-bot.git"
},
"bin": { "bin": {
"ritmex-bot": "./bin/ritmex-bot" "ritmex-bot": "./bin/ritmex-bot"
}, },
"scripts": { "scripts": {
"dev": "bun run index.ts", "dev": "bun run index.ts",
"start": "bun run index.ts", "start": "bun run index.ts",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"test": "bun x vitest run", "test": "bun x vitest run",
"test:exchange-contract": "bun x vitest run tests/exchange-contract-suite.test.ts tests/exchange-factory.test.ts tests/config.test.ts", "test:exchange-contract": "bun x vitest run tests/exchange-contract-suite.test.ts tests/exchange-factory.test.ts tests/config.test.ts",
"test:watch": "bun x vitest", "test:watch": "bun x vitest",
@@ -22,25 +28,26 @@
"pm2:start:maker-points": "pm2 start bun --name ritmex-maker-points --cwd . --restart-delay 5000 -- run index.ts --strategy maker-points --exchange standx --silent" "pm2:start:maker-points": "pm2 start bun --name ritmex-maker-points --cwd . --restart-delay 5000 -- run index.ts --strategy maker-points --exchange standx --silent"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "^1.3.9",
"vitest": "^3.2.4" "oxlint": "^1.54.0",
"vitest": "^4.0.18"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.9.2" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"@grvt/client": "^1.6.25", "@grvt/client": "^1.6.27",
"@nadohq/client": "^0.1.0-alpha.45", "@nadohq/client": "^0.1.0-alpha.51",
"@noble/ed25519": "^3.0.0", "@noble/ed25519": "^3.0.0",
"axios": "^1.13.4", "axios": "^1.13.6",
"bignumber.js": "^9.3.1", "bignumber.js": "^10.0.2",
"ccxt": "^4.5.35", "ccxt": "^4.5.40",
"dotenv": "^17.2.3", "dotenv": "^17.3.1",
"ethereum-cryptography": "^2.2.1", "ethereum-cryptography": "^3.2.0",
"ink": "^6.6.0", "ink": "^6.8.0",
"react": "^19.2.4", "react": "^19.2.4",
"trading-signals": "^7.4.3", "trading-signals": "^7.4.3",
"viem": "^2.45.1", "viem": "^2.46.3",
"ws": "^8.19.0" "ws": "^8.19.0"
} }
} }
+2
View File
@@ -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";
} }
} }
+14 -5
View File
@@ -15,7 +15,7 @@ import {
routeTrailingStopOrder, routeTrailingStopOrder,
} from "../exchanges/order-router"; } from "../exchanges/order-router";
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env"; import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
import type { AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../exchanges/types"; import type { Depth, Kline, Order, Ticker } from "../exchanges/types";
import { startStrategy } from "./strategy-runner"; import { startStrategy } from "./strategy-runner";
import type { import type {
CommandErrorPayload, CommandErrorPayload,
@@ -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 {
@@ -266,7 +275,7 @@ async function handleMarketTicker(
buildAdapterFromEnvFn: typeof buildAdapterFromEnv buildAdapterFromEnvFn: typeof buildAdapterFromEnv
): Promise<unknown> { ): Promise<unknown> {
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn); const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
const ticker = await waitForFirst<AsterTicker>( const ticker = await waitForFirst<Ticker>(
(cb) => adapter.watchTicker(symbol, cb), (cb) => adapter.watchTicker(symbol, cb),
command.timeoutMs, command.timeoutMs,
"market ticker" "market ticker"
@@ -279,7 +288,7 @@ async function handleMarketDepth(
buildAdapterFromEnvFn: typeof buildAdapterFromEnv buildAdapterFromEnvFn: typeof buildAdapterFromEnv
): Promise<unknown> { ): Promise<unknown> {
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn); const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
const depth = await waitForFirst<AsterDepth>( const depth = await waitForFirst<Depth>(
(cb) => adapter.watchDepth(symbol, cb), (cb) => adapter.watchDepth(symbol, cb),
command.timeoutMs, command.timeoutMs,
"market depth" "market depth"
@@ -300,7 +309,7 @@ async function handleMarketKline(
buildAdapterFromEnvFn: typeof buildAdapterFromEnv buildAdapterFromEnvFn: typeof buildAdapterFromEnv
): Promise<unknown> { ): Promise<unknown> {
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn); const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
const klines = await waitForFirst<AsterKline[]>( const klines = await waitForFirst<Kline[]>(
(cb) => adapter.watchKlines(symbol, command.interval, cb), (cb) => adapter.watchKlines(symbol, command.interval, cb),
command.timeoutMs, command.timeoutMs,
"market kline" "market kline"
@@ -379,7 +388,7 @@ async function handleOrderCreate(
timeInForce: payload.timeInForce, timeInForce: payload.timeInForce,
}; };
let order: AsterOrder; let order: Order;
switch (payload.type) { switch (payload.type) {
case "limit": case "limit":
order = await routeLimitOrder({ order = await routeLimitOrder({
+2 -1
View File
@@ -155,7 +155,7 @@ function parseExchangeCommand(
options: Record<string, string | boolean>, options: Record<string, string | boolean>,
common: CommandCommonOptions & { help?: boolean } common: CommandCommonOptions & { help?: boolean }
): ParsedCliCommand { ): ParsedCliCommand {
assertAllowedOptions(options, new Set([...GLOBAL_OPTION_NAMES])); assertAllowedOptions(options, new Set(GLOBAL_OPTION_NAMES));
if (action === "list") return { kind: "exchange-list", ...common }; if (action === "list") return { kind: "exchange-list", ...common };
if (action === "capabilities") return { kind: "exchange-capabilities", ...common }; if (action === "capabilities") return { kind: "exchange-capabilities", ...common };
throw new CommandParseError(`Unsupported exchange action '${action}'`); throw new CommandParseError(`Unsupported exchange action '${action}'`);
@@ -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}'`);
} }
+1
View File
@@ -98,6 +98,7 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
nado: { envKeys: ["NADO_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-PERP" }, 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 {
+4 -4
View File
@@ -1,4 +1,4 @@
import type { AsterOrder } from "../../exchanges/types"; import type { Order } from "../../exchanges/types";
export interface OrderTarget { export interface OrderTarget {
side: "BUY" | "SELL"; side: "BUY" | "SELL";
@@ -8,11 +8,11 @@ export interface OrderTarget {
} }
export function makeOrderPlan( export function makeOrderPlan(
openOrders: AsterOrder[], openOrders: Order[],
targets: OrderTarget[] targets: OrderTarget[]
): { toCancel: AsterOrder[]; toPlace: OrderTarget[] } { ): { toCancel: Order[]; toPlace: OrderTarget[] } {
const unmatched = new Set(targets.map((_, idx) => idx)); const unmatched = new Set(targets.map((_, idx) => idx));
const toCancel: AsterOrder[] = []; const toCancel: Order[] = [];
for (const order of openOrders) { for (const order of openOrders) {
const orderPrice = String(order.price); const orderPrice = String(order.price);
+2 -2
View File
@@ -1,11 +1,11 @@
import type { ExchangeAdapter } from "../../exchanges/adapter"; import type { ExchangeAdapter } from "../../exchanges/adapter";
import type { AsterOrder } from "../../exchanges/types"; import type { Order } from "../../exchanges/types";
import { isUnknownOrderError } from "../../utils/errors"; import { isUnknownOrderError } from "../../utils/errors";
export async function safeCancelOrder( export async function safeCancelOrder(
exchange: ExchangeAdapter, exchange: ExchangeAdapter,
symbol: string, symbol: string,
order: AsterOrder, order: Order,
onResolved: (orderId: number | string) => void, onResolved: (orderId: number | string) => void,
onUnknown: () => void, onUnknown: () => void,
onError: (err: unknown) => void onError: (err: unknown) => void
+13 -11
View File
@@ -1,5 +1,5 @@
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { AsterOrder } from "../exchanges/types"; import type { Order } from "../exchanges/types";
import { import {
routeCloseOrder, routeCloseOrder,
routeLimitOrder, routeLimitOrder,
@@ -88,7 +88,7 @@ export function unlockOperating(
export async function deduplicateOrders( export async function deduplicateOrders(
adapter: ExchangeAdapter, adapter: ExchangeAdapter,
symbol: string, symbol: string,
openOrders: AsterOrder[], openOrders: Order[],
locks: OrderLockMap, locks: OrderLockMap,
timers: OrderTimerMap, timers: OrderTimerMap,
pendings: OrderPendingMap, pendings: OrderPendingMap,
@@ -134,12 +134,13 @@ type PlaceOrderOptions = {
skipDedupe?: boolean; skipDedupe?: boolean;
slPrice?: number; slPrice?: number;
tpPrice?: number; tpPrice?: number;
clientOrderId?: string;
}; };
export async function placeOrder( export async function placeOrder(
adapter: ExchangeAdapter, adapter: ExchangeAdapter,
symbol: string, symbol: string,
openOrders: AsterOrder[], openOrders: Order[],
locks: OrderLockMap, locks: OrderLockMap,
timers: OrderTimerMap, timers: OrderTimerMap,
pendings: OrderPendingMap, pendings: OrderPendingMap,
@@ -150,7 +151,7 @@ export async function placeOrder(
reduceOnly = false, reduceOnly = false,
guard?: OrderGuardOptions, guard?: OrderGuardOptions,
opts?: PlaceOrderOptions opts?: PlaceOrderOptions
): Promise<AsterOrder | undefined> { ): Promise<Order | undefined> {
const type = "LIMIT"; const type = "LIMIT";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
const priceNum = Number(price); const priceNum = Number(price);
@@ -180,6 +181,7 @@ export async function placeOrder(
closePosition, closePosition,
slPrice: opts?.slPrice, slPrice: opts?.slPrice,
tpPrice: opts?.tpPrice, tpPrice: opts?.tpPrice,
clientOrderId: opts?.clientOrderId,
}); });
pendings[type] = String(order.orderId); pendings[type] = String(order.orderId);
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${opts?.slPrice ? ` sl=${opts.slPrice}` : ""}`); log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${opts?.slPrice ? ` sl=${opts.slPrice}` : ""}`);
@@ -197,7 +199,7 @@ export async function placeOrder(
export async function placeMarketOrder( export async function placeMarketOrder(
adapter: ExchangeAdapter, adapter: ExchangeAdapter,
symbol: string, symbol: string,
openOrders: AsterOrder[], openOrders: Order[],
locks: OrderLockMap, locks: OrderLockMap,
timers: OrderTimerMap, timers: OrderTimerMap,
pendings: OrderPendingMap, pendings: OrderPendingMap,
@@ -207,7 +209,7 @@ export async function placeMarketOrder(
reduceOnly = false, reduceOnly = false,
guard?: OrderGuardOptions, guard?: OrderGuardOptions,
opts?: { qtyStep: number } opts?: { qtyStep: number }
): Promise<AsterOrder | undefined> { ): Promise<Order | undefined> {
const type = "MARKET"; const type = "MARKET";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return; if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
@@ -247,7 +249,7 @@ export async function placeMarketOrder(
export async function placeStopLossOrder( export async function placeStopLossOrder(
adapter: ExchangeAdapter, adapter: ExchangeAdapter,
symbol: string, symbol: string,
openOrders: AsterOrder[], openOrders: Order[],
locks: OrderLockMap, locks: OrderLockMap,
timers: OrderTimerMap, timers: OrderTimerMap,
pendings: OrderPendingMap, pendings: OrderPendingMap,
@@ -258,7 +260,7 @@ export async function placeStopLossOrder(
log: LogHandler, log: LogHandler,
guard?: OrderGuardOptions, guard?: OrderGuardOptions,
opts?: { priceTick: number; qtyStep: number } opts?: { priceTick: number; qtyStep: number }
): Promise<AsterOrder | undefined> { ): Promise<Order | undefined> {
const type = "STOP_MARKET"; const type = "STOP_MARKET";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return; if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
@@ -314,7 +316,7 @@ export async function placeStopLossOrder(
export async function placeTrailingStopOrder( export async function placeTrailingStopOrder(
adapter: ExchangeAdapter, adapter: ExchangeAdapter,
symbol: string, symbol: string,
openOrders: AsterOrder[], openOrders: Order[],
locks: OrderLockMap, locks: OrderLockMap,
timers: OrderTimerMap, timers: OrderTimerMap,
pendings: OrderPendingMap, pendings: OrderPendingMap,
@@ -325,7 +327,7 @@ export async function placeTrailingStopOrder(
log: LogHandler, log: LogHandler,
guard?: OrderGuardOptions, guard?: OrderGuardOptions,
opts?: { priceTick: number; qtyStep: number } opts?: { priceTick: number; qtyStep: number }
): Promise<AsterOrder | undefined> { ): Promise<Order | undefined> {
const type = "TRAILING_STOP_MARKET"; const type = "TRAILING_STOP_MARKET";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
if (!adapter.supportsTrailingStops()) { if (!adapter.supportsTrailingStops()) {
@@ -375,7 +377,7 @@ export async function placeTrailingStopOrder(
export async function marketClose( export async function marketClose(
adapter: ExchangeAdapter, adapter: ExchangeAdapter,
symbol: string, symbol: string,
openOrders: AsterOrder[], openOrders: Order[],
locks: OrderLockMap, locks: OrderLockMap,
timers: OrderTimerMap, timers: OrderTimerMap,
pendings: OrderPendingMap, pendings: OrderPendingMap,
+91
View File
@@ -0,0 +1,91 @@
import { extractMessage } from "../utils/errors";
/**
* Wrap a callback so exceptions inside it are swallowed and logged.
* Used by adapter watch* methods to prevent gateway callback errors
* from killing the event loop.
*/
export function createSafeInvoke(adapterName: string) {
return function safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[${adapterName}] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
};
}
export interface InitManager {
ensureInitialized(context?: string): Promise<void>;
}
/**
* Creates a reusable init-once-with-retry manager.
* Every adapter uses the same pattern: cache the init promise, retry on
* failure with exponential back-off, and deduplicate context error logs.
*/
export function createInitManager(
adapterName: string,
doInitialize: () => Promise<void>,
): InitManager {
let initPromise: Promise<void> | null = null;
const initContexts = new Set<string>();
let retryTimer: ReturnType<typeof setTimeout> | null = null;
let retryDelayMs = 3000;
let lastInitErrorAt = 0;
function clearRetry(): void {
if (retryTimer) {
clearTimeout(retryTimer);
retryTimer = null;
}
retryDelayMs = 3000;
}
function handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - lastInitErrorAt < 5000) return;
lastInitErrorAt = now;
console.error(`[${adapterName}] ${context} failed`, error);
}
function scheduleRetry(): void {
if (retryTimer) return;
retryTimer = setTimeout(() => {
retryTimer = null;
if (initPromise) return;
retryDelayMs = Math.min(retryDelayMs * 2, 60_000);
void ensureInitialized("retry");
}, retryDelayMs);
}
function ensureInitialized(context?: string): Promise<void> {
if (!initPromise) {
initContexts.clear();
initPromise = doInitialize()
.then((value) => {
clearRetry();
return value;
})
.catch((error) => {
handleInitError("initialize", error);
initPromise = null;
scheduleRetry();
throw error;
});
}
if (context && !initContexts.has(context)) {
initContexts.add(context);
initPromise.catch((error) => {
handleInitError(context, error);
scheduleRetry();
});
}
return initPromise;
}
return { ensureInitialized };
}
+14 -14
View File
@@ -1,30 +1,30 @@
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterOrder, Order,
AsterDepth, Depth,
AsterTicker, Ticker,
AsterKline, Kline,
CreateOrderParams, CreateOrderParams,
} from "./types"; } from "./types";
export interface AccountListener { export interface AccountListener {
(snapshot: AsterAccountSnapshot): void; (snapshot: AccountSnapshot): void;
} }
export interface OrderListener { export interface OrderListener {
(orders: AsterOrder[]): void; (orders: Order[]): void;
} }
export interface DepthListener { export interface DepthListener {
(depth: AsterDepth): void; (depth: Depth): void;
} }
export interface TickerListener { export interface TickerListener {
(ticker: AsterTicker): void; (ticker: Ticker): void;
} }
export interface KlineListener { export interface KlineListener {
(klines: AsterKline[]): void; (klines: Kline[]): void;
} }
export interface FundingRateSnapshot { export interface FundingRateSnapshot {
@@ -72,18 +72,18 @@ export interface ExchangeAdapter {
watchTicker(symbol: string, cb: TickerListener): void; watchTicker(symbol: string, cb: TickerListener): void;
watchKlines(symbol: string, interval: string, cb: KlineListener): void; watchKlines(symbol: string, interval: string, cb: KlineListener): void;
watchFundingRate?(symbol: string, cb: FundingRateListener): void; watchFundingRate?(symbol: string, cb: FundingRateListener): void;
createOrder(params: CreateOrderParams): Promise<AsterOrder>; createOrder(params: CreateOrderParams): Promise<Order>;
cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>; cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>;
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>; cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
cancelAllOrders(params: { symbol: string }): Promise<void>; cancelAllOrders(params: { symbol: string }): Promise<void>;
getPrecision?(): Promise<ExchangePrecision | null>; getPrecision?(): Promise<ExchangePrecision | null>;
// 连接保护相关方法(可选,仅 StandX 支持) // Connection protection methods (optional)
onConnectionEvent?(listener: ConnectionEventListener): void; onConnectionEvent?(listener: ConnectionEventListener): void;
offConnectionEvent?(listener: ConnectionEventListener): void; offConnectionEvent?(listener: ConnectionEventListener): void;
onRestHealthEvent?(listener: RestHealthListener): void; onRestHealthEvent?(listener: RestHealthListener): void;
offRestHealthEvent?(listener: RestHealthListener): void; offRestHealthEvent?(listener: RestHealthListener): void;
queryOpenOrders?(): Promise<AsterOrder[]>; queryOpenOrders?(): Promise<Order[]>;
queryAccountSnapshot?(): Promise<AsterAccountSnapshot | null>; queryAccountSnapshot?(): Promise<AccountSnapshot | null>;
changeMarginMode?(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void>; changeMarginMode?(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void>;
forceCancelAllOrders?(): Promise<boolean>; forceCancelAllOrders?(): Promise<boolean>;
} }
-168
View File
@@ -1,168 +0,0 @@
import type {
AccountListener,
DepthListener,
ExchangeAdapter,
ExchangePrecision,
KlineListener,
OrderListener,
TickerListener,
} from "./adapter";
import type { AsterOrder, CreateOrderParams, AsterDepth, AsterTicker, AsterKline } from "./types";
import { extractMessage } from "../utils/errors";
import { AsterGateway } from "./aster/client";
export interface AsterCredentials {
apiKey?: string;
apiSecret?: string;
symbol?: string;
}
export class AsterExchangeAdapter implements ExchangeAdapter {
readonly id = "aster";
private readonly gateway: AsterGateway;
private readonly symbol: string;
private initPromise: Promise<void> | null = null;
private lastInitErrorAt = 0;
private readonly initContexts = new Set<string>();
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
constructor(credentials: AsterCredentials = {}) {
this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret });
this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase();
}
supportsTrailingStops(): boolean {
return true;
}
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[AsterExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway.ensureInitialized(this.symbol).then((value) => {
this.clearRetry();
return value;
}).catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[AsterExchangeAdapter] ${context} failed`, error);
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => {
cb(snapshot);
}));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => {
cb(orders);
}));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: AsterDepth) => {
cb(depth);
}));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: AsterTicker) => {
cb(ticker);
}));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: AsterKline[]) => {
cb(klines);
}));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) });
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList });
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
try {
const precision = await this.gateway.getPrecision(this.symbol);
if (!precision) return null;
return {
priceTick: precision.priceTick,
qtyStep: precision.qtyStep,
priceDecimals: precision.priceDecimals,
sizeDecimals: precision.sizeDecimals,
};
} catch (error) {
console.error("[AsterExchangeAdapter] getPrecision failed", error);
return null;
}
}
}
+109
View File
@@ -0,0 +1,109 @@
import type {
AccountListener,
DepthListener,
ExchangeAdapter,
ExchangePrecision,
KlineListener,
OrderListener,
TickerListener,
} from "../adapter";
import type { Order, CreateOrderParams, Depth, Ticker, Kline } from "../types";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
import { AsterGateway } from "./gateway";
export interface AsterCredentials {
apiKey?: string;
apiSecret?: string;
symbol?: string;
}
export class AsterExchangeAdapter implements ExchangeAdapter {
readonly id = "aster";
private readonly gateway: AsterGateway;
private readonly symbol: string;
private readonly safeInvoke = createSafeInvoke("AsterExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
constructor(credentials: AsterCredentials = {}) {
this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret });
this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase();
this.init = createInitManager("AsterExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
}
supportsTrailingStops(): boolean {
return true;
}
watchAccount(cb: AccountListener): void {
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => {
cb(snapshot);
}));
}
watchOrders(cb: OrderListener): void {
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => {
cb(orders);
}));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: Depth) => {
cb(depth);
}));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: Ticker) => {
cb(ticker);
}));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: Kline[]) => {
cb(klines);
}));
}
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({ symbol: params.symbol, orderId: Number(params.orderId) });
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList });
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
try {
const precision = await this.gateway.getPrecision(this.symbol);
if (!precision) return null;
return {
priceTick: precision.priceTick,
qtyStep: precision.qtyStep,
priceDecimals: precision.priceDecimals,
sizeDecimals: precision.sizeDecimals,
};
} catch (error) {
console.error("[AsterExchangeAdapter] getPrecision failed", error);
return null;
}
}
}
@@ -1,11 +1,16 @@
import crypto from "crypto"; import crypto from "crypto";
import { setInterval, clearInterval, setTimeout, clearTimeout } from "timers"; import { setInterval, clearInterval, setTimeout, clearTimeout } from "timers";
import type { import type {
AsterAccountPosition, AccountPosition,
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
Ticker,
CreateOrderParams,
PositionSide,
} from "../types";
import type {
AsterSpotAccount, AsterSpotAccount,
AsterSpotAggTrade, AsterSpotAggTrade,
AsterSpotBookTicker, AsterSpotBookTicker,
@@ -18,18 +23,15 @@ import type {
AsterSpotTicker24h, AsterSpotTicker24h,
AsterSpotTrade, AsterSpotTrade,
AsterSpotUserTrade, AsterSpotUserTrade,
AsterTicker,
AsterFuturesExchangeInfo, AsterFuturesExchangeInfo,
AsterFuturesSymbolInfo, AsterFuturesSymbolInfo,
CancelSpotOrderParams, CancelSpotOrderParams,
CreateOrderParams,
CreateSpotOrderParams, CreateSpotOrderParams,
PositionSide,
QuerySpotOrderParams, QuerySpotOrderParams,
SpotAllOrdersParams, SpotAllOrdersParams,
SpotOpenOrdersParams, SpotOpenOrdersParams,
SpotUserTradesParams, SpotUserTradesParams,
} from "../types"; } from "./types";
import { decimalsOf } from "../../utils/math"; import { decimalsOf } from "../../utils/math";
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -244,7 +246,7 @@ export class AsterSpotRestClient {
}; };
} }
async createOrder(params: CreateSpotOrderParams): Promise<AsterOrder> { async createOrder(params: CreateSpotOrderParams): Promise<Order> {
const response = await this.request<any>({ const response = await this.request<any>({
path: "/api/v1/order", path: "/api/v1/order",
method: "POST", method: "POST",
@@ -255,7 +257,7 @@ export class AsterSpotRestClient {
return toOrderFromRest(response); return toOrderFromRest(response);
} }
async cancelOrder(params: CancelSpotOrderParams): Promise<AsterOrder> { async cancelOrder(params: CancelSpotOrderParams): Promise<Order> {
const response = await this.request<any>({ const response = await this.request<any>({
path: "/api/v1/order", path: "/api/v1/order",
method: "DELETE", method: "DELETE",
@@ -270,7 +272,7 @@ export class AsterSpotRestClient {
return toOrderFromRest(response); return toOrderFromRest(response);
} }
async getOrder(params: QuerySpotOrderParams): Promise<AsterOrder> { async getOrder(params: QuerySpotOrderParams): Promise<Order> {
const response = await this.request<any>({ const response = await this.request<any>({
path: "/api/v1/order", path: "/api/v1/order",
method: "GET", method: "GET",
@@ -285,7 +287,7 @@ export class AsterSpotRestClient {
return toOrderFromRest(response); return toOrderFromRest(response);
} }
async getOpenOrders(params: SpotOpenOrdersParams = {}): Promise<AsterOrder[]> { async getOpenOrders(params: SpotOpenOrdersParams = {}): Promise<Order[]> {
const response = await this.request<any[]>({ const response = await this.request<any[]>({
path: "/api/v1/openOrders", path: "/api/v1/openOrders",
method: "GET", method: "GET",
@@ -319,7 +321,7 @@ export class AsterSpotRestClient {
}); });
} }
async getAllOrders(params: SpotAllOrdersParams): Promise<AsterOrder[]> { async getAllOrders(params: SpotAllOrdersParams): Promise<Order[]> {
const response = await this.request<any[]>({ const response = await this.request<any[]>({
path: "/api/v1/allOrders", path: "/api/v1/allOrders",
method: "GET", method: "GET",
@@ -543,13 +545,13 @@ export class AsterSpotRestClient {
} }
try { try {
return JSON.parse(text) as T; return JSON.parse(text) as T;
} catch (error) { } catch {
throw new Error(`[AsterSpotRestClient] 无法解析响应: ${text.slice(0, 200)}`); throw new Error(`[AsterSpotRestClient] 无法解析响应: ${text.slice(0, 200)}`);
} }
} }
} }
function toDepth(streamSymbol: string, data: any): AsterDepth { function toDepth(streamSymbol: string, data: any): Depth {
return { return {
eventType: data.e, eventType: data.e,
eventTime: data.E, eventTime: data.E,
@@ -561,7 +563,7 @@ function toDepth(streamSymbol: string, data: any): AsterDepth {
}; };
} }
function toTicker(data: any): AsterTicker { function toTicker(data: any): Ticker {
return { return {
eventType: data.e, eventType: data.e,
eventTime: data.E, eventTime: data.E,
@@ -584,7 +586,7 @@ function toTicker(data: any): AsterTicker {
}; };
} }
function toKline(data: any): AsterKline { function toKline(data: any): Kline {
return { return {
eventType: data.e, eventType: data.e,
eventTime: data.E, eventTime: data.E,
@@ -607,7 +609,7 @@ function toKline(data: any): AsterKline {
}; };
} }
function fromRestKline(entry: any[], interval: string, symbol: string): AsterKline { function fromRestKline(entry: any[], interval: string, symbol: string): Kline {
return { return {
eventType: undefined, eventType: undefined,
eventTime: undefined, eventTime: undefined,
@@ -628,7 +630,7 @@ function fromRestKline(entry: any[], interval: string, symbol: string): AsterKli
}; };
} }
function toOrderFromRest(raw: any): AsterOrder { function toOrderFromRest(raw: any): Order {
return { return {
avgPrice: raw.avgPrice ?? "0", avgPrice: raw.avgPrice ?? "0",
clientOrderId: raw.clientOrderId ?? "", clientOrderId: raw.clientOrderId ?? "",
@@ -656,7 +658,7 @@ function toOrderFromRest(raw: any): AsterOrder {
}; };
} }
function toOrderFromEvent(event: any): AsterOrder { function toOrderFromEvent(event: any): Order {
return { return {
avgPrice: event.ap ?? "0", avgPrice: event.ap ?? "0",
clientOrderId: event.c ?? "", clientOrderId: event.c ?? "",
@@ -684,7 +686,7 @@ function toOrderFromEvent(event: any): AsterOrder {
}; };
} }
function toPositionFromRisk(raw: any): AsterAccountPosition { function toPositionFromRisk(raw: any): AccountPosition {
const positionSide = String(raw.positionSide ?? raw.ps ?? "BOTH").toUpperCase() as PositionSide; const positionSide = String(raw.positionSide ?? raw.ps ?? "BOTH").toUpperCase() as PositionSide;
return { return {
symbol: raw.symbol ?? raw.s ?? "", symbol: raw.symbol ?? raw.s ?? "",
@@ -708,16 +710,16 @@ function toPositionFromRisk(raw: any): AsterAccountPosition {
}; };
} }
function deepCloneAccount(snapshot: AsterAccountSnapshot | null): AsterAccountSnapshot | null { function deepCloneAccount(snapshot: AccountSnapshot | null): AccountSnapshot | null {
return snapshot ? JSON.parse(JSON.stringify(snapshot)) : null; return snapshot ? JSON.parse(JSON.stringify(snapshot)) : null;
} }
function sumUnrealizedProfit(positions: AsterAccountPosition[]): string { function sumUnrealizedProfit(positions: AccountPosition[]): string {
const total = positions.reduce((acc, position) => acc + Number(position.unrealizedProfit ?? 0), 0); const total = positions.reduce((acc, position) => acc + Number(position.unrealizedProfit ?? 0), 0);
return total.toFixed(8); return total.toFixed(8);
} }
function clonePositions(positions: AsterAccountPosition[]): AsterAccountPosition[] { function clonePositions(positions: AccountPosition[]): AccountPosition[] {
return positions.map((position) => ({ return positions.map((position) => ({
...position, ...position,
updateTime: position.updateTime ?? Date.now(), updateTime: position.updateTime ?? Date.now(),
@@ -763,18 +765,18 @@ export class AsterRestClient {
this.apiSecret = requireEnv(options.apiSecret ?? process.env.ASTER_API_SECRET, "ASTER_API_SECRET"); this.apiSecret = requireEnv(options.apiSecret ?? process.env.ASTER_API_SECRET, "ASTER_API_SECRET");
} }
async getAccount(): Promise<AsterAccountSnapshot> { async getAccount(): Promise<AccountSnapshot> {
return this.signedRequest<AsterAccountSnapshot>({ path: "/fapi/v2/account", method: "GET", params: {} }); return this.signedRequest<AccountSnapshot>({ path: "/fapi/v2/account", method: "GET", params: {} });
} }
async getOpenOrders(symbol?: string): Promise<AsterOrder[]> { async getOpenOrders(symbol?: string): Promise<Order[]> {
const params: Record<string, unknown> = {}; const params: Record<string, unknown> = {};
if (symbol) params.symbol = symbol; if (symbol) params.symbol = symbol;
const raw = await this.signedRequest<any[]>({ path: "/fapi/v1/openOrders", method: "GET", params }); const raw = await this.signedRequest<any[]>({ path: "/fapi/v1/openOrders", method: "GET", params });
return raw.map(toOrderFromRest); return raw.map(toOrderFromRest);
} }
async getPositions(symbol?: string): Promise<AsterAccountPosition[]> { async getPositions(symbol?: string): Promise<AccountPosition[]> {
const params: Record<string, unknown> = {}; const params: Record<string, unknown> = {};
if (symbol) params.symbol = symbol.toUpperCase(); if (symbol) params.symbol = symbol.toUpperCase();
const raw = await this.signedRequest<any[]>({ path: "/fapi/v2/positionRisk", method: "GET", params }); const raw = await this.signedRequest<any[]>({ path: "/fapi/v2/positionRisk", method: "GET", params });
@@ -795,12 +797,12 @@ export class AsterRestClient {
} }
try { try {
return JSON.parse(text) as AsterFuturesExchangeInfo; return JSON.parse(text) as AsterFuturesExchangeInfo;
} catch (error) { } catch {
throw new Error(`[AsterRestClient] 无法解析交易规则响应: ${text.slice(0, 200)}`); throw new Error(`[AsterRestClient] 无法解析交易规则响应: ${text.slice(0, 200)}`);
} }
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
// Sanitize and normalize params for Aster futures API. Paradex-specific flags // Sanitize and normalize params for Aster futures API. Paradex-specific flags
// like reduceOnly/closePosition on STOP/TRAILING should not leak here. // like reduceOnly/closePosition on STOP/TRAILING should not leak here.
const payload: Record<string, unknown> = {}; const payload: Record<string, unknown> = {};
@@ -813,6 +815,7 @@ export class AsterRestClient {
if (params.activationPrice !== undefined) payload.activationPrice = params.activationPrice; if (params.activationPrice !== undefined) payload.activationPrice = params.activationPrice;
if (params.callbackRate !== undefined) payload.callbackRate = params.callbackRate; if (params.callbackRate !== undefined) payload.callbackRate = params.callbackRate;
if (params.quantity !== undefined) payload.quantity = Math.abs(params.quantity); if (params.quantity !== undefined) payload.quantity = Math.abs(params.quantity);
if (params.clientOrderId !== undefined) payload.newClientOrderId = params.clientOrderId;
// Aster rejects reduceOnly/closePosition for certain order types (e.g. STOP/TRAILING). // Aster rejects reduceOnly/closePosition for certain order types (e.g. STOP/TRAILING).
// Keep the behavior exchange-specific by stripping them here for Aster. // Keep the behavior exchange-specific by stripping them here for Aster.
@@ -830,12 +833,12 @@ export class AsterRestClient {
return toOrderFromRest(response); return toOrderFromRest(response);
} }
async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<AsterOrder> { async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<Order> {
const response = await this.signedRequest<any>({ path: "/fapi/v1/order", method: "DELETE", params }); const response = await this.signedRequest<any>({ path: "/fapi/v1/order", method: "DELETE", params });
return toOrderFromRest(response); return toOrderFromRest(response);
} }
async cancelOrders(params: { symbol: string; orderIdList?: Array<number | string>; origClientOrderIdList?: string[] }): Promise<AsterOrder[]> { async cancelOrders(params: { symbol: string; orderIdList?: Array<number | string>; origClientOrderIdList?: string[] }): Promise<Order[]> {
const payload: Record<string, unknown> = { symbol: params.symbol }; const payload: Record<string, unknown> = { symbol: params.symbol };
if (params.orderIdList?.length) { if (params.orderIdList?.length) {
payload.orderIdList = `[${params.orderIdList payload.orderIdList = `[${params.orderIdList
@@ -853,7 +856,7 @@ export class AsterRestClient {
await this.signedRequest({ path: "/fapi/v1/allOpenOrders", method: "DELETE", params }); await this.signedRequest({ path: "/fapi/v1/allOpenOrders", method: "DELETE", params });
} }
async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise<AsterKline[]> { async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise<Kline[]> {
const upper = symbol.toUpperCase(); const upper = symbol.toUpperCase();
const url = `${FUTURES_REST_BASE}/fapi/v1/continuousKlines?pair=${upper}&contractType=PERPETUAL&interval=${encodeURIComponent(interval)}&limit=${limit}`; const url = `${FUTURES_REST_BASE}/fapi/v1/continuousKlines?pair=${upper}&contractType=PERPETUAL&interval=${encodeURIComponent(interval)}&limit=${limit}`;
let response: Response; let response: Response;
@@ -869,7 +872,7 @@ export class AsterRestClient {
try { try {
const payload = JSON.parse(text) as any[]; const payload = JSON.parse(text) as any[];
return payload.map((entry) => fromRestKline(entry, interval, upper)); return payload.map((entry) => fromRestKline(entry, interval, upper));
} catch (error) { } catch {
throw new Error(`[AsterRestClient] 无法解析K线响应: ${text.slice(0, 200)}`); throw new Error(`[AsterRestClient] 无法解析K线响应: ${text.slice(0, 200)}`);
} }
} }
@@ -899,7 +902,7 @@ export class AsterRestClient {
const payload = JSON.parse(text) as any; const payload = JSON.parse(text) as any;
// The response shape mirrors Binance: { symbol, markPrice, indexPrice, lastFundingRate, nextFundingTime, time } // The response shape mirrors Binance: { symbol, markPrice, indexPrice, lastFundingRate, nextFundingTime, time }
return payload; return payload;
} catch (error) { } catch {
throw new Error(`[AsterRestClient] 无法解析资金费率响应: ${text.slice(0, 200)}`); throw new Error(`[AsterRestClient] 无法解析资金费率响应: ${text.slice(0, 200)}`);
} }
} }
@@ -942,16 +945,16 @@ export class AsterRestClient {
} }
try { try {
return JSON.parse(text) as T; return JSON.parse(text) as T;
} catch (error) { } catch {
throw new Error(`[AsterRestClient] 无法解析响应: ${text.slice(0, 200)}`); throw new Error(`[AsterRestClient] 无法解析响应: ${text.slice(0, 200)}`);
} }
} }
} }
type DepthHandler = (depth: AsterDepth) => void; type DepthHandler = (depth: Depth) => void;
type TickerHandler = (ticker: AsterTicker) => void; type TickerHandler = (ticker: Ticker) => void;
type KlineHandler = (kline: AsterKline) => void; type KlineHandler = (kline: Kline) => void;
type StreamKind = "depth" | "ticker" | "kline"; type StreamKind = "depth" | "ticker" | "kline";
@@ -1244,7 +1247,7 @@ export class AsterUserStream {
} }
} }
function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AsterAccountSnapshot | null { function updateAccountSnapshot(snapshot: AccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AccountSnapshot | null {
if (!snapshot) return snapshot; if (!snapshot) return snapshot;
const next = deepCloneAccount(snapshot); const next = deepCloneAccount(snapshot);
if (!next) return snapshot; if (!next) return snapshot;
@@ -1294,7 +1297,7 @@ function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { e
return next; return next;
} }
function mergeOrderSnapshot(map: Map<string, AsterOrder>, order: AsterOrder): void { function mergeOrderSnapshot(map: Map<string, Order>, order: Order): void {
const rawId = order.orderId; const rawId = order.orderId;
if (rawId === undefined || rawId === null) return; if (rawId === undefined || rawId === null) return;
const key = String(rawId); const key = String(rawId);
@@ -1311,18 +1314,18 @@ export class AsterGateway {
private readonly publicStreams: AsterPublicStreams; private readonly publicStreams: AsterPublicStreams;
private readonly userStream: AsterUserStream; private readonly userStream: AsterUserStream;
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private readonly openOrders = new Map<string, AsterOrder>(); private readonly openOrders = new Map<string, Order>();
private positionSyncTimer: ReturnType<typeof setInterval> | null = null; private positionSyncTimer: ReturnType<typeof setInterval> | null = null;
private positionSyncInFlight = false; private positionSyncInFlight = false;
private readonly accountEvent = new SimpleEvent<AsterAccountSnapshot>(); private readonly accountEvent = new SimpleEvent<AccountSnapshot>();
private readonly ordersEvent = new SimpleEvent<AsterOrder[]>(); private readonly ordersEvent = new SimpleEvent<Order[]>();
private readonly depthEvents = new Map<string, SimpleEvent<AsterDepth>>(); private readonly depthEvents = new Map<string, SimpleEvent<Depth>>();
private readonly tickerEvents = new Map<string, SimpleEvent<AsterTicker>>(); private readonly tickerEvents = new Map<string, SimpleEvent<Ticker>>();
private readonly klineEvents = new Map<string, SimpleEvent<AsterKline[]>>(); private readonly klineEvents = new Map<string, SimpleEvent<Kline[]>>();
private readonly klineStores = new Map<string, AsterKline[]>(); private readonly klineStores = new Map<string, Kline[]>();
private readonly klineRefreshTimers = new Map<string, ReturnType<typeof setInterval>>(); private readonly klineRefreshTimers = new Map<string, ReturnType<typeof setInterval>>();
private readonly klineInitialFetches = new Map<string, Promise<void>>(); private readonly klineInitialFetches = new Map<string, Promise<void>>();
private initialized = false; private initialized = false;
@@ -1366,7 +1369,7 @@ export class AsterGateway {
}); });
} }
async ensureInitialized(symbol: string): Promise<void> { async ensureInitialized(_symbol: string): Promise<void> {
if (this.initialized) return; if (this.initialized) return;
if (this.initializing) return this.initializing; if (this.initializing) return this.initializing;
this.initializing = (async () => { this.initializing = (async () => {
@@ -1381,21 +1384,21 @@ export class AsterGateway {
return this.initializing; return this.initializing;
} }
onAccount(listener: (snapshot: AsterAccountSnapshot) => void): void { onAccount(listener: (snapshot: AccountSnapshot) => void): void {
this.accountEvent.add(listener); this.accountEvent.add(listener);
if (this.accountSnapshot) listener(this.accountSnapshot); if (this.accountSnapshot) listener(this.accountSnapshot);
} }
onOrders(listener: (orders: AsterOrder[]) => void): void { onOrders(listener: (orders: Order[]) => void): void {
this.ordersEvent.add(listener); this.ordersEvent.add(listener);
listener(Array.from(this.openOrders.values())); listener(Array.from(this.openOrders.values()));
} }
onDepth(symbol: string, listener: (depth: AsterDepth) => void): void { onDepth(symbol: string, listener: (depth: Depth) => void): void {
const upper = symbol.toUpperCase(); const upper = symbol.toUpperCase();
let event = this.depthEvents.get(upper); let event = this.depthEvents.get(upper);
if (!event) { if (!event) {
event = new SimpleEvent<AsterDepth>(); event = new SimpleEvent<Depth>();
this.depthEvents.set(upper, event); this.depthEvents.set(upper, event);
this.publicStreams.subscribeDepth(upper, (depth) => { this.publicStreams.subscribeDepth(upper, (depth) => {
event?.emit(depth); event?.emit(depth);
@@ -1404,11 +1407,11 @@ export class AsterGateway {
event.add(listener); event.add(listener);
} }
onTicker(symbol: string, listener: (ticker: AsterTicker) => void): void { onTicker(symbol: string, listener: (ticker: Ticker) => void): void {
const upper = symbol.toUpperCase(); const upper = symbol.toUpperCase();
let event = this.tickerEvents.get(upper); let event = this.tickerEvents.get(upper);
if (!event) { if (!event) {
event = new SimpleEvent<AsterTicker>(); event = new SimpleEvent<Ticker>();
this.tickerEvents.set(upper, event); this.tickerEvents.set(upper, event);
this.publicStreams.subscribeTicker(upper, (ticker) => { this.publicStreams.subscribeTicker(upper, (ticker) => {
event?.emit(ticker); event?.emit(ticker);
@@ -1417,12 +1420,12 @@ export class AsterGateway {
event.add(listener); event.add(listener);
} }
onKlines(symbol: string, interval: string, listener: (klines: AsterKline[]) => void): void { onKlines(symbol: string, interval: string, listener: (klines: Kline[]) => void): void {
const upper = symbol.toUpperCase(); const upper = symbol.toUpperCase();
const key = `${upper}:${interval}`; const key = `${upper}:${interval}`;
let event = this.klineEvents.get(key); let event = this.klineEvents.get(key);
if (!event) { if (!event) {
event = new SimpleEvent<AsterKline[]>(); event = new SimpleEvent<Kline[]>();
this.klineEvents.set(key, event); this.klineEvents.set(key, event);
this.publicStreams.subscribeKline(symbol, interval, (kline) => { this.publicStreams.subscribeKline(symbol, interval, (kline) => {
const storeKey = `${upper}:${interval}`; const storeKey = `${upper}:${interval}`;
@@ -1509,7 +1512,7 @@ export class AsterGateway {
console.error("[AsterGateway] 刷新持仓失败", positionError); console.error("[AsterGateway] 刷新持仓失败", positionError);
} }
const normalizedPositions = clonePositions(positions); const normalizedPositions = clonePositions(positions);
const snapshot: AsterAccountSnapshot = { const snapshot: AccountSnapshot = {
...account, ...account,
positions: normalizedPositions, positions: normalizedPositions,
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions), totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
@@ -1547,7 +1550,7 @@ export class AsterGateway {
if (!Array.isArray(positions)) return; if (!Array.isArray(positions)) return;
const normalizedPositions = clonePositions(positions); const normalizedPositions = clonePositions(positions);
if (!this.accountSnapshot) { if (!this.accountSnapshot) {
const snapshot: AsterAccountSnapshot = { const snapshot: AccountSnapshot = {
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
canWithdraw: true, canWithdraw: true,
@@ -1561,7 +1564,7 @@ export class AsterGateway {
this.accountEvent.emit(snapshot); this.accountEvent.emit(snapshot);
return; return;
} }
const nextSnapshot: AsterAccountSnapshot = { const nextSnapshot: AccountSnapshot = {
...this.accountSnapshot, ...this.accountSnapshot,
positions: normalizedPositions, positions: normalizedPositions,
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions), totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
@@ -1576,15 +1579,15 @@ export class AsterGateway {
} }
} }
getAccountSnapshot(): AsterAccountSnapshot | null { getAccountSnapshot(): AccountSnapshot | null {
return this.accountSnapshot; return this.accountSnapshot;
} }
getOpenOrdersSnapshot(): AsterOrder[] { getOpenOrdersSnapshot(): Order[] {
return Array.from(this.openOrders.values()); return Array.from(this.openOrders.values());
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
const normalized = await this.normalizeOrderParams(params); const normalized = await this.normalizeOrderParams(params);
const order = await this.rest.createOrder(normalized); const order = await this.rest.createOrder(normalized);
mergeOrderSnapshot(this.openOrders, order); mergeOrderSnapshot(this.openOrders, order);
+14 -98
View File
@@ -1,101 +1,17 @@
import type { AsterOrder, CreateOrderParams } from "../types"; import { createOrderHandlers } from "../order-handlers";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { const handlers = createOrderHandlers({
if (params.quantity === undefined) { exchangeName: "Aster",
params.quantity = intent.quantity; defaultLimitTimeInForce: "GTX",
} supportsTrailingStop: true,
if (params.timeInForce === undefined && intent.timeInForce) { supportsTriggerType: true,
params.timeInForce = intent.timeInForce; });
}
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export const {
const params: CreateOrderParams = applyCommonFields( createLimitOrder,
{ createMarketOrder,
symbol: intent.symbol, createStopOrder,
side: intent.side, createTrailingStopOrder,
type: "LIMIT", createClosePositionOrder,
quantity: intent.quantity, } = handlers;
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
triggerType: intent.triggerType,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "TRAILING_STOP_MARKET",
quantity: intent.quantity,
activationPrice: intent.activationPrice,
callbackRate: intent.callbackRate,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
},
intent
);
return intent.adapter.createOrder(params);
}
+248
View File
@@ -0,0 +1,248 @@
import type { DepthLevel, OrderSide, OrderType, TimeInForce } from "../types";
export interface AsterSpotRateLimit {
rateLimitType: string;
interval: string;
intervalNum: number;
limit: number;
}
export interface AsterSpotExchangeFilter {
filterType: string;
[key: string]: string | number | boolean | undefined;
}
export interface AsterFuturesSymbolFilter {
filterType: string;
tickSize?: string;
stepSize?: string;
minPrice?: string;
maxPrice?: string;
minQty?: string;
maxQty?: string;
[key: string]: string | number | boolean | undefined;
}
export interface AsterFuturesSymbolInfo {
symbol: string;
pair?: string;
contractType?: string;
pricePrecision?: number;
quantityPrecision?: number;
baseAssetPrecision?: number;
quotePrecision?: number;
underlyingType?: string;
filters?: AsterFuturesSymbolFilter[];
}
export interface AsterFuturesExchangeInfo {
timezone?: string;
serverTime?: number;
symbols?: AsterFuturesSymbolInfo[];
}
export interface AsterSpotAssetInfo {
asset: string;
}
export interface AsterSpotSymbolInfo {
symbol: string;
status: string;
baseAsset: string;
quoteAsset: string;
baseAssetPrecision?: number;
quotePrecision?: number;
pricePrecision?: number;
quantityPrecision?: number;
orderTypes: string[];
timeInForce: string[];
ocoAllowed: boolean;
filters: AsterSpotExchangeFilter[];
}
export interface AsterSpotExchangeInfo {
timezone: string;
serverTime: number;
rateLimits: AsterSpotRateLimit[];
exchangeFilters: AsterSpotExchangeFilter[];
assets?: AsterSpotAssetInfo[];
symbols: AsterSpotSymbolInfo[];
}
export interface AsterSpotDepth {
lastUpdateId: number;
E?: number;
T?: number;
bids: DepthLevel[];
asks: DepthLevel[];
}
export interface AsterSpotTrade {
id: number;
price: string;
qty: string;
baseQty?: string;
quoteQty?: string;
time: number;
isBuyerMaker: boolean;
}
export interface AsterSpotHistoricalTrade extends AsterSpotTrade {
isBestMatch?: boolean;
}
export interface AsterSpotAggTrade {
a: number;
p: string;
q: string;
f: number;
l: number;
T: number;
m: boolean;
M?: boolean;
}
export interface AsterSpotKline {
openTime: number;
open: string;
high: string;
low: string;
close: string;
volume: string;
closeTime: number;
quoteAssetVolume: string;
numberOfTrades: number;
takerBuyBaseAssetVolume: string;
takerBuyQuoteAssetVolume: string;
}
export interface AsterSpotTicker24h {
symbol: string;
priceChange: string;
priceChangePercent: string;
weightedAvgPrice: string;
prevClosePrice: string;
lastPrice: string;
lastQty: string;
bidPrice: string;
bidQty: string;
askPrice: string;
askQty: string;
openPrice: string;
highPrice: string;
lowPrice: string;
volume: string;
quoteVolume: string;
openTime: number;
closeTime: number;
firstId: number;
lastId: number;
count: number;
baseAsset?: string;
quoteAsset?: string;
}
export interface AsterSpotPriceTicker {
symbol: string;
price: string;
time?: number;
}
export interface AsterSpotBookTicker {
symbol: string;
bidPrice: string;
bidQty: string;
askPrice: string;
askQty: string;
time?: number;
}
export interface AsterSpotCommissionRate {
symbol: string;
makerCommissionRate: string;
takerCommissionRate: string;
}
export interface CreateSpotOrderParams {
symbol: string;
side: OrderSide;
type: OrderType;
timeInForce?: TimeInForce;
quantity?: number | string;
quoteOrderQty?: number | string;
price?: number | string;
newClientOrderId?: string;
stopPrice?: number | string;
recvWindow?: number;
}
export interface CancelSpotOrderParams {
symbol: string;
orderId?: number | string;
origClientOrderId?: string;
recvWindow?: number;
}
export interface QuerySpotOrderParams extends CancelSpotOrderParams {}
export interface SpotOpenOrdersParams {
symbol?: string;
recvWindow?: number;
orderIdList?: Array<number | string>;
origClientOrderIdList?: string[];
}
export interface SpotAllOrdersParams {
symbol: string;
orderId?: number;
startTime?: number;
endTime?: number;
limit?: number;
recvWindow?: number;
}
export interface AsterSpotAccountBalance {
asset: string;
free: string;
locked: string;
}
export interface AsterSpotAccount {
feeTier: number;
canTrade: boolean;
canDeposit: boolean;
canWithdraw: boolean;
canBurnAsset?: boolean;
updateTime: number;
makerCommission?: string;
takerCommission?: string;
buyerCommission?: string;
sellerCommission?: string;
balances: AsterSpotAccountBalance[];
}
export interface SpotUserTradesParams {
symbol?: string;
orderId?: number;
startTime?: number;
endTime?: number;
fromId?: number;
limit?: number;
recvWindow?: number;
}
export interface AsterSpotUserTrade {
symbol: string;
id: number;
orderId: number;
side: OrderSide;
price: string;
qty: string;
quoteQty?: string;
commission: string;
commissionAsset: string;
time: number;
counterpartyId?: number;
maker: boolean;
buyer: boolean;
}
+17 -84
View File
@@ -6,9 +6,10 @@ import type {
OrderListener, OrderListener,
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types"; import type { Order, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors"; import { extractMessage } from "../../utils/errors";
import { BackpackGateway, type BackpackGatewayOptions } from "./gateway"; import { BackpackGateway, type BackpackGatewayOptions } from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
export interface BackpackCredentials { export interface BackpackCredentials {
apiKey?: string; apiKey?: string;
@@ -23,11 +24,8 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
readonly id = "backpack"; readonly id = "backpack";
private readonly gateway: BackpackGateway; private readonly gateway: BackpackGateway;
private readonly symbol: string; private readonly symbol: string;
private initPromise: Promise<void> | null = null; private readonly safeInvoke = createSafeInvoke("BackpackExchangeAdapter");
private readonly initContexts = new Set<string>(); private readonly init: ReturnType<typeof createInitManager>;
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: BackpackCredentials = {}) { constructor(credentials: BackpackCredentials = {}) {
const apiKey = credentials.apiKey ?? process.env.BACKPACK_API_KEY; const apiKey = credentials.apiKey ?? process.env.BACKPACK_API_KEY;
@@ -54,6 +52,9 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
this.gateway = new BackpackGateway(gatewayOptions); this.gateway = new BackpackGateway(gatewayOptions);
this.symbol = symbol; this.symbol = symbol;
this.init = createInitManager("BackpackExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
} }
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
@@ -61,118 +62,50 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
} }
watchAccount(cb: AccountListener): void { watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount"); void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb)); this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
} }
watchOrders(cb: OrderListener): void { watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders"); void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb)); this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
} }
watchDepth(_symbol: string, cb: DepthListener): void { watchDepth(_symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth"); void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(this.safeInvoke("watchDepth", cb)); this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
} }
watchTicker(_symbol: string, cb: TickerListener): void { watchTicker(_symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker"); void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(this.safeInvoke("watchTicker", cb)); this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
} }
watchKlines(_symbol: string, interval: string, cb: KlineListener): void { watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized(`watchKlines:${interval}`); void this.init.ensureInitialized(`watchKlines:${interval}`);
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb)); this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized("createOrder"); await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params); return this.gateway.createOrder(params);
} }
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> { async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder"); await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder({ orderId: params.orderId }); await this.gateway.cancelOrder({ orderId: params.orderId });
} }
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> { async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders"); await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ orderIdList: params.orderIdList }); await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
} }
async cancelAllOrders(_params: { symbol: string }): Promise<void> { async cancelAllOrders(_params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders"); await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(); await this.gateway.cancelAllOrders();
} }
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[BackpackExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
if (process.env.BACKPACK_DEBUG === "1") {
console.error(`[BackpackExchangeAdapter] initialize succeeded`);
}
if (process.env.BACKPACK_DEBUG === "1") {
console.error(`[BackpackExchangeAdapter] initialize succeeded`);
}
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[BackpackExchangeAdapter] ${context} failed`, error);
}
private logError(context: string, error: unknown): void { private logError(context: string, error: unknown): void {
if (process.env.BACKPACK_DEBUG === "1" || process.env.BACKPACK_DEBUG === "true") { if (process.env.BACKPACK_DEBUG === "1" || process.env.BACKPACK_DEBUG === "true") {
console.error(`[BackpackExchangeAdapter] ${context} failed: ${extractMessage(error)}`); console.error(`[BackpackExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
+28 -33
View File
@@ -7,14 +7,13 @@ import ccxt, {
import NodeWebSocket from "ws"; import NodeWebSocket from "ws";
import { sign, utils as edUtils, hashes as edHashes } from "@noble/ed25519"; import { sign, utils as edUtils, hashes as edHashes } from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512"; import { sha512 } from "@noble/hashes/sha512";
import { randomBytes } from "crypto";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterAccountPosition, AccountPosition,
AsterOrder, Order,
AsterDepth, Depth,
AsterTicker, Ticker,
AsterKline, Kline,
CreateOrderParams, CreateOrderParams,
OrderType, OrderType,
} from "../types"; } from "../types";
@@ -93,8 +92,8 @@ export class BackpackGateway {
private tickerPollTimer: ReturnType<typeof setInterval> | null = null; private tickerPollTimer: ReturnType<typeof setInterval> | null = null;
private readonly klinePollTimers = new Map<string, ReturnType<typeof setInterval>>(); private readonly klinePollTimers = new Map<string, ReturnType<typeof setInterval>>();
private readonly localOrders = new Map<string, AsterOrder>(); private readonly localOrders = new Map<string, Order>();
private lastBalanceSnapshot: AsterAccountSnapshot | null = null; private lastBalanceSnapshot: AccountSnapshot | null = null;
private marketId = ""; private marketId = "";
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
@@ -137,7 +136,6 @@ export class BackpackGateway {
} }
private async doInitialize(symbol?: string): Promise<void> { private async doInitialize(symbol?: string): Promise<void> {
try {
await this.exchange.loadMarkets(); await this.exchange.loadMarkets();
const requested = (symbol ?? this.symbol).toUpperCase(); const requested = (symbol ?? this.symbol).toUpperCase();
const market = this.findMarket(requested); const market = this.findMarket(requested);
@@ -156,9 +154,6 @@ export class BackpackGateway {
}); });
} }
this.initialized = true; this.initialized = true;
} catch (error) {
throw error;
}
} }
private findMarket(requested: string): any | null { private findMarket(requested: string): any | null {
@@ -249,7 +244,7 @@ export class BackpackGateway {
this.exchange.fetchOpenOrders(this.marketSymbol), this.exchange.fetchOpenOrders(this.marketSymbol),
this.exchange.fetchOrders(this.marketSymbol, undefined, 200, {}), this.exchange.fetchOrders(this.marketSymbol, undefined, 200, {}),
]); ]);
const active = new Map<string, AsterOrder>(); const active = new Map<string, Order>();
for (const entry of [...openOrders, ...allOrders]) { for (const entry of [...openOrders, ...allOrders]) {
const status = this.normalizeStatus(entry.status ?? (entry.info?.status as string)); const status = this.normalizeStatus(entry.status ?? (entry.info?.status as string));
if (this.isTerminalStatus(status)) continue; if (this.isTerminalStatus(status)) continue;
@@ -320,7 +315,7 @@ export class BackpackGateway {
// ---- Order actions ----------------------------------------------------- // ---- Order actions -----------------------------------------------------
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized(); await this.ensureInitialized();
const symbol = this.marketSymbol; const symbol = this.marketSymbol;
const normalizedType = this.normalizeOrderType(params.type); const normalizedType = this.normalizeOrderType(params.type);
@@ -404,11 +399,11 @@ export class BackpackGateway {
// ---- Mapping helpers --------------------------------------------------- // ---- Mapping helpers ---------------------------------------------------
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot { private mapBalanceToAccountSnapshot(balance: Balances): AccountSnapshot {
return this.mapBalanceToAccountSnapshotWithPositions(balance, []); return this.mapBalanceToAccountSnapshotWithPositions(balance, []);
} }
private async fetchAccountSnapshot(): Promise<AsterAccountSnapshot> { private async fetchAccountSnapshot(): Promise<AccountSnapshot> {
await this.ensureInitialized(); await this.ensureInitialized();
const [balance, positions] = await Promise.all([ const [balance, positions] = await Promise.all([
this.exchange.fetchBalance(), this.exchange.fetchBalance(),
@@ -422,7 +417,7 @@ export class BackpackGateway {
return this.mapBalanceToAccountSnapshotWithPositions(balance, positions ?? []); return this.mapBalanceToAccountSnapshotWithPositions(balance, positions ?? []);
} }
private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AsterAccountSnapshot { private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AccountSnapshot {
const now = Date.now(); const now = Date.now();
const assets = this.normalizeAssets(balance, now); const assets = this.normalizeAssets(balance, now);
const positions = this.normalizePositions(rawPositions, now); const positions = this.normalizePositions(rawPositions, now);
@@ -430,7 +425,7 @@ export class BackpackGateway {
const totalUnrealized = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0")); const totalUnrealized = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0"));
const availableBalance = this.sumStrings(assets.map((asset) => asset.availableBalance)); const availableBalance = this.sumStrings(assets.map((asset) => asset.availableBalance));
const snapshot: AsterAccountSnapshot = { const snapshot: AccountSnapshot = {
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
canWithdraw: true, canWithdraw: true,
@@ -452,9 +447,9 @@ export class BackpackGateway {
return snapshot; return snapshot;
} }
private normalizeAssets(balance: Balances, now: number): AsterAccountSnapshot["assets"] { private normalizeAssets(balance: Balances, now: number): AccountSnapshot["assets"] {
const metaKeys = new Set(["free", "used", "total", "info", "timestamp", "datetime", "debt"]); const metaKeys = new Set(["free", "used", "total", "info", "timestamp", "datetime", "debt"]);
const assets: AsterAccountSnapshot["assets"] = []; const assets: AccountSnapshot["assets"] = [];
for (const [currency, value] of Object.entries(balance)) { for (const [currency, value] of Object.entries(balance)) {
if (metaKeys.has(currency)) continue; if (metaKeys.has(currency)) continue;
if (!value || typeof value !== "object") continue; if (!value || typeof value !== "object") continue;
@@ -465,9 +460,9 @@ export class BackpackGateway {
return assets; return assets;
} }
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] { private normalizePositions(rawPositions: any[], now: number): AccountSnapshot["positions"] {
if (!Array.isArray(rawPositions)) return []; if (!Array.isArray(rawPositions)) return [];
const positions: AsterAccountSnapshot["positions"] = []; const positions: AccountSnapshot["positions"] = [];
for (const raw of rawPositions) { for (const raw of rawPositions) {
const info = raw?.info ?? raw ?? {}; const info = raw?.info ?? raw ?? {};
const quantity = this.toNumber(raw?.contracts ?? info.netExposureQuantity ?? info.netQuantity); const quantity = this.toNumber(raw?.contracts ?? info.netExposureQuantity ?? info.netQuantity);
@@ -498,7 +493,7 @@ export class BackpackGateway {
return positions; return positions;
} }
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth { private mapOrderBookToDepth(orderbook: CcxtOrderBook): Depth {
return { return {
lastUpdateId: orderbook.nonce || Date.now(), lastUpdateId: orderbook.nonce || Date.now(),
bids: (orderbook.bids ?? []) bids: (orderbook.bids ?? [])
@@ -511,7 +506,7 @@ export class BackpackGateway {
}; };
} }
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker { private mapTickerToAsterTicker(ticker: CcxtTicker): Ticker {
return { return {
symbol: ticker.symbol, symbol: ticker.symbol,
lastPrice: ticker.last?.toString() ?? "0", lastPrice: ticker.last?.toString() ?? "0",
@@ -527,7 +522,7 @@ export class BackpackGateway {
private mapOHLCVToKline( private mapOHLCVToKline(
candle: [number, number, number, number, number, number], candle: [number, number, number, number, number, number],
interval: string interval: string
): AsterKline { ): Kline {
const [openTime, open, high, low, close, volume] = candle; const [openTime, open, high, low, close, volume] = candle;
return { return {
openTime, openTime,
@@ -541,7 +536,7 @@ export class BackpackGateway {
}; };
} }
private mapRestOrder(order: CcxtOrder): AsterOrder { private mapRestOrder(order: CcxtOrder): Order {
const info = (order.info ?? {}) as Record<string, unknown>; const info = (order.info ?? {}) as Record<string, unknown>;
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL"; const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
let type = this.normalizeOrderType(order.type ?? (info.o as string)) as OrderType; let type = this.normalizeOrderType(order.type ?? (info.o as string)) as OrderType;
@@ -584,7 +579,7 @@ export class BackpackGateway {
}; };
} }
private mapWsOrder(data: Record<string, unknown>): AsterOrder { private mapWsOrder(data: Record<string, unknown>): Order {
const sideRaw = String(data.S ?? "").toUpperCase(); const sideRaw = String(data.S ?? "").toUpperCase();
const side: "BUY" | "SELL" = sideRaw === "BID" ? "BUY" : "SELL"; const side: "BUY" | "SELL" = sideRaw === "BID" ? "BUY" : "SELL";
const triggerPresent = data.P != null || data.B != null; const triggerPresent = data.P != null || data.B != null;
@@ -631,7 +626,7 @@ export class BackpackGateway {
} }
} }
private mapWsPosition(data: Record<string, unknown>): AsterAccountPosition | null { private mapWsPosition(data: Record<string, unknown>): AccountPosition | null {
const quantityRaw = data.q ?? data.Q; const quantityRaw = data.q ?? data.Q;
const qty = Number(this.toStringAmount(quantityRaw)); const qty = Number(this.toStringAmount(quantityRaw));
if (!Number.isFinite(qty)) return null; if (!Number.isFinite(qty)) return null;
@@ -656,8 +651,8 @@ export class BackpackGateway {
}; };
} }
private mergeWsPosition(position: AsterAccountPosition): void { private mergeWsPosition(position: AccountPosition): void {
const snapshot: AsterAccountSnapshot = this.lastBalanceSnapshot const snapshot: AccountSnapshot = this.lastBalanceSnapshot
? { ? {
...this.lastBalanceSnapshot, ...this.lastBalanceSnapshot,
positions: this.lastBalanceSnapshot.positions ? [...this.lastBalanceSnapshot.positions] : [], positions: this.lastBalanceSnapshot.positions ? [...this.lastBalanceSnapshot.positions] : [],
@@ -697,7 +692,7 @@ export class BackpackGateway {
this.emitAccount(snapshot); this.emitAccount(snapshot);
} }
private emitAccount(snapshot: AsterAccountSnapshot): void { private emitAccount(snapshot: AccountSnapshot): void {
for (const listener of this.accountListeners) { for (const listener of this.accountListeners) {
try { try {
listener(snapshot); listener(snapshot);
@@ -774,7 +769,7 @@ export class BackpackGateway {
void this.resubscribeAllTopics(); void this.resubscribeAllTopics();
}; };
private handleWsClose = (event: any): void => { private handleWsClose = (_event: any): void => {
if (this.wsCleanup) { if (this.wsCleanup) {
try { try {
this.wsCleanup(); this.wsCleanup();
+14 -82
View File
@@ -1,84 +1,16 @@
import type { AsterOrder, CreateOrderParams } from "../types"; import { createOrderHandlers } from "../order-handlers";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { const handlers = createOrderHandlers({
if (params.quantity === undefined) { exchangeName: "Backpack",
params.quantity = intent.quantity; defaultLimitTimeInForce: "GTX",
} supportsTrailingStop: false,
if (params.timeInForce === undefined && intent.timeInForce) { supportsTriggerType: false,
params.timeInForce = intent.timeInForce; });
}
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export const {
const params: CreateOrderParams = applyCommonFields( createLimitOrder,
{ createMarketOrder,
symbol: intent.symbol, createStopOrder,
side: intent.side, createTrailingStopOrder,
type: "LIMIT", createClosePositionOrder,
quantity: intent.quantity, } = handlers;
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
throw new Error("Backpack exchange does not support trailing stop orders");
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
},
intent
);
return intent.adapter.createOrder(params);
}
+30 -100
View File
@@ -1,4 +1,3 @@
import { clearTimeout, setTimeout } from "timers";
import type { import type {
AccountListener, AccountListener,
DepthListener, DepthListener,
@@ -9,9 +8,9 @@ import type {
OrderListener, OrderListener,
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types"; import type { Order, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import { BinanceGateway, type BinanceGatewayOptions } from "./gateway"; import { BinanceGateway, type BinanceGatewayOptions } from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
export interface BinanceCredentials { export interface BinanceCredentials {
apiKey?: string; apiKey?: string;
@@ -32,11 +31,8 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
private readonly gateway: BinanceGateway; private readonly gateway: BinanceGateway;
private readonly symbol: string; private readonly symbol: string;
private readonly marketType: "spot" | "perp" | "auto"; private readonly marketType: "spot" | "perp" | "auto";
private initPromise: Promise<void> | null = null; private readonly safeInvoke = createSafeInvoke("BinanceExchangeAdapter");
private readonly initContexts = new Set<string>(); private readonly init: ReturnType<typeof createInitManager>;
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: BinanceCredentials = {}) { constructor(credentials: BinanceCredentials = {}) {
const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY; const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY;
@@ -61,6 +57,9 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
futuresWsUrl: credentials.futuresWsUrl, futuresWsUrl: credentials.futuresWsUrl,
logger: credentials.logger, logger: credentials.logger,
}); });
this.init = createInitManager("BinanceExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
} }
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
@@ -69,163 +68,94 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
watchAccount(cb: AccountListener): void { watchAccount(cb: AccountListener): void {
const safe = this.safeInvoke("watchAccount", cb); const safe = this.safeInvoke("watchAccount", cb);
void this.ensureInitialized("watchAccount") void this.init.ensureInitialized("watchAccount")
.then(() => { .then(() => {
this.gateway.onAccount(safe); this.gateway.onAccount(safe);
}) });
.catch((error) => this.handleInitError("watchAccount", error));
} }
watchOrders(cb: OrderListener): void { watchOrders(cb: OrderListener): void {
const safe = this.safeInvoke("watchOrders", cb); const safe = this.safeInvoke("watchOrders", cb);
void this.ensureInitialized("watchOrders") void this.init.ensureInitialized("watchOrders")
.then(() => { .then(() => {
this.gateway.onOrders(safe); this.gateway.onOrders(safe);
}) });
.catch((error) => this.handleInitError("watchOrders", error));
} }
watchDepth(symbol: string, cb: DepthListener): void { watchDepth(symbol: string, cb: DepthListener): void {
const safe = this.safeInvoke("watchDepth", cb); const safe = this.safeInvoke("watchDepth", cb);
void this.ensureInitialized(`watchDepth:${symbol}`) void this.init.ensureInitialized(`watchDepth:${symbol}`)
.then(() => { .then(() => {
this.gateway.onDepth(symbol, safe); this.gateway.onDepth(symbol, safe);
}) });
.catch((error) => this.handleInitError("watchDepth", error));
} }
watchTicker(symbol: string, cb: TickerListener): void { watchTicker(symbol: string, cb: TickerListener): void {
const safe = this.safeInvoke("watchTicker", cb); const safe = this.safeInvoke("watchTicker", cb);
void this.ensureInitialized(`watchTicker:${symbol}`) void this.init.ensureInitialized(`watchTicker:${symbol}`)
.then(() => { .then(() => {
this.gateway.onTicker(symbol, safe); this.gateway.onTicker(symbol, safe);
}) });
.catch((error) => this.handleInitError("watchTicker", error));
} }
watchKlines(symbol: string, interval: string, cb: KlineListener): void { watchKlines(symbol: string, interval: string, cb: KlineListener): void {
const safe = this.safeInvoke("watchKlines", cb); const safe = this.safeInvoke("watchKlines", cb);
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`) void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`)
.then(() => { .then(() => {
this.gateway.onKlines(symbol, interval, safe); this.gateway.onKlines(symbol, interval, safe);
}) });
.catch((error) => this.handleInitError("watchKlines", error));
} }
watchFundingRate(symbol: string, cb: FundingRateListener): void { watchFundingRate(symbol: string, cb: FundingRateListener): void {
const safe = this.safeInvoke("watchFundingRate", cb); const safe = this.safeInvoke("watchFundingRate", cb);
void this.ensureInitialized(`watchFundingRate:${symbol}`) void this.init.ensureInitialized(`watchFundingRate:${symbol}`)
.then(() => { .then(() => {
this.gateway.onFundingRate(symbol, safe); this.gateway.onFundingRate(symbol, safe);
}) });
.catch((error) => this.handleInitError("watchFundingRate", error));
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized("createOrder"); await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params); return this.gateway.createOrder(params);
} }
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> { async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder"); await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params); await this.gateway.cancelOrder(params);
} }
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> { async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders"); await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params); await this.gateway.cancelOrders(params);
} }
async cancelAllOrders(params: { symbol: string }): Promise<void> { async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders"); await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params); await this.gateway.cancelAllOrders(params);
} }
async getPrecision(): Promise<ExchangePrecision | null> { async getPrecision(): Promise<ExchangePrecision | null> {
await this.ensureInitialized("getPrecision"); await this.init.ensureInitialized("getPrecision");
return this.gateway.getPrecision(this.symbol); return this.gateway.getPrecision(this.symbol);
} }
async queryOpenOrders(): Promise<AsterOrder[]> { async queryOpenOrders(): Promise<Order[]> {
await this.ensureInitialized("queryOpenOrders"); await this.init.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders(); return this.gateway.queryOpenOrders();
} }
async queryAccountSnapshot() { async queryAccountSnapshot() {
await this.ensureInitialized("queryAccountSnapshot"); await this.init.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot(); return this.gateway.queryAccountSnapshot();
} }
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> { async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
await this.ensureInitialized("changeMarginMode"); await this.init.ensureInitialized("changeMarginMode");
await this.gateway.changeMarginMode(params.symbol, params.marginMode); await this.gateway.changeMarginMode(params.symbol, params.marginMode);
} }
async forceCancelAllOrders(): Promise<boolean> { async forceCancelAllOrders(): Promise<boolean> {
await this.ensureInitialized("forceCancelAllOrders"); await this.init.ensureInitialized("forceCancelAllOrders");
return this.gateway.forceCancelAllOrders(); return this.gateway.forceCancelAllOrders();
} }
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[BinanceExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[BinanceExchangeAdapter] ${context} failed`, error);
}
} }
+37 -37
View File
@@ -1,21 +1,18 @@
import ccxt, { import ccxt, {
type Balances, type Balances,
type Order as CcxtOrder, type Order as CcxtOrder,
type OrderBook as CcxtOrderBook,
type OHLCV as CcxtOhlcv,
type Ticker as CcxtTicker,
} from "ccxt"; } from "ccxt";
import axios from "axios"; import axios from "axios";
import { createHash } from "crypto"; import { createHash } from "crypto";
import NodeWebSocket from "ws"; import NodeWebSocket from "ws";
import type { import type {
AsterAccountAsset, AccountAsset,
AsterAccountPosition, AccountPosition,
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
OrderType, OrderType,
PositionSide, PositionSide,
@@ -228,9 +225,9 @@ export class BinanceGateway {
private readonly accountListeners = new Set<AccountListener>(); private readonly accountListeners = new Set<AccountListener>();
private readonly orderListeners = new Set<OrderListener>(); private readonly orderListeners = new Set<OrderListener>();
private readonly depthSubs = new Map<string, PublicSubscription<AsterDepth>>(); private readonly depthSubs = new Map<string, PublicSubscription<Depth>>();
private readonly tickerSubs = new Map<string, PublicSubscription<AsterTicker>>(); private readonly tickerSubs = new Map<string, PublicSubscription<Ticker>>();
private readonly klineSubs = new Map<string, PublicSubscription<AsterKline[]>>(); private readonly klineSubs = new Map<string, PublicSubscription<Kline[]>>();
private readonly fundingSubs = new Map<string, PublicSubscription<{ symbol: string; fundingRate: number; updateTime: number }>>(); private readonly fundingSubs = new Map<string, PublicSubscription<{ symbol: string; fundingRate: number; updateTime: number }>>();
private accountPollTimer: ReturnType<typeof setInterval> | null = null; private accountPollTimer: ReturnType<typeof setInterval> | null = null;
@@ -257,15 +254,15 @@ export class BinanceGateway {
}, },
}; };
private readonly localOrders = new Map<string, AsterOrder>(); private readonly localOrders = new Map<string, Order>();
private readonly spotBalances = new Map<string, { free: number; locked: number }>(); private readonly spotBalances = new Map<string, { free: number; locked: number }>();
private readonly perpBalances = new Map<string, { wallet: number; available: number }>(); private readonly perpBalances = new Map<string, { wallet: number; available: number }>();
private readonly perpPositions = new Map<string, AsterAccountPosition>(); private readonly perpPositions = new Map<string, AccountPosition>();
private readonly lastMarkPriceBySymbol = new Map<string, number>(); private readonly lastMarkPriceBySymbol = new Map<string, number>();
private lastSpotSnapshot: AsterAccountSnapshot | null = null; private lastSpotSnapshot: AccountSnapshot | null = null;
private lastPerpSnapshot: AsterAccountSnapshot | null = null; private lastPerpSnapshot: AccountSnapshot | null = null;
constructor(options: BinanceGatewayOptions) { constructor(options: BinanceGatewayOptions) {
this.apiKey = options.apiKey; this.apiKey = options.apiKey;
@@ -468,7 +465,7 @@ export class BinanceGateway {
}); });
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized(params.symbol); await this.ensureInitialized(params.symbol);
const market = this.resolveMarket(params.symbol); const market = this.resolveMarket(params.symbol);
const exchange = this.getExchange(market.kind); const exchange = this.getExchange(market.kind);
@@ -490,6 +487,9 @@ export class BinanceGateway {
if (params.callbackRate != null) { if (params.callbackRate != null) {
extra.callbackRate = params.callbackRate; extra.callbackRate = params.callbackRate;
} }
if (params.clientOrderId != null) {
extra.newClientOrderId = params.clientOrderId;
}
if (market.kind === "perp") { if (market.kind === "perp") {
if (params.reduceOnly != null) { if (params.reduceOnly != null) {
@@ -604,10 +604,10 @@ export class BinanceGateway {
}; };
} }
async queryOpenOrders(): Promise<AsterOrder[]> { async queryOpenOrders(): Promise<Order[]> {
await this.ensureInitialized(this.defaultSymbol); await this.ensureInitialized(this.defaultSymbol);
const kinds = this.getPrivateKinds(); const kinds = this.getPrivateKinds();
const result: AsterOrder[] = []; const result: Order[] = [];
for (const kind of kinds) { for (const kind of kinds) {
const exchange = this.getExchange(kind); const exchange = this.getExchange(kind);
const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[]; const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[];
@@ -622,7 +622,7 @@ export class BinanceGateway {
return result; return result;
} }
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
await this.ensureInitialized(this.defaultSymbol); await this.ensureInitialized(this.defaultSymbol);
const kinds = this.getPrivateKinds(); const kinds = this.getPrivateKinds();
for (const kind of kinds) { for (const kind of kinds) {
@@ -913,7 +913,7 @@ export class BinanceGateway {
this.lastPerpSnapshot = this.buildPerpSnapshot(); this.lastPerpSnapshot = this.buildPerpSnapshot();
} }
private mapSpotExecutionReport(payload: any): AsterOrder | null { private mapSpotExecutionReport(payload: any): Order | null {
const order = payload; const order = payload;
const symbolRaw = String(order?.s ?? "").toUpperCase(); const symbolRaw = String(order?.s ?? "").toUpperCase();
if (!symbolRaw) return null; if (!symbolRaw) return null;
@@ -943,7 +943,7 @@ export class BinanceGateway {
}; };
} }
private mapPerpOrderTradeUpdate(payload: any): AsterOrder | null { private mapPerpOrderTradeUpdate(payload: any): Order | null {
const order = payload?.o; const order = payload?.o;
if (!order) return null; if (!order) return null;
const symbolRaw = String(order?.s ?? "").toUpperCase(); const symbolRaw = String(order?.s ?? "").toUpperCase();
@@ -1114,7 +1114,7 @@ export class BinanceGateway {
(ws as any).onerror = (event: any) => onError(event?.error ?? event); (ws as any).onerror = (event: any) => onError(event?.error ?? event);
} }
private mapDepthPayload(payload: unknown, market: BinanceMarketRef): AsterDepth | null { private mapDepthPayload(payload: unknown, market: BinanceMarketRef): Depth | null {
const data = payload as any; const data = payload as any;
const bids = Array.isArray(data?.b) ? data.b : []; const bids = Array.isArray(data?.b) ? data.b : [];
const asks = Array.isArray(data?.a) ? data.a : []; const asks = Array.isArray(data?.a) ? data.a : [];
@@ -1132,7 +1132,7 @@ export class BinanceGateway {
}; };
} }
private mapTickerPayload(payload: unknown, market: BinanceMarketRef): AsterTicker | null { private mapTickerPayload(payload: unknown, market: BinanceMarketRef): Ticker | null {
const data = payload as any; const data = payload as any;
const lastPrice = String(data?.c ?? ""); const lastPrice = String(data?.c ?? "");
if (!lastPrice) return null; if (!lastPrice) return null;
@@ -1159,7 +1159,7 @@ export class BinanceGateway {
}; };
} }
private mapKlinePayload(payload: unknown, interval: string): AsterKline[] | null { private mapKlinePayload(payload: unknown, interval: string): Kline[] | null {
const data = payload as any; const data = payload as any;
const kline = data?.k; const kline = data?.k;
if (!kline) return null; if (!kline) return null;
@@ -1291,7 +1291,7 @@ export class BinanceGateway {
} }
private async attachPerpPositions(): Promise<void> { private async attachPerpPositions(): Promise<void> {
const next = new Map<string, AsterAccountPosition>(); const next = new Map<string, AccountPosition>();
try { try {
const raw = (await this.perpExchange.fetchPositions()) as any[]; const raw = (await this.perpExchange.fetchPositions()) as any[];
for (const row of raw ?? []) { for (const row of raw ?? []) {
@@ -1328,7 +1328,7 @@ export class BinanceGateway {
private async fetchAndUpdateOrders(kind: MarketKind): Promise<void> { private async fetchAndUpdateOrders(kind: MarketKind): Promise<void> {
const exchange = this.getExchange(kind); const exchange = this.getExchange(kind);
const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[]; const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[];
const remote = new Map<string, AsterOrder>(); const remote = new Map<string, Order>();
for (const order of openOrders) { for (const order of openOrders) {
const market = this.resolveMarketByCcxtSymbol(kind, String(order?.symbol ?? "")); const market = this.resolveMarketByCcxtSymbol(kind, String(order?.symbol ?? ""));
const mapped = this.mapCcxtOrder(order, kind, market?.id ?? String(order?.symbol ?? "")); const mapped = this.mapCcxtOrder(order, kind, market?.id ?? String(order?.symbol ?? ""));
@@ -1370,7 +1370,7 @@ export class BinanceGateway {
} }
} }
private upsertOrder(order: AsterOrder, kind: MarketKind, marketId: string): void { private upsertOrder(order: Order, kind: MarketKind, marketId: string): void {
const id = String(order.orderId); const id = String(order.orderId);
const symbol = this.resolveDisplaySymbol(kind, marketId); const symbol = this.resolveDisplaySymbol(kind, marketId);
const normalized = { ...order, symbol }; const normalized = { ...order, symbol };
@@ -1405,7 +1405,7 @@ export class BinanceGateway {
if (changed) this.emitOrders(); if (changed) this.emitOrders();
} }
private isOrderActive(order: AsterOrder): boolean { private isOrderActive(order: Order): boolean {
const status = String(order.status ?? "").toUpperCase(); const status = String(order.status ?? "").toUpperCase();
if (!status) return true; if (!status) return true;
if (status === "FILLED" || status === "CANCELED" || status === "CANCELLED" || status === "REJECTED" || status === "EXPIRED") { if (status === "FILLED" || status === "CANCELED" || status === "CANCELLED" || status === "REJECTED" || status === "EXPIRED") {
@@ -1415,7 +1415,7 @@ export class BinanceGateway {
return true; return true;
} }
private buildCombinedAccountSnapshot(): AsterAccountSnapshot | null { private buildCombinedAccountSnapshot(): AccountSnapshot | null {
const kinds = this.getPrivateKinds(); const kinds = this.getPrivateKinds();
if (kinds.length === 0) return null; if (kinds.length === 0) return null;
if (kinds.length === 1) { if (kinds.length === 1) {
@@ -1424,7 +1424,7 @@ export class BinanceGateway {
const spot = this.buildSpotSnapshot(); const spot = this.buildSpotSnapshot();
const perp = this.buildPerpSnapshot(); const perp = this.buildPerpSnapshot();
const perpAssetsTagged: AsterAccountAsset[] = perp.assets.map((asset) => ({ const perpAssetsTagged: AccountAsset[] = perp.assets.map((asset) => ({
...asset, ...asset,
asset: `${asset.asset}0`, asset: `${asset.asset}0`,
})); }));
@@ -1444,8 +1444,8 @@ export class BinanceGateway {
}; };
} }
private buildSpotSnapshot(): AsterAccountSnapshot { private buildSpotSnapshot(): AccountSnapshot {
const assets: AsterAccountAsset[] = []; const assets: AccountAsset[] = [];
let totalWallet = 0; let totalWallet = 0;
const now = Date.now(); const now = Date.now();
for (const [asset, balance] of this.spotBalances.entries()) { for (const [asset, balance] of this.spotBalances.entries()) {
@@ -1474,9 +1474,9 @@ export class BinanceGateway {
}; };
} }
private buildPerpSnapshot(): AsterAccountSnapshot { private buildPerpSnapshot(): AccountSnapshot {
const now = Date.now(); const now = Date.now();
const assets: AsterAccountAsset[] = []; const assets: AccountAsset[] = [];
let totalWallet = 0; let totalWallet = 0;
for (const [asset, balance] of this.perpBalances.entries()) { for (const [asset, balance] of this.perpBalances.entries()) {
totalWallet += balance.wallet; totalWallet += balance.wallet;
@@ -1509,7 +1509,7 @@ export class BinanceGateway {
}; };
} }
private mapCcxtOrder(order: CcxtOrder, kind: MarketKind, marketId: string): AsterOrder { private mapCcxtOrder(order: CcxtOrder, kind: MarketKind, marketId: string): Order {
const symbol = this.resolveDisplaySymbol(kind, marketId); const symbol = this.resolveDisplaySymbol(kind, marketId);
const side = String(order.side ?? "buy").toUpperCase() === "SELL" ? "SELL" : "BUY"; const side = String(order.side ?? "buy").toUpperCase() === "SELL" ? "SELL" : "BUY";
return { return {
+14 -98
View File
@@ -1,100 +1,16 @@
import type { AsterOrder, CreateOrderParams } from "../types"; import { createOrderHandlers } from "../order-handlers";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { const handlers = createOrderHandlers({
if (params.quantity === undefined) { exchangeName: "Binance",
params.quantity = intent.quantity; defaultLimitTimeInForce: "GTX",
} supportsTrailingStop: true,
if (params.timeInForce === undefined && intent.timeInForce) { supportsTriggerType: true,
params.timeInForce = intent.timeInForce; });
}
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export const {
const params: CreateOrderParams = applyCommonFields( createLimitOrder,
{ createMarketOrder,
symbol: intent.symbol, createStopOrder,
side: intent.side, createTrailingStopOrder,
type: "LIMIT", createClosePositionOrder,
quantity: intent.quantity, } = handlers;
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
triggerType: intent.triggerType,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "TRAILING_STOP_MARKET",
quantity: intent.quantity,
activationPrice: intent.activationPrice,
callbackRate: intent.callbackRate,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
},
intent
);
return intent.adapter.createOrder(params);
}
+14 -1
View File
@@ -1,5 +1,5 @@
import type { ExchangeAdapter } from "./adapter"; import type { ExchangeAdapter } from "./adapter";
import { AsterExchangeAdapter, type AsterCredentials } from "./aster-adapter"; import { AsterExchangeAdapter, type AsterCredentials } from "./aster/adapter";
import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter"; import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter"; import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter"; import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
@@ -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,
});
} }
} }
+8 -8
View File
@@ -11,8 +11,8 @@ import type {
TickerListener, TickerListener,
} from "./adapter"; } from "./adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterOrder, Order,
CreateOrderParams, CreateOrderParams,
} from "./types"; } from "./types";
@@ -66,7 +66,7 @@ export class DryRunExchangeAdapter implements ExchangeAdapter {
this.inner.watchFundingRate(symbol, cb); this.inner.watchFundingRate(symbol, cb);
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
this.record("createOrder", params); this.record("createOrder", params);
return createSyntheticOrder(params, ++this.syntheticCounter); return createSyntheticOrder(params, ++this.syntheticCounter);
} }
@@ -104,12 +104,12 @@ export class DryRunExchangeAdapter implements ExchangeAdapter {
this.inner.offRestHealthEvent?.(listener); this.inner.offRestHealthEvent?.(listener);
} }
async queryOpenOrders(): Promise<AsterOrder[]> { async queryOpenOrders(): Promise<Order[]> {
if (!this.inner.queryOpenOrders) return []; if (!this.inner.queryOpenOrders) return [];
return this.inner.queryOpenOrders(); return this.inner.queryOpenOrders();
} }
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
if (!this.inner.queryAccountSnapshot) return null; if (!this.inner.queryAccountSnapshot) return null;
return this.inner.queryAccountSnapshot(); return this.inner.queryAccountSnapshot();
} }
@@ -128,12 +128,12 @@ export class DryRunExchangeAdapter implements ExchangeAdapter {
} }
} }
function createSyntheticOrder(params: CreateOrderParams, counter: number): AsterOrder { function createSyntheticOrder(params: CreateOrderParams, counter: number): Order {
const now = Date.now(); const now = Date.now();
const orderId = `dry-run-${now}-${counter}`; const orderId = params.clientOrderId ?? `dry-run-${now}-${counter}`;
return { return {
orderId, orderId,
clientOrderId: orderId, clientOrderId: params.clientOrderId ?? orderId,
symbol: params.symbol, symbol: params.symbol,
side: params.side, side: params.side,
type: params.type, type: params.type,
+17 -83
View File
@@ -1,4 +1,3 @@
import { setTimeout, clearTimeout } from "timers";
import path from "path"; import path from "path";
import { createRequire } from "module"; import { createRequire } from "module";
import type { import type {
@@ -9,8 +8,7 @@ import type {
OrderListener, OrderListener,
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types"; import type { Order, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import { import {
GrvtGateway, GrvtGateway,
type GrvtEnvironment, type GrvtEnvironment,
@@ -18,6 +16,7 @@ import {
type GrvtHostsOverride, type GrvtHostsOverride,
type GrvtSignatureProvider, type GrvtSignatureProvider,
} from "./gateway"; } from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
export interface GrvtCredentials { export interface GrvtCredentials {
cookie?: string; cookie?: string;
@@ -40,11 +39,8 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
private readonly gateway: GrvtGateway; private readonly gateway: GrvtGateway;
private readonly symbol: string; private readonly symbol: string;
private readonly instrument: string; private readonly instrument: string;
private initPromise: Promise<void> | null = null; private readonly safeInvoke = createSafeInvoke("GrvtExchangeAdapter");
private readonly initContexts = new Set<string>(); private readonly init: ReturnType<typeof createInitManager>;
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
private klineInterval = "1m"; private klineInterval = "1m";
constructor(credentials: GrvtCredentials = {}) { constructor(credentials: GrvtCredentials = {}) {
@@ -91,6 +87,9 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
pollIntervals: credentials.pollIntervals, pollIntervals: credentials.pollIntervals,
logger: credentials.logger, logger: credentials.logger,
}); });
this.init = createInitManager("GrvtExchangeAdapter", () =>
this.gateway.ensureInitialized(this.klineInterval),
);
} }
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
@@ -98,115 +97,50 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
} }
watchAccount(cb: AccountListener): void { watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount"); void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb)); this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
} }
watchOrders(cb: OrderListener): void { watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders"); void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb)); this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
} }
watchDepth(_symbol: string, cb: DepthListener): void { watchDepth(_symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth"); void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(this.safeInvoke("watchDepth", cb)); this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
} }
watchTicker(_symbol: string, cb: TickerListener): void { watchTicker(_symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker"); void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(this.safeInvoke("watchTicker", cb)); this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
} }
watchKlines(_symbol: string, interval: string, cb: KlineListener): void { watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
this.klineInterval = interval ?? this.klineInterval; this.klineInterval = interval ?? this.klineInterval;
void this.ensureInitialized("watchKlines", this.klineInterval); void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(this.safeInvoke("watchKlines", cb)); this.gateway.onKlines(this.safeInvoke("watchKlines", cb));
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized("createOrder"); await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params); return this.gateway.createOrder(params);
} }
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> { async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder"); await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params); await this.gateway.cancelOrder(params);
} }
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> { async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders"); await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params); await this.gateway.cancelOrders(params);
} }
async cancelAllOrders(_params: { symbol: string }): Promise<void> { async cancelAllOrders(_params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders"); await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(); await this.gateway.cancelAllOrders();
} }
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[GrvtExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string, interval?: string): Promise<void> {
if (interval) {
this.klineInterval = interval;
}
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.klineInterval)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[GrvtExchangeAdapter] ${context} failed`, error);
}
} }
function requireValue<T>(value: T | undefined | null, key: string): T { function requireValue<T>(value: T | undefined | null, key: string): T {
+52 -50
View File
@@ -23,21 +23,23 @@ import type {
IOrder, IOrder,
} from "@grvt/client/interfaces"; } from "@grvt/client/interfaces";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterAccountPosition, AccountPosition,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
OrderSide, OrderSide,
} from "../types";
import type {
GrvtSignedOrder, GrvtSignedOrder,
GrvtSignature, GrvtSignature,
GrvtUnsignedOrder, GrvtUnsignedOrder,
GrvtTimeInForce, GrvtTimeInForce,
GrvtOrderMetadataInput, GrvtOrderMetadataInput,
GrvtTriggerMetadata, GrvtTriggerMetadata,
} from "../types"; } from "./types";
const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000; const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000;
const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500; const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500;
@@ -172,7 +174,7 @@ interface HostsConfig {
interface KlineCache { interface KlineCache {
interval: string; interval: string;
values: AsterKline[]; values: Kline[];
} }
interface InstrumentInfo { interface InstrumentInfo {
@@ -215,12 +217,12 @@ export class GrvtGateway {
private instrumentInfo: InstrumentInfo | null = null; private instrumentInfo: InstrumentInfo | null = null;
private sessionPromise: Promise<void> | null = null; private sessionPromise: Promise<void> | null = null;
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private openOrders: AsterOrder[] = []; private openOrders: Order[] = [];
private positions: AsterAccountPosition[] = []; private positions: AccountPosition[] = [];
private lastPositionsUpdateAt: number = 0; private lastPositionsUpdateAt: number = 0;
private depthSnapshot: AsterDepth | null = null; private depthSnapshot: Depth | null = null;
private tickerSnapshot: AsterTicker | null = null; private tickerSnapshot: Ticker | null = null;
private klineCache: KlineCache | null = null; private klineCache: KlineCache | null = null;
// WebSocket state // WebSocket state
@@ -228,11 +230,11 @@ export class GrvtGateway {
private wsConnected = false; private wsConnected = false;
private positionsRefreshTimer: ReturnType<typeof setTimeout> | null = null; private positionsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
private accountListeners = new Set<(snapshot: AsterAccountSnapshot) => void>(); private accountListeners = new Set<(snapshot: AccountSnapshot) => void>();
private ordersListeners = new Set<(orders: AsterOrder[]) => void>(); private ordersListeners = new Set<(orders: Order[]) => void>();
private depthListeners = new Set<(depth: AsterDepth) => void>(); private depthListeners = new Set<(depth: Depth) => void>();
private tickerListeners = new Set<(ticker: AsterTicker) => void>(); private tickerListeners = new Set<(ticker: Ticker) => void>();
private klineListeners = new Set<(klines: AsterKline[]) => void>(); private klineListeners = new Set<(klines: Kline[]) => void>();
private accountTimer: ReturnType<typeof setInterval> | null = null; private accountTimer: ReturnType<typeof setInterval> | null = null;
private ordersTimer: ReturnType<typeof setInterval> | null = null; private ordersTimer: ReturnType<typeof setInterval> | null = null;
@@ -316,7 +318,7 @@ export class GrvtGateway {
this.initialized = true; this.initialized = true;
} }
onAccount(listener: (snapshot: AsterAccountSnapshot) => void): () => void { onAccount(listener: (snapshot: AccountSnapshot) => void): () => void {
this.accountListeners.add(listener); this.accountListeners.add(listener);
if (this.accountSnapshot) listener(cloneAccount(this.accountSnapshot)); if (this.accountSnapshot) listener(cloneAccount(this.accountSnapshot));
return () => { return () => {
@@ -324,7 +326,7 @@ export class GrvtGateway {
}; };
} }
onOrders(listener: (orders: AsterOrder[]) => void): () => void { onOrders(listener: (orders: Order[]) => void): () => void {
this.ordersListeners.add(listener); this.ordersListeners.add(listener);
if (this.openOrders.length) listener(cloneOrders(this.openOrders)); if (this.openOrders.length) listener(cloneOrders(this.openOrders));
return () => { return () => {
@@ -332,7 +334,7 @@ export class GrvtGateway {
}; };
} }
onDepth(listener: (depth: AsterDepth) => void): () => void { onDepth(listener: (depth: Depth) => void): () => void {
this.depthListeners.add(listener); this.depthListeners.add(listener);
if (this.depthSnapshot) listener(cloneDepth(this.depthSnapshot)); if (this.depthSnapshot) listener(cloneDepth(this.depthSnapshot));
return () => { return () => {
@@ -340,7 +342,7 @@ export class GrvtGateway {
}; };
} }
onTicker(listener: (ticker: AsterTicker) => void): () => void { onTicker(listener: (ticker: Ticker) => void): () => void {
this.tickerListeners.add(listener); this.tickerListeners.add(listener);
if (this.tickerSnapshot) listener(cloneTicker(this.tickerSnapshot)); if (this.tickerSnapshot) listener(cloneTicker(this.tickerSnapshot));
return () => { return () => {
@@ -348,7 +350,7 @@ export class GrvtGateway {
}; };
} }
onKlines(listener: (klines: AsterKline[]) => void): () => void { onKlines(listener: (klines: Kline[]) => void): () => void {
this.klineListeners.add(listener); this.klineListeners.add(listener);
if (this.klineCache) listener(cloneKlines(this.klineCache.values)); if (this.klineCache) listener(cloneKlines(this.klineCache.values));
return () => { return () => {
@@ -356,19 +358,19 @@ export class GrvtGateway {
}; };
} }
getAccountSnapshot(): AsterAccountSnapshot | null { getAccountSnapshot(): AccountSnapshot | null {
return this.accountSnapshot ? cloneAccount(this.accountSnapshot) : null; return this.accountSnapshot ? cloneAccount(this.accountSnapshot) : null;
} }
getOpenOrders(): AsterOrder[] { getOpenOrders(): Order[] {
return cloneOrders(this.openOrders); return cloneOrders(this.openOrders);
} }
getPositions(): AsterAccountPosition[] { getPositions(): AccountPosition[] {
return this.positions.map((position) => ({ ...position })); return this.positions.map((position) => ({ ...position }));
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
if (params.type === "TRAILING_STOP_MARKET") { if (params.type === "TRAILING_STOP_MARKET") {
throw new Error(TRAILING_NOT_SUPPORTED_ERROR); throw new Error(TRAILING_NOT_SUPPORTED_ERROR);
} }
@@ -660,7 +662,7 @@ export class GrvtGateway {
this.emitKlines(klines); this.emitKlines(klines);
} }
private mergeOrder(order: AsterOrder): void { private mergeOrder(order: Order): void {
const index = this.openOrders.findIndex((item) => String(item.orderId) === String(order.orderId)); const index = this.openOrders.findIndex((item) => String(item.orderId) === String(order.orderId));
if (index >= 0) { if (index >= 0) {
this.openOrders[index] = order; this.openOrders[index] = order;
@@ -678,7 +680,7 @@ export class GrvtGateway {
} }
} }
private emitAccount(snapshot: AsterAccountSnapshot): void { private emitAccount(snapshot: AccountSnapshot): void {
const cloned = cloneAccount(snapshot); const cloned = cloneAccount(snapshot);
this.accountListeners.forEach((listener) => { this.accountListeners.forEach((listener) => {
try { try {
@@ -689,7 +691,7 @@ export class GrvtGateway {
}); });
} }
private emitOrders(orders: AsterOrder[]): void { private emitOrders(orders: Order[]): void {
const cloned = cloneOrders(orders); const cloned = cloneOrders(orders);
this.ordersListeners.forEach((listener) => { this.ordersListeners.forEach((listener) => {
try { try {
@@ -700,7 +702,7 @@ export class GrvtGateway {
}); });
} }
private emitDepth(depth: AsterDepth): void { private emitDepth(depth: Depth): void {
const cloned = cloneDepth(depth); const cloned = cloneDepth(depth);
this.depthListeners.forEach((listener) => { this.depthListeners.forEach((listener) => {
try { try {
@@ -711,7 +713,7 @@ export class GrvtGateway {
}); });
} }
private emitTicker(ticker: AsterTicker): void { private emitTicker(ticker: Ticker): void {
const cloned = cloneTicker(ticker); const cloned = cloneTicker(ticker);
this.tickerListeners.forEach((listener) => { this.tickerListeners.forEach((listener) => {
try { try {
@@ -722,7 +724,7 @@ export class GrvtGateway {
}); });
} }
private emitKlines(klines: AsterKline[]): void { private emitKlines(klines: Kline[]): void {
const cloned = cloneKlines(klines); const cloned = cloneKlines(klines);
this.klineListeners.forEach((listener) => { this.klineListeners.forEach((listener) => {
try { try {
@@ -974,15 +976,15 @@ function normalizeHex(value: string): string {
return `0x${trimmed.toLowerCase()}`; return `0x${trimmed.toLowerCase()}`;
} }
function cloneAccount(snapshot: AsterAccountSnapshot): AsterAccountSnapshot { function cloneAccount(snapshot: AccountSnapshot): AccountSnapshot {
return JSON.parse(JSON.stringify(snapshot)); return JSON.parse(JSON.stringify(snapshot));
} }
function cloneOrders(orders: AsterOrder[]): AsterOrder[] { function cloneOrders(orders: Order[]): Order[] {
return orders.map((order) => ({ ...order })); return orders.map((order) => ({ ...order }));
} }
function cloneDepth(depth: AsterDepth): AsterDepth { function cloneDepth(depth: Depth): Depth {
return { return {
...depth, ...depth,
bids: depth.bids.map((level) => [...level] as [string, string]), bids: depth.bids.map((level) => [...level] as [string, string]),
@@ -990,11 +992,11 @@ function cloneDepth(depth: AsterDepth): AsterDepth {
}; };
} }
function cloneTicker(ticker: AsterTicker): AsterTicker { function cloneTicker(ticker: Ticker): Ticker {
return { ...ticker }; return { ...ticker };
} }
function cloneKlines(klines: AsterKline[]): AsterKline[] { function cloneKlines(klines: Kline[]): Kline[] {
return klines.map((kline) => ({ ...kline })); return klines.map((kline) => ({ ...kline }));
} }
@@ -1023,11 +1025,11 @@ function scaleDecimal(value: string | number | undefined, decimals: number): big
return scaleDecimal(fixed.toFixed(decimals), decimals); return scaleDecimal(fixed.toFixed(decimals), decimals);
} }
function sumUnrealized(positions: AsterAccountPosition[]): number { function sumUnrealized(positions: AccountPosition[]): number {
return positions.reduce((total, position) => total + Number(position.unrealizedProfit ?? 0), 0); return positions.reduce((total, position) => total + Number(position.unrealizedProfit ?? 0), 0);
} }
function getNewestPositionEventTime(positions: AsterAccountPosition[]): number { function getNewestPositionEventTime(positions: AccountPosition[]): number {
if (!positions.length) return Date.now(); if (!positions.length) return Date.now();
return positions.reduce((max, p) => Math.max(max, Number(p.updateTime) || 0), 0); return positions.reduce((max, p) => Math.max(max, Number(p.updateTime) || 0), 0);
} }
@@ -1036,7 +1038,7 @@ function mapAccountSnapshot(
response: IApiSubAccountSummaryResponse, response: IApiSubAccountSummaryResponse,
symbol: string, symbol: string,
instrument: string instrument: string
): AsterAccountSnapshot { ): AccountSnapshot {
const result = response.result; const result = response.result;
if (!result) { if (!result) {
return emptyAccount(symbol); return emptyAccount(symbol);
@@ -1068,7 +1070,7 @@ function mapPositions(
response: IApiPositionsResponse, response: IApiPositionsResponse,
symbol: string, symbol: string,
instrument: string instrument: string
): AsterAccountPosition[] { ): AccountPosition[] {
return (response.result ?? []) return (response.result ?? [])
.filter((entry) => !entry.instrument || entry.instrument === instrument) .filter((entry) => !entry.instrument || entry.instrument === instrument)
.map((entry) => ({ .map((entry) => ({
@@ -1084,11 +1086,11 @@ function mapPositions(
})); }));
} }
function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): AsterOrder[] { function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): Order[] {
return (response.result ?? []).map((order) => mapOrder(order, symbol)); return (response.result ?? []).map((order) => mapOrder(order, symbol));
} }
function mapOrder(order: IOrder, symbol: string): AsterOrder { function mapOrder(order: IOrder, symbol: string): Order {
const leg = order.legs?.[0]; const leg = order.legs?.[0];
const state = order.state; const state = order.state;
const metadata = order.metadata; const metadata = order.metadata;
@@ -1126,7 +1128,7 @@ function mapOrder(order: IOrder, symbol: string): AsterOrder {
}; };
} }
function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): AsterDepth | null { function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): Depth | null {
const result = response.result; const result = response.result;
if (!result) return null; if (!result) return null;
const toLevel = (entries: Array<{ price?: string; size?: string }> | undefined) => const toLevel = (entries: Array<{ price?: string; size?: string }> | undefined) =>
@@ -1140,7 +1142,7 @@ function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): AsterD
}; };
} }
function mapTicker(response: IApiTickerResponse, symbol: string): AsterTicker | null { function mapTicker(response: IApiTickerResponse, symbol: string): Ticker | null {
const result = response.result; const result = response.result;
if (!result) return null; if (!result) return null;
const buyVolume = Number(result.buy_volume_24h_b ?? 0); const buyVolume = Number(result.buy_volume_24h_b ?? 0);
@@ -1161,7 +1163,7 @@ function mapTicker(response: IApiTickerResponse, symbol: string): AsterTicker |
}; };
} }
function mapKlines(response: IApiCandlestickResponse, symbol: string): AsterKline[] { function mapKlines(response: IApiCandlestickResponse, _symbol: string): Kline[] {
return (response.result ?? []).reverse().map((entry) => ({ return (response.result ?? []).reverse().map((entry) => ({
openTime: nsToMs(entry.open_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS), openTime: nsToMs(entry.open_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
closeTime: nsToMs(entry.close_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS), closeTime: nsToMs(entry.close_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
@@ -1174,7 +1176,7 @@ function mapKlines(response: IApiCandlestickResponse, symbol: string): AsterKlin
})); }));
} }
function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): AsterOrder { function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): Order {
const order = response.result ?? (response as unknown as { order?: IOrder }).order; const order = response.result ?? (response as unknown as { order?: IOrder }).order;
if (!order) { if (!order) {
return { return {
@@ -1551,7 +1553,7 @@ function padPrivateKey(value: string): string {
return hex; return hex;
} }
function emptyAccount(symbol: string): AsterAccountSnapshot { function emptyAccount(symbol: string): AccountSnapshot {
return { return {
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
@@ -1568,5 +1570,5 @@ function emptyAccount(symbol: string): AsterAccountSnapshot {
updateTime: Date.now(), updateTime: Date.now(),
}, },
], ],
} as AsterAccountSnapshot; } as AccountSnapshot;
} }
+18 -89
View File
@@ -1,92 +1,21 @@
import type { AsterOrder, CreateOrderParams } from "../types"; import { createOrderHandlers } from "../order-handlers";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { const handlers = createOrderHandlers({
if (params.quantity === undefined) { exchangeName: "GRVT",
params.quantity = intent.quantity; defaultLimitTimeInForce: "GTX",
} supportsTrailingStop: false,
if (params.timeInForce === undefined && intent.timeInForce) { supportsTriggerType: true,
params.timeInForce = intent.timeInForce; defaultStopTriggerType: "STOP_LOSS",
} stopDefaultReduceOnly: true,
if (intent.reduceOnly !== undefined) { stopDefaultClosePosition: true,
params.reduceOnly = toStringBoolean(intent.reduceOnly); closeDefaultClosePosition: true,
} });
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export const {
const params: CreateOrderParams = applyCommonFields( createLimitOrder,
{ createMarketOrder,
symbol: intent.symbol, createStopOrder,
side: intent.side, createTrailingStopOrder,
type: "LIMIT", createClosePositionOrder,
quantity: intent.quantity, } = handlers;
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
triggerType: intent.triggerType ?? "STOP_LOSS",
closePosition: toStringBoolean(intent.closePosition ?? true),
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
throw new Error("GRVT exchange does not support trailing stop orders");
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
closePosition: toStringBoolean(intent.closePosition ?? true),
},
intent
);
return intent.adapter.createOrder(params);
}
+202
View File
@@ -0,0 +1,202 @@
export interface GrvtOrderLeg {
instrument: string;
size: string;
limit_price?: string;
is_buying_asset?: boolean;
}
export type GrvtTimeInForce =
| "GOOD_TILL_TIME"
| "ALL_OR_NONE"
| "IMMEDIATE_OR_CANCEL"
| "FILL_OR_KILL";
export interface GrvtOrderMetadata {
client_order_id?: string;
create_time?: string;
broker?: string | null;
trigger?: GrvtTriggerMetadata;
}
export interface GrvtOrderState {
status?: string;
reject_reason?: string | null;
book_size?: string[];
traded_size?: string[];
update_time?: string;
avg_fill_price?: string[];
}
export interface GrvtOrder {
order_id: string;
client_order_id?: string;
sub_account_id?: string;
is_market?: boolean;
time_in_force?: GrvtTimeInForce;
post_only?: boolean;
reduce_only?: boolean;
legs?: GrvtOrderLeg[];
metadata?: GrvtOrderMetadata;
state?: GrvtOrderState;
instrument?: string;
}
export interface GrvtTrade {
price: string;
size: string;
taker_side: "BUY" | "SELL";
timestamp: string;
}
export interface GrvtTradeHistoryResponse {
result?: GrvtTrade[];
}
export interface GrvtWebsocketMessage<T> {
stream: string;
selector: string;
sequence_number?: string;
feed: T;
}
export interface GrvtOrderUpdateFeed {
order_id: string;
client_order_id?: string;
sub_account_id?: string;
state?: GrvtOrderState;
traded_size?: string[];
update_time?: string;
}
export interface GrvtPositionUpdateFeed {
instrument: string;
size: string;
entry_price?: string;
mark_price?: string;
unrealized_pnl?: string;
sub_account_id?: string;
update_time?: string;
}
export interface GrvtDepthUpdateFeed {
instrument: string;
bids: GrvtDepthLevel[];
asks: GrvtDepthLevel[];
event_time?: string;
}
export interface GrvtTickerUpdateFeed {
instrument: string;
mark_price?: string;
last_trade_price?: string;
best_bid_price?: string;
best_ask_price?: string;
volume_24h?: string;
}
export interface GrvtOpenOrdersResponse {
result?: GrvtOrder[];
}
export interface GrvtPositionsResponse {
result?: GrvtPosition[];
}
export interface GrvtPosition {
instrument: string;
size: string;
entry_price?: string;
mark_price?: string;
unrealized_pnl?: string;
}
export interface GrvtAccountSnapshot {
total_unrealized_pnl?: string;
positions: GrvtPosition[];
settle_currency?: string;
available_balance?: string;
}
export interface GrvtBalancesResponse {
result?: {
total_unrealized_pnl?: string;
positions?: GrvtPosition[];
};
}
export interface GrvtDepthLevel {
price: string;
size: string;
}
export interface GrvtDepth {
instrument: string;
event_time?: string;
bids: GrvtDepthLevel[];
asks: GrvtDepthLevel[];
}
export interface GrvtTicker {
instrument: string;
mark_price?: string;
last_trade_price?: string;
best_bid_price?: string;
best_ask_price?: string;
volume_24h?: string;
}
export interface GrvtKline {
open_time: number;
close_time: number;
open: string;
high: string;
low: string;
close: string;
volume: string;
number_of_trades?: number;
}
export interface GrvtSignature {
signer: string;
r: string;
s: string;
v: number;
expiration: string;
nonce: number;
}
export interface GrvtUnsignedOrderLeg {
instrument: string;
size: string;
limit_price?: string;
is_buying_asset: boolean;
}
export interface GrvtTriggerMetadata {
trigger_type: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
tpsl: {
trigger_by: "UNSPECIFIED" | "INDEX" | "LAST" | "MID" | "MARK";
trigger_price: string;
close_position: boolean;
};
}
export interface GrvtOrderMetadataInput {
client_order_id: string;
trigger?: GrvtTriggerMetadata;
broker?: string | null;
}
export interface GrvtUnsignedOrder {
sub_account_id: string;
is_market: boolean;
time_in_force: GrvtTimeInForce;
post_only: boolean;
reduce_only: boolean;
legs: GrvtUnsignedOrderLeg[];
metadata: GrvtOrderMetadataInput;
}
export interface GrvtSignedOrder extends GrvtUnsignedOrder {
signature: GrvtSignature;
}
+2 -2
View File
@@ -7,7 +7,7 @@ import type {
OrderListener, OrderListener,
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types"; import type { Order, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors"; import { extractMessage } from "../../utils/errors";
import { LighterGateway, type LighterGatewayOptions } from "./gateway"; import { LighterGateway, type LighterGatewayOptions } from "./gateway";
@@ -102,7 +102,7 @@ export class LighterExchangeAdapter implements ExchangeAdapter {
this.gateway.watchKlines(interval, handler); this.gateway.watchKlines(interval, handler);
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized("createOrder"); await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params); return this.gateway.createOrder(params);
} }
+19 -22
View File
@@ -8,20 +8,18 @@ import type {
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { import type {
AsterAccountAsset, AccountAsset,
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
} from "../types"; } from "../types";
import { extractMessage } from "../../utils/errors";
import type { OrderSide, OrderType } from "../types"; import type { OrderSide, OrderType } from "../types";
import { LighterHttpClient } from "./http-client"; import { LighterHttpClient } from "./http-client";
import { HttpNonceManager } from "./nonce-manager"; import { HttpNonceManager } from "./nonce-manager";
import { LighterSigner, type CreateOrderSignParams } from "./signer"; import { LighterSigner, type CreateOrderSignParams } from "./signer";
import { bytesToHex } from "./bytes";
import type { import type {
LighterAccountDetails, LighterAccountDetails,
LighterAccountAsset, LighterAccountAsset,
@@ -39,7 +37,6 @@ import {
LIGHTER_HOSTS, LIGHTER_HOSTS,
LIGHTER_ORDER_TYPE, LIGHTER_ORDER_TYPE,
LIGHTER_TIME_IN_FORCE, LIGHTER_TIME_IN_FORCE,
DEFAULT_ORDER_EXPIRY_PLACEHOLDER,
IMMEDIATE_OR_CANCEL_EXPIRY_PLACEHOLDER, IMMEDIATE_OR_CANCEL_EXPIRY_PLACEHOLDER,
type LighterEnvironment, type LighterEnvironment,
} from "./constants"; } from "./constants";
@@ -192,12 +189,12 @@ export class LighterGateway {
private accountPollInFlight = false; private accountPollInFlight = false;
private ordersResyncTimer: ReturnType<typeof setInterval> | null = null; private ordersResyncTimer: ReturnType<typeof setInterval> | null = null;
private ordersResyncInFlight = false; private ordersResyncInFlight = false;
private readonly klineCache = new Map<string, AsterKline[]>(); private readonly klineCache = new Map<string, Kline[]>();
private readonly accountEvent = createEvent<AsterAccountSnapshot>(); private readonly accountEvent = createEvent<AccountSnapshot>();
private readonly ordersEvent = createEvent<AsterOrder[]>(); private readonly ordersEvent = createEvent<Order[]>();
private readonly depthEvent = createEvent<AsterDepth>(); private readonly depthEvent = createEvent<Depth>();
private readonly tickerEvent = createEvent<AsterTicker>(); private readonly tickerEvent = createEvent<Ticker>();
private readonly klinesEvent = createEvent<AsterKline[]>(); private readonly klinesEvent = createEvent<Kline[]>();
private readonly auth = { token: null as string | null, expiresAt: 0 }; private readonly auth = { token: null as string | null, expiresAt: 0 };
private readonly l1Address: string | null; private readonly l1Address: string | null;
private loggedCreateOrderPayload = false; private loggedCreateOrderPayload = false;
@@ -362,8 +359,8 @@ export class LighterGateway {
this.klinesEvent.add(handler); this.klinesEvent.add(handler);
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
const run = async (): Promise<AsterOrder> => { const run = async (): Promise<Order> => {
await this.ensureInitialized(); await this.ensureInitialized();
const conversion = this.mapCreateOrderParams(params); const conversion = this.mapCreateOrderParams(params);
const { baseAmountScaledString, priceScaledString, triggerPriceScaledString, ...signParams } = conversion; const { baseAmountScaledString, priceScaledString, triggerPriceScaledString, ...signParams } = conversion;
@@ -1713,7 +1710,7 @@ export class LighterGateway {
const bestAsk = getBestPrice(this.orderBook.asks, "ask"); const bestAsk = getBestPrice(this.orderBook.asks, "ask");
if (bestBid == null && bestAsk == null) return; if (bestBid == null && bestAsk == null) return;
const last = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : (bestBid ?? bestAsk ?? 0); const last = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : (bestBid ?? bestAsk ?? 0);
const ticker: AsterTicker = { const ticker: Ticker = {
symbol: this.displaySymbol, symbol: this.displaySymbol,
eventType: "lighterSyntheticTicker", eventType: "lighterSyntheticTicker",
eventTime: Date.now(), eventTime: Date.now(),
@@ -1739,10 +1736,10 @@ export class LighterGateway {
this.tickerEvent.emit(ticker); this.tickerEvent.emit(ticker);
} }
private buildAccountAssets(): AsterAccountAsset[] { private buildAccountAssets(): AccountAsset[] {
if (!this.assets.size) return []; if (!this.assets.size) return [];
const now = Date.now(); const now = Date.now();
const list: AsterAccountAsset[] = []; const list: AccountAsset[] = [];
for (const asset of this.assets.values()) { for (const asset of this.assets.values()) {
const balanceNum = parseNumber(asset.balance); const balanceNum = parseNumber(asset.balance);
const lockedNum = parseNumber(asset.locked_balance ?? 0); const lockedNum = parseNumber(asset.locked_balance ?? 0);
@@ -2042,7 +2039,7 @@ function mergeLevels(existing: LighterOrderBookLevel[], updates: LighterOrderBoo
return Array.from(map.entries()).map(([price, size]) => ({ price, size } as LighterOrderBookLevel)); return Array.from(map.entries()).map(([price, size]) => ({ price, size } as LighterOrderBookLevel));
} }
function cloneKlines(klines: AsterKline[]): AsterKline[] { function cloneKlines(klines: Kline[]): Kline[] {
return klines.map((kline) => ({ ...kline })); return klines.map((kline) => ({ ...kline }));
} }
@@ -2210,7 +2207,7 @@ function extractJsonRequestId(message: any): string | null {
function tryParseTxInfo(value: string): unknown { function tryParseTxInfo(value: string): unknown {
try { try {
return JSON.parse(value); return JSON.parse(value);
} catch (_) { } catch {
return value; return value;
} }
} }
+22 -22
View File
@@ -1,12 +1,12 @@
import type { import type {
AsterAccountAsset, AccountAsset,
AsterAccountPosition, AccountPosition,
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterDepthLevel, DepthLevel,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
OrderSide, OrderSide,
OrderType, OrderType,
} from "../types"; } from "../types";
@@ -23,8 +23,8 @@ import { coerceBooleanFlag, normalizeBooleanFlag } from "./flags";
import { normalizeOrderIdentity } from "./order-identity"; import { normalizeOrderIdentity } from "./order-identity";
import { normalizeOrderStatus } from "./status"; import { normalizeOrderStatus } from "./status";
export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): AsterDepth { export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): Depth {
const toLevels = (levels: LighterOrderBookLevel[]): AsterDepthLevel[] => const toLevels = (levels: LighterOrderBookLevel[]): DepthLevel[] =>
levels.map((level) => [level.price, level.size]); levels.map((level) => [level.price, level.size]);
return { return {
symbol, symbol,
@@ -36,7 +36,7 @@ export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): Ast
}; };
} }
export function toTicker(symbol: string, stats: LighterMarketStats): AsterTicker { export function toTicker(symbol: string, stats: LighterMarketStats): Ticker {
return { return {
symbol, symbol,
eventType: "lighterTicker", eventType: "lighterTicker",
@@ -50,10 +50,10 @@ export function toTicker(symbol: string, stats: LighterMarketStats): AsterTicker
priceChange: stats.daily_price_change != null ? String(stats.daily_price_change) : undefined, priceChange: stats.daily_price_change != null ? String(stats.daily_price_change) : undefined,
markPrice: stats.mid_price ?? stats.mark_price ?? stats.index_price, markPrice: stats.mid_price ?? stats.mark_price ?? stats.index_price,
weightedAvgPrice: undefined, weightedAvgPrice: undefined,
} as AsterTicker; } as Ticker;
} }
export function toKlines(symbol: string, interval: string, klines: LighterKline[]): AsterKline[] { export function toKlines(symbol: string, interval: string, klines: LighterKline[]): Kline[] {
return klines.map((entry) => ({ return klines.map((entry) => ({
symbol, symbol,
eventType: "lighterKline", eventType: "lighterKline",
@@ -72,11 +72,11 @@ export function toKlines(symbol: string, interval: string, klines: LighterKline[
})); }));
} }
export function toOrders(symbol: string, orders: LighterOrder[]): AsterOrder[] { export function toOrders(symbol: string, orders: LighterOrder[]): Order[] {
return orders.map((order) => lighterOrderToAster(symbol, order)); return orders.map((order) => lighterOrderToAster(symbol, order));
} }
export function lighterOrderToAster(symbol: string, order: LighterOrder): AsterOrder { export function lighterOrderToAster(symbol: string, order: LighterOrder): Order {
const booleanIsAsk = normalizeBooleanFlag(order.is_ask); const booleanIsAsk = normalizeBooleanFlag(order.is_ask);
const normalizedSide = order.side?.toLowerCase(); const normalizedSide = order.side?.toLowerCase();
const side: OrderSide = const side: OrderSide =
@@ -128,7 +128,7 @@ function computeExecutedQty(order: LighterOrder): string {
if (Number.isFinite(initial) && Number.isFinite(remaining)) { if (Number.isFinite(initial) && Number.isFinite(remaining)) {
return (initial - remaining).toString(); return (initial - remaining).toString();
} }
} catch (_) { } catch {
// fall through // fall through
} }
} }
@@ -162,7 +162,7 @@ export function toAccountSnapshot(
symbol: string, symbol: string,
details: LighterAccountDetails, details: LighterAccountDetails,
positions: LighterPosition[] = [], positions: LighterPosition[] = [],
assets: AsterAccountAsset[] = [], assets: AccountAsset[] = [],
options?: { options?: {
marketSymbol?: string | null; marketSymbol?: string | null;
marketId?: number | null; marketId?: number | null;
@@ -172,7 +172,7 @@ export function toAccountSnapshot(
baseAssetId?: number | null; baseAssetId?: number | null;
quoteAssetId?: number | null; quoteAssetId?: number | null;
} }
): AsterAccountSnapshot { ): AccountSnapshot {
const targetSymbol = options?.marketSymbol ?? null; const targetSymbol = options?.marketSymbol ?? null;
const targetMarketId = const targetMarketId =
options?.marketId != null && Number.isFinite(Number(options.marketId)) options?.marketId != null && Number.isFinite(Number(options.marketId))
@@ -228,7 +228,7 @@ export function toAccountSnapshot(
}; };
} }
function defaultAsset(details: LighterAccountDetails, quoteAsset: string): AsterAccountAsset[] { function defaultAsset(details: LighterAccountDetails, quoteAsset: string): AccountAsset[] {
return [ return [
{ {
asset: quoteAsset || "USDC", asset: quoteAsset || "USDC",
@@ -239,7 +239,7 @@ function defaultAsset(details: LighterAccountDetails, quoteAsset: string): Aster
]; ];
} }
function computeTotalWalletBalance(assets: AsterAccountAsset[], details: LighterAccountDetails): string { function computeTotalWalletBalance(assets: AccountAsset[], details: LighterAccountDetails): string {
const sum = assets.reduce((acc, asset) => acc + Number(asset.walletBalance ?? 0), 0); const sum = assets.reduce((acc, asset) => acc + Number(asset.walletBalance ?? 0), 0);
if (Number.isFinite(sum) && sum > 0) { if (Number.isFinite(sum) && sum > 0) {
return sum.toString(); return sum.toString();
@@ -247,7 +247,7 @@ function computeTotalWalletBalance(assets: AsterAccountAsset[], details: Lighter
return details.total_asset_value ?? details.collateral ?? "0"; return details.total_asset_value ?? details.collateral ?? "0";
} }
function findAsset(assets: AsterAccountAsset[], target: string | undefined | null): AsterAccountAsset | undefined { function findAsset(assets: AccountAsset[], target: string | undefined | null): AccountAsset | undefined {
if (!target) return undefined; if (!target) return undefined;
const normalizedTarget = target.toUpperCase().split(/[-:/]/)[0]; const normalizedTarget = target.toUpperCase().split(/[-:/]/)[0];
return assets.find((asset) => asset.asset.toUpperCase().split(/[-:/]/)[0] === normalizedTarget); return assets.find((asset) => asset.asset.toUpperCase().split(/[-:/]/)[0] === normalizedTarget);
@@ -261,7 +261,7 @@ function normalizeMarketType(value: string | null | undefined): "perp" | "spot"
return undefined; return undefined;
} }
function lighterPositionToAster(symbol: string, position: LighterPosition): AsterAccountPosition { function lighterPositionToAster(symbol: string, position: LighterPosition): AccountPosition {
const sign = position.sign ?? 0; const sign = position.sign ?? 0;
const positionSide = sign > 0 ? "LONG" : sign < 0 ? "SHORT" : "BOTH"; const positionSide = sign > 0 ? "LONG" : sign < 0 ? "SHORT" : "BOTH";
const magnitude = Number(position.position ?? 0); const magnitude = Number(position.position ?? 0);
+19 -90
View File
@@ -1,93 +1,22 @@
import type { AsterOrder, CreateOrderParams } from "../types"; import { createOrderHandlers } from "../order-handlers";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { const handlers = createOrderHandlers({
if (params.quantity === undefined) { exchangeName: "Lighter",
params.quantity = intent.quantity; defaultLimitTimeInForce: "GTC",
} defaultMarketTimeInForce: "IOC",
if (params.timeInForce === undefined && intent.timeInForce) { defaultCloseTimeInForce: "IOC",
params.timeInForce = intent.timeInForce; supportsTrailingStop: false,
} supportsTriggerType: false,
if (intent.reduceOnly !== undefined) { stopDefaultReduceOnly: true,
params.reduceOnly = toStringBoolean(intent.reduceOnly); stopDefaultClosePosition: true,
} closeDefaultClosePosition: true,
if (intent.closePosition !== undefined) { });
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export const {
const params: CreateOrderParams = applyCommonFields( createLimitOrder,
{ createMarketOrder,
symbol: intent.symbol, createStopOrder,
side: intent.side, createTrailingStopOrder,
type: "LIMIT", createClosePositionOrder,
quantity: intent.quantity, } = handlers;
price: intent.price,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
timeInForce: intent.timeInForce ?? "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
closePosition: toStringBoolean(intent.closePosition ?? true),
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
throw new Error("Lighter exchange does not support trailing stop orders");
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
closePosition: toStringBoolean(intent.closePosition ?? true),
timeInForce: intent.timeInForce ?? "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
+18 -82
View File
@@ -1,4 +1,3 @@
import { setTimeout, clearTimeout } from "timers";
import type { import type {
AccountListener, AccountListener,
DepthListener, DepthListener,
@@ -9,9 +8,10 @@ import type {
OrderListener, OrderListener,
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types"; import type { Order, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors"; import { extractMessage } from "../../utils/errors";
import { NadoGateway, type NadoGatewayOptions } from "./gateway"; import { NadoGateway, type NadoGatewayOptions } from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
import type { ChainEnv } from "@nadohq/shared"; import type { ChainEnv } from "@nadohq/shared";
import type { Address } from "viem"; import type { Address } from "viem";
@@ -35,11 +35,8 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
private readonly gateway: NadoGateway; private readonly gateway: NadoGateway;
private readonly symbol: string; private readonly symbol: string;
private initPromise: Promise<void> | null = null; private readonly safeInvoke = createSafeInvoke("NadoExchangeAdapter");
private readonly initContexts = new Set<string>(); private readonly init: ReturnType<typeof createInitManager>;
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: NadoCredentials = {}) { constructor(credentials: NadoCredentials = {}) {
const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY; const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY;
@@ -79,6 +76,9 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
(process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined), (process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined),
logger: (context, error) => this.logError(context, error), logger: (context, error) => this.logError(context, error),
}); });
this.init = createInitManager("NadoExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
} }
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
@@ -86,52 +86,52 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
} }
watchAccount(cb: AccountListener): void { watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount"); void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb)); this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
} }
watchOrders(cb: OrderListener): void { watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders"); void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb)); this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
} }
watchDepth(symbol: string, cb: DepthListener): void { watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized(`watchDepth:${symbol}`); void this.init.ensureInitialized(`watchDepth:${symbol}`);
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb)); this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
} }
watchTicker(symbol: string, cb: TickerListener): void { watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized(`watchTicker:${symbol}`); void this.init.ensureInitialized(`watchTicker:${symbol}`);
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb)); this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
} }
watchKlines(symbol: string, interval: string, cb: KlineListener): void { watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`); void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb)); this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
} }
watchFundingRate(symbol: string, cb: FundingRateListener): void { watchFundingRate(symbol: string, cb: FundingRateListener): void {
void this.ensureInitialized(`watchFundingRate:${symbol}`); void this.init.ensureInitialized(`watchFundingRate:${symbol}`);
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb)); this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized("createOrder"); await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params); return this.gateway.createOrder(params);
} }
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> { async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder"); await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params); await this.gateway.cancelOrder(params);
} }
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> { async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders"); await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params); await this.gateway.cancelOrders(params);
} }
async cancelAllOrders(params: { symbol: string }): Promise<void> { async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders"); await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params); await this.gateway.cancelAllOrders(params);
} }
@@ -144,70 +144,6 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
} }
} }
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[NadoExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[NadoExchangeAdapter] ${context} failed`, error);
}
private logError(context: string, error: unknown): void { private logError(context: string, error: unknown): void {
const detail = extractMessage(error); const detail = extractMessage(error);
const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`; const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`;
+23 -23
View File
@@ -23,13 +23,13 @@ import type {
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { import type {
AsterAccountAsset, AccountAsset,
AsterAccountPosition, AccountPosition,
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
TimeInForce, TimeInForce,
} from "../types"; } from "../types";
@@ -331,7 +331,7 @@ export class NadoGateway {
private readonly openOrdersByDigest = new Map<string, LocalOrder>(); private readonly openOrdersByDigest = new Map<string, LocalOrder>();
private readonly triggerOrdersByDigest = new Map<string, TriggerOrder>(); private readonly triggerOrdersByDigest = new Map<string, TriggerOrder>();
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private lastAccountSyncAt = 0; private lastAccountSyncAt = 0;
private gatewayWs: NodeWebSocket | null = null; private gatewayWs: NodeWebSocket | null = null;
@@ -356,7 +356,7 @@ export class NadoGateway {
private accountPollTimer: ReturnType<typeof setInterval> | null = null; private accountPollTimer: ReturnType<typeof setInterval> | null = null;
private ordersPollTimer: ReturnType<typeof setInterval> | null = null; private ordersPollTimer: ReturnType<typeof setInterval> | null = null;
private triggerOrdersPollTimer: ReturnType<typeof setInterval> | null = null; private triggerOrdersPollTimer: ReturnType<typeof setInterval> | null = null;
private klinesState = new Map<string, { productId: number; periodSec: number; klines: AsterKline[] }>(); private klinesState = new Map<string, { productId: number; periodSec: number; klines: Kline[] }>();
private subscriptionRequestId = 1; private subscriptionRequestId = 1;
private subscriptionAuthComplete = false; private subscriptionAuthComplete = false;
@@ -567,7 +567,7 @@ export class NadoGateway {
}; };
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized(params.symbol); await this.ensureInitialized(params.symbol);
const meta = this.getSymbolMetaOrThrow(params.symbol); const meta = this.getSymbolMetaOrThrow(params.symbol);
if (params.type === "STOP_MARKET") { if (params.type === "STOP_MARKET") {
@@ -1139,7 +1139,7 @@ export class NadoGateway {
return Number.isFinite(asNumber) && asNumber > 0 ? Math.floor(asNumber) : 60; return Number.isFinite(asNumber) && asNumber > 0 ? Math.floor(asNumber) : 60;
} }
private async fetchCandlesticks(productId: number, periodSec: number, limit: number): Promise<AsterKline[]> { private async fetchCandlesticks(productId: number, periodSec: number, limit: number): Promise<Kline[]> {
try { try {
const result = await this.indexer.getCandlesticks({ productId, period: periodSec, limit }); const result = await this.indexer.getCandlesticks({ productId, period: periodSec, limit });
const reversed = Array.from(result).reverse(); const reversed = Array.from(result).reverse();
@@ -1262,8 +1262,8 @@ export class NadoGateway {
} }
} }
private buildAsterOrdersSnapshot(): AsterOrder[] { private buildAsterOrdersSnapshot(): Order[] {
const orders: AsterOrder[] = []; const orders: Order[] = [];
for (const order of this.openOrdersByDigest.values()) { for (const order of this.openOrdersByDigest.values()) {
const meta = this.symbolMetaByProductId.get(order.productId); const meta = this.symbolMetaByProductId.get(order.productId);
if (!meta) continue; if (!meta) continue;
@@ -1322,7 +1322,7 @@ export class NadoGateway {
return orders; return orders;
} }
private buildDepthSnapshot(productId: number, symbol: string): AsterDepth | null { private buildDepthSnapshot(productId: number, symbol: string): Depth | null {
const bbo = this.bestBidOfferByProductId.get(productId); const bbo = this.bestBidOfferByProductId.get(productId);
if (!bbo) return null; if (!bbo) return null;
const bids: [string, string][] = [[fromX18(bbo.bidX18).toFixed(), fromX18(bbo.bidQtyX18).toFixed()]]; const bids: [string, string][] = [[fromX18(bbo.bidX18).toFixed(), fromX18(bbo.bidQtyX18).toFixed()]];
@@ -1336,7 +1336,7 @@ export class NadoGateway {
}; };
} }
private buildTickerSnapshot(productId: number, symbol: string): AsterTicker | null { private buildTickerSnapshot(productId: number, symbol: string): Ticker | null {
const bbo = this.bestBidOfferByProductId.get(productId); const bbo = this.bestBidOfferByProductId.get(productId);
if (!bbo) return null; if (!bbo) return null;
const trade = this.lastTradePriceByProductId.get(productId); const trade = this.lastTradePriceByProductId.get(productId);
@@ -1360,10 +1360,10 @@ export class NadoGateway {
}; };
} }
private mapSubaccountInfoToAsterSnapshot(data: NonNullable<NadoSubaccountInfoResponse["data"]>): AsterAccountSnapshot { private mapSubaccountInfoToAsterSnapshot(data: NonNullable<NadoSubaccountInfoResponse["data"]>): AccountSnapshot {
const now = nowMs(); const now = nowMs();
const assets: AsterAccountAsset[] = []; const assets: AccountAsset[] = [];
const positions: AsterAccountPosition[] = []; const positions: AccountPosition[] = [];
const spotBalanceByProductId = new Map<number, string>(); const spotBalanceByProductId = new Map<number, string>();
for (const entry of data.spot_balances ?? []) { for (const entry of data.spot_balances ?? []) {
@@ -1684,7 +1684,7 @@ export class NadoGateway {
try { try {
const text = typeof data === "string" ? data : data.toString("utf8"); const text = typeof data === "string" ? data : data.toString("utf8");
message = JSON.parse(text); message = JSON.parse(text);
} catch (_error) { } catch {
return; return;
} }
if (!message || typeof message !== "object") return; if (!message || typeof message !== "object") return;
@@ -1771,7 +1771,7 @@ export class NadoGateway {
if (state.periodSec !== event.granularity) continue; if (state.periodSec !== event.granularity) continue;
const openTime = event.timestamp * 1000; const openTime = event.timestamp * 1000;
const closeTime = openTime + state.periodSec * 1000 - 1; const closeTime = openTime + state.periodSec * 1000 - 1;
const next: AsterKline = { const next: Kline = {
openTime, openTime,
closeTime, closeTime,
open: fromX18(event.open_x18).toFixed(), open: fromX18(event.open_x18).toFixed(),
@@ -1795,7 +1795,7 @@ export class NadoGateway {
} }
} }
private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> { private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)"); if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
const side = params.side; const side = params.side;
const qty = params.quantity ?? 0; const qty = params.quantity ?? 0;
@@ -1905,7 +1905,7 @@ export class NadoGateway {
}; };
} }
private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> { private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)"); if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
const side = params.side; const side = params.side;
const qty = params.quantity ?? 0; const qty = params.quantity ?? 0;
@@ -2017,7 +2017,7 @@ export class NadoGateway {
}; };
} }
private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> { private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)"); if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)");
const side = params.side; const side = params.side;
const qty = params.quantity ?? 0; const qty = params.quantity ?? 0;
+19 -90
View File
@@ -1,93 +1,22 @@
import type { AsterOrder, CreateOrderParams } from "../types"; import { createOrderHandlers } from "../order-handlers";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { const handlers = createOrderHandlers({
if (params.quantity === undefined) { exchangeName: "Nado",
params.quantity = intent.quantity; defaultLimitTimeInForce: "GTC",
} defaultMarketTimeInForce: "IOC",
if (params.timeInForce === undefined && intent.timeInForce) { defaultCloseTimeInForce: "IOC",
params.timeInForce = intent.timeInForce; supportsTrailingStop: false,
} supportsTriggerType: false,
if (intent.reduceOnly !== undefined) { stopDefaultReduceOnly: true,
params.reduceOnly = toStringBoolean(intent.reduceOnly); stopDefaultClosePosition: true,
} closeDefaultClosePosition: true,
if (intent.closePosition !== undefined) { });
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export const {
const params: CreateOrderParams = applyCommonFields( createLimitOrder,
{ createMarketOrder,
symbol: intent.symbol, createStopOrder,
side: intent.side, createTrailingStopOrder,
type: "LIMIT", createClosePositionOrder,
quantity: intent.quantity, } = handlers;
price: intent.price,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
timeInForce: intent.timeInForce ?? "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
closePosition: toStringBoolean(intent.closePosition ?? true),
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
throw new Error("Nado exchange does not support trailing stop orders");
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
closePosition: toStringBoolean(intent.closePosition ?? true),
timeInForce: intent.timeInForce ?? "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
+150
View File
@@ -0,0 +1,150 @@
import type {
AccountListener,
ConnectionEventListener,
DepthListener,
ExchangeAdapter,
ExchangePrecision,
FundingRateListener,
KlineListener,
OrderListener,
TickerListener,
} from "../adapter";
import { createInitManager, createSafeInvoke } from "../adapter-utils";
import type { CreateOrderParams, Order } from "../types";
import { OndoperpsGateway, type OndoperpsGatewayOptions } from "./gateway";
export interface OndoperpsCredentials {
apiKeyId?: string;
apiSecret?: string;
symbol?: string;
baseUrl?: string;
wsUrl?: string;
builderCode?: string;
builderFeeRateBps?: number;
logger?: OndoperpsGatewayOptions["logger"];
}
export class OndoperpsExchangeAdapter implements ExchangeAdapter {
readonly id = "ondoperps";
private readonly gateway: OndoperpsGateway;
private readonly symbol: string;
private readonly safeInvoke = createSafeInvoke("OndoperpsExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
constructor(credentials: OndoperpsCredentials = {}) {
const apiKeyId = credentials.apiKeyId ?? process.env.ONDOPERPS_API_KEY_ID ?? process.env.ONDOPERP_API_KEY_ID ?? process.env.ONDO_KEY_ID;
const apiSecret = credentials.apiSecret ?? process.env.ONDOPERPS_API_SECRET ?? process.env.ONDOPERP_API_SECRET ?? process.env.ONDO_API_SECRET;
if (!apiKeyId || !apiSecret) {
throw new Error("Missing ONDOPERPS_API_KEY_ID or ONDOPERPS_API_SECRET environment variable");
}
this.symbol = credentials.symbol ?? process.env.ONDOPERPS_SYMBOL ?? process.env.ONDOPERP_SYMBOL ?? process.env.TRADE_SYMBOL ?? "BTC-USD.P";
const sandbox = parseBoolean(process.env.ONDOPERPS_SANDBOX ?? process.env.ONDOPERP_SANDBOX);
this.gateway = new OndoperpsGateway({
apiKeyId,
apiSecret,
symbol: this.symbol,
baseUrl: credentials.baseUrl ?? process.env.ONDOPERPS_BASE_URL ?? process.env.ONDOPERP_BASE_URL ?? (sandbox ? "https://api.ondoperps-sandbox.xyz" : undefined),
wsUrl: credentials.wsUrl ?? process.env.ONDOPERPS_WS_URL ?? process.env.ONDOPERP_WS_URL ?? (sandbox ? "wss://api.ondoperps-sandbox.xyz/ws" : undefined),
builderCode: credentials.builderCode ?? process.env.ONDOPERPS_BUILDER_CODE ?? process.env.ONDOPERP_BUILDER_CODE,
builderFeeRateBps: credentials.builderFeeRateBps ?? parseNumber(
process.env.ONDOPERPS_BUILDER_FEE_RATE_BPS ?? process.env.ONDOPERP_BUILDER_FEE_RATE_BPS,
),
logger: credentials.logger,
});
this.init = createInitManager("OndoperpsExchangeAdapter", () => this.gateway.ensureInitialized());
}
supportsTrailingStops(): boolean {
return false;
}
watchAccount(cb: AccountListener): void {
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
}
watchFundingRate(symbol: string, cb: FundingRateListener): void {
void this.init.ensureInitialized("watchFundingRate");
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
}
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
await this.init.ensureInitialized("getPrecision");
return this.gateway.getPrecision(this.symbol);
}
onConnectionEvent(listener: ConnectionEventListener): void {
this.gateway.onConnectionEvent(listener);
}
offConnectionEvent(listener: ConnectionEventListener): void {
this.gateway.offConnectionEvent(listener);
}
async queryOpenOrders(): Promise<Order[]> {
await this.init.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders();
}
async queryAccountSnapshot() {
await this.init.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot();
}
async forceCancelAllOrders(): Promise<boolean> {
await this.init.ensureInitialized("forceCancelAllOrders");
return this.gateway.forceCancelAllOrders();
}
}
function parseBoolean(value: string | undefined): boolean {
if (!value) return false;
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
}
function parseNumber(value: string | undefined): number | undefined {
if (!value) return undefined;
const number = Number(value);
return Number.isFinite(number) ? number : undefined;
}
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
import { createOrderHandlers } from "../order-handlers";
const handlers = createOrderHandlers({
exchangeName: "Ondo Perps",
defaultLimitTimeInForce: "GTX",
defaultMarketTimeInForce: "IOC",
defaultCloseTimeInForce: "IOC",
defaultStopTimeInForce: "GTC",
defaultStopTriggerType: "STOP_LOSS",
supportsTrailingStop: false,
supportsTriggerType: true,
stopDefaultReduceOnly: true,
stopDefaultClosePosition: true,
closeDefaultClosePosition: true,
});
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+173
View File
@@ -0,0 +1,173 @@
export interface OndoperpsApiResponse<T> {
success: boolean;
result?: T;
error?: string;
error_code?: string;
}
export interface OndoperpsOrder {
orderId: string;
clientOrderId?: string;
parentOrderId?: string;
side: "buy" | "sell";
price?: string;
size: string;
market: string;
filledSize?: string;
lastFillSize?: string;
filledCost?: string;
realizedPnl?: string;
fee?: string;
feeRebate?: string;
status: "open" | "fullyfilled" | "canceled" | "pending" | "untriggered" | string;
createdAt: string;
filledAt?: string;
canceledAt?: string;
cancelReason?: string;
type: "limit" | "market" | "stopMarket" | "takeProfitMarket" | string;
timeInForce?: "GTC" | "IOC" | "FOK";
reduceOnly?: boolean;
closePosition?: boolean;
stopOrderType?: "stopLoss" | "takeProfit";
triggerPrice?: string;
}
export interface OndoperpsPosition {
market: string;
direction: "long" | "short" | "neutral";
netQuantity: string;
averageEntryPrice: string;
usedMargin: string;
unrealizedPnl: string;
markPrice: string;
liquidationPrice: string;
bankruptcyPrice: string;
maintenanceMargin: string;
notionalValue: string;
leverage: string;
netFundingSinceNeutral: string;
returnOnEquity: string;
stopLossTriggerPrice?: string;
takeProfitTriggerPrice?: string;
}
export interface OndoperpsBalance {
walletBalance: string;
realizedPnl: string;
unrealizedPnl: string;
marginBalance: string;
usedMargin: string;
availableMargin: string;
withdrawableMargin: string;
maintenanceMarginRequirement: string;
totalMaintenanceMargin: string;
marginRatio: string;
leverage: string;
underLiquidation: boolean;
totalFundingPayments: string;
totalTradingFees: string;
totalPnL: string;
netInvested?: string;
}
export interface OndoperpsStopOrders {
market: string;
positionDirection: "long" | "short" | "neutral";
stopLoss?: string | null;
takeProfit?: string | null;
}
export type OndoperpsBookLevel = [string, string];
export interface OndoperpsBookSnapshot {
market: string;
time: string;
bids: OndoperpsBookLevel[];
asks: OndoperpsBookLevel[];
depthLevels?: string;
}
export interface OndoperpsCandle {
startTime: string;
open: string;
high: string;
low: string;
close: string;
volume: string;
}
export interface OndoperpsWsKline {
m: string;
t: number;
s: number;
e: number;
o: number;
h: number;
l: number;
c: number;
v: number;
x?: boolean;
}
export interface OndoperpsContract {
market: string;
displayName?: string;
productType: string;
contractType: string;
baseCurrency: string;
quoteCurrency: string;
disabled: boolean;
lastPrice?: string;
baseVolume?: string;
quoteVolume?: string;
usdVolume?: string;
bid?: string;
ask?: string;
high?: string;
low?: string;
openInterest?: string;
openInterestUsd?: string;
indexPrice?: string;
fundingRate?: string;
nextFundingRate?: string;
nextFundingRateTimestamp?: string;
makerFee?: string;
takerFee?: string;
priceChangePercent?: string;
isClosed?: boolean;
}
export interface OndoperpsMarkPrice {
market: string;
price: string;
markPrice: string;
oraclePrice?: string;
lastExternalPrice?: string;
lastUpdatedTime?: string;
}
export interface OndoperpsFundingRate {
market: string;
rate: string;
intervalEnds?: string;
}
export interface OndoperpsTradingPair {
market: string;
baseIncrement: string;
quoteIncrement: string;
}
export interface OndoperpsMarketsResult {
perps?: {
tradingPairs?: OndoperpsTradingPair[];
};
}
export interface OndoperpsWsMessage {
type: "pong" | "loggedIn" | "subscribed" | "unsubscribed" | "update" | "error" | string;
channel?: string;
code?: number;
msg?: string;
data?: unknown;
}
+147
View File
@@ -0,0 +1,147 @@
import type { Order, CreateOrderParams, TimeInForce } from "./types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "./order-schema";
import { toStringBoolean } from "./order-schema";
export interface OrderHandlerConfig {
exchangeName: string;
defaultLimitTimeInForce: TimeInForce | "GTX";
defaultMarketTimeInForce?: TimeInForce;
defaultCloseTimeInForce?: TimeInForce;
defaultStopTimeInForce?: TimeInForce;
defaultStopTriggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
supportsTrailingStop: boolean;
supportsTriggerType: boolean;
stopDefaultReduceOnly?: boolean;
stopDefaultClosePosition?: boolean;
closeDefaultClosePosition?: boolean;
stopPriceAsPrice?: boolean;
}
export interface OrderHandlers {
createLimitOrder(intent: LimitOrderIntent): Promise<Order>;
createMarketOrder(intent: MarketOrderIntent): Promise<Order>;
createStopOrder(intent: StopOrderIntent): Promise<Order>;
createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order>;
createClosePositionOrder(intent: ClosePositionIntent): Promise<Order>;
}
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
if (params.quantity === undefined) {
params.quantity = intent.quantity;
}
if (params.timeInForce === undefined && intent.timeInForce) {
params.timeInForce = intent.timeInForce;
}
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
if (intent.clientOrderId !== undefined) {
params.clientOrderId = intent.clientOrderId;
}
return params;
}
export function createOrderHandlers(config: OrderHandlerConfig): OrderHandlers {
return {
async createLimitOrder(intent: LimitOrderIntent): Promise<Order> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "LIMIT",
quantity: intent.quantity,
price: intent.price,
timeInForce: intent.timeInForce ?? config.defaultLimitTimeInForce,
slPrice: intent.slPrice,
tpPrice: intent.tpPrice,
},
intent,
);
return intent.adapter.createOrder(params);
},
async createMarketOrder(intent: MarketOrderIntent): Promise<Order> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
timeInForce: intent.timeInForce ?? config.defaultMarketTimeInForce,
},
intent,
);
return intent.adapter.createOrder(params);
},
async createStopOrder(intent: StopOrderIntent): Promise<Order> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? (config.defaultStopTimeInForce ?? "GTC"),
triggerType: config.supportsTriggerType ? (intent.triggerType ?? config.defaultStopTriggerType) : undefined,
price: config.stopPriceAsPrice ? intent.stopPrice : undefined,
},
intent,
);
if (config.stopDefaultReduceOnly && intent.reduceOnly === undefined) {
params.reduceOnly = "true";
}
if (config.stopDefaultClosePosition && intent.closePosition === undefined) {
params.closePosition = "true";
}
return intent.adapter.createOrder(params);
},
async createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order> {
if (!config.supportsTrailingStop) {
throw new Error(`${config.exchangeName} does not support trailing stop orders`);
}
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "TRAILING_STOP_MARKET",
quantity: intent.quantity,
activationPrice: intent.activationPrice,
callbackRate: intent.callbackRate,
timeInForce: intent.timeInForce ?? "GTC",
},
intent,
);
return intent.adapter.createOrder(params);
},
async createClosePositionOrder(intent: ClosePositionIntent): Promise<Order> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
timeInForce: intent.timeInForce ?? config.defaultCloseTimeInForce,
},
intent,
);
if (config.closeDefaultClosePosition && intent.closePosition === undefined) {
params.closePosition = "true";
}
return intent.adapter.createOrder(params);
},
};
}
+21 -12
View File
@@ -1,5 +1,5 @@
import type { ExchangeAdapter } from "./adapter"; import type { ExchangeAdapter } from "./adapter";
import type { AsterOrder } from "./types"; import type { Order } from "./types";
import { SUPPORTED_EXCHANGE_IDS, type SupportedExchangeId } from "./create-adapter"; import { SUPPORTED_EXCHANGE_IDS, type SupportedExchangeId } from "./create-adapter";
import type { import type {
BaseOrderIntent, BaseOrderIntent,
@@ -17,15 +17,16 @@ 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;
interface ExchangeOrderHandlers { interface ExchangeOrderHandlers {
limit(intent: LimitOrderIntent): Promise<AsterOrder>; limit(intent: LimitOrderIntent): Promise<Order>;
market(intent: MarketOrderIntent): Promise<AsterOrder>; market(intent: MarketOrderIntent): Promise<Order>;
stop(intent: StopOrderIntent): Promise<AsterOrder>; stop(intent: StopOrderIntent): Promise<Order>;
trailingStop?: (intent: TrailingStopOrderIntent) => Promise<AsterOrder>; trailingStop?: (intent: TrailingStopOrderIntent) => Promise<Order>;
close(intent: ClosePositionIntent): Promise<AsterOrder>; close(intent: ClosePositionIntent): Promise<Order>;
} }
const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = { const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
@@ -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 {
@@ -117,19 +126,19 @@ function getHandlers(intent: BaseOrderIntent): ExchangeOrderHandlers {
return handlers; return handlers;
} }
export function routeLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export function routeLimitOrder(intent: LimitOrderIntent): Promise<Order> {
return getHandlers(intent).limit(intent); return getHandlers(intent).limit(intent);
} }
export function routeMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> { export function routeMarketOrder(intent: MarketOrderIntent): Promise<Order> {
return getHandlers(intent).market(intent); return getHandlers(intent).market(intent);
} }
export function routeStopOrder(intent: StopOrderIntent): Promise<AsterOrder> { export function routeStopOrder(intent: StopOrderIntent): Promise<Order> {
return getHandlers(intent).stop(intent); return getHandlers(intent).stop(intent);
} }
export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> { export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order> {
const handlers = getHandlers(intent); const handlers = getHandlers(intent);
if (!handlers.trailingStop) { if (!handlers.trailingStop) {
throw new Error("Trailing stop orders are not supported by the current exchange"); throw new Error("Trailing stop orders are not supported by the current exchange");
@@ -137,6 +146,6 @@ export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise
return handlers.trailingStop(intent); return handlers.trailingStop(intent);
} }
export function routeCloseOrder(intent: ClosePositionIntent): Promise<AsterOrder> { export function routeCloseOrder(intent: ClosePositionIntent): Promise<Order> {
return getHandlers(intent).close(intent); return getHandlers(intent).close(intent);
} }
+3 -3
View File
@@ -9,13 +9,13 @@ export interface BaseOrderIntent {
reduceOnly?: boolean; reduceOnly?: boolean;
closePosition?: boolean; closePosition?: boolean;
timeInForce?: TimeInForce | "GTX"; timeInForce?: TimeInForce | "GTX";
clientOrderId?: string;
} }
export interface LimitOrderIntent extends BaseOrderIntent { export interface LimitOrderIntent extends BaseOrderIntent {
price: number; price: number;
// StandX TPSL 参数 slPrice?: number;
slPrice?: number; // 止损价格 tpPrice?: number;
tpPrice?: number; // 止盈价格
} }
export interface MarketOrderIntent extends BaseOrderIntent { export interface MarketOrderIntent extends BaseOrderIntent {
+17 -81
View File
@@ -1,4 +1,3 @@
import { setTimeout, clearTimeout } from "timers";
import type { import type {
AccountListener, AccountListener,
DepthListener, DepthListener,
@@ -7,9 +6,10 @@ import type {
OrderListener, OrderListener,
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types"; import type { Order, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors"; import { extractMessage } from "../../utils/errors";
import { ParadexGateway, type ParadexGatewayOptions } from "./gateway"; import { ParadexGateway, type ParadexGatewayOptions } from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
export interface ParadexCredentials { export interface ParadexCredentials {
privateKey?: string; privateKey?: string;
@@ -26,11 +26,8 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
private readonly gateway: ParadexGateway; private readonly gateway: ParadexGateway;
private readonly symbol: string; private readonly symbol: string;
private initPromise: Promise<void> | null = null; private readonly safeInvoke = createSafeInvoke("ParadexExchangeAdapter");
private readonly initContexts = new Set<string>(); private readonly init: ReturnType<typeof createInitManager>;
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: ParadexCredentials = {}) { constructor(credentials: ParadexCredentials = {}) {
const privateKey = credentials.privateKey ?? process.env.PARADEX_PRIVATE_KEY; const privateKey = credentials.privateKey ?? process.env.PARADEX_PRIVATE_KEY;
@@ -54,6 +51,9 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
}); });
this.symbol = symbol; this.symbol = symbol;
this.init = createInitManager("ParadexExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
} }
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
@@ -61,114 +61,50 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
} }
watchAccount(cb: AccountListener): void { watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount"); void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb)); this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
} }
watchOrders(cb: OrderListener): void { watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders"); void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb)); this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
} }
watchDepth(symbol: string, cb: DepthListener): void { watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized(`watchDepth:${symbol}`); void this.init.ensureInitialized(`watchDepth:${symbol}`);
this.gateway.onDepth(this.safeInvoke("watchDepth", cb)); this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
} }
watchTicker(symbol: string, cb: TickerListener): void { watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized(`watchTicker:${symbol}`); void this.init.ensureInitialized(`watchTicker:${symbol}`);
this.gateway.onTicker(this.safeInvoke("watchTicker", cb)); this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
} }
watchKlines(symbol: string, interval: string, cb: KlineListener): void { watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`); void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb)); this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized("createOrder"); await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params); return this.gateway.createOrder(params);
} }
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> { async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder"); await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params); await this.gateway.cancelOrder(params);
} }
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> { async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders"); await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params); await this.gateway.cancelOrders(params);
} }
async cancelAllOrders(params: { symbol: string }): Promise<void> { async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders"); await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params); await this.gateway.cancelAllOrders(params);
} }
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[ParadexExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[ParadexExchangeAdapter] ${context} failed`, error);
}
private logError(context: string, error: unknown): void { private logError(context: string, error: unknown): void {
const detail = extractMessage(error); const detail = extractMessage(error);
if (context === "initialize" && typeof error === "string" && /initialized/i.test(error)) { if (context === "initialize" && typeof error === "string" && /initialized/i.test(error)) {
+23 -24
View File
@@ -7,12 +7,12 @@ import ccxt, {
} from "ccxt"; } from "ccxt";
import { createRequire } from "module"; import { createRequire } from "module";
import type { import type {
AsterAccountAsset, AccountAsset,
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
OrderType, OrderType,
} from "../types"; } from "../types";
@@ -32,7 +32,7 @@ function loadCcxtPro(): any | null {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = require("ccxt.pro"); const mod = require("ccxt.pro");
return mod?.default ?? mod; return mod?.default ?? mod;
} catch (_error) { } catch {
return null; return null;
} }
} }
@@ -91,8 +91,8 @@ export class ParadexGateway {
private depthListeners = new Set<DepthListener>(); private depthListeners = new Set<DepthListener>();
private tickerListeners = new Set<TickerListener>(); private tickerListeners = new Set<TickerListener>();
private klineListeners = new Map<string, Set<KlineListener>>(); private klineListeners = new Map<string, Set<KlineListener>>();
private readonly localOrders = new Map<string, AsterOrder>(); private readonly localOrders = new Map<string, Order>();
private lastBalanceSnapshot: AsterAccountSnapshot | null = null; private lastBalanceSnapshot: AccountSnapshot | null = null;
private accountPollTimer: NodeJS.Timeout | null = null; private accountPollTimer: NodeJS.Timeout | null = null;
private orderPollTimer: NodeJS.Timeout | null = null; private orderPollTimer: NodeJS.Timeout | null = null;
@@ -546,7 +546,7 @@ export class ParadexGateway {
this.klinePollTimers.set(interval, setInterval(() => void poll(), this.pollIntervals.klines)); this.klinePollTimers.set(interval, setInterval(() => void poll(), this.pollIntervals.klines));
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized(params.symbol); await this.ensureInitialized(params.symbol);
const symbol = this.marketSymbol; const symbol = this.marketSymbol;
const type = this.mapOrderTypeToCcxt(params.type); const type = this.mapOrderTypeToCcxt(params.type);
@@ -571,7 +571,6 @@ export class ParadexGateway {
const market = typeof (this.exchange as any).market === "function" const market = typeof (this.exchange as any).market === "function"
? (this.exchange as any).market(symbol) ? (this.exchange as any).market(symbol)
: (this.exchange.markets ?? {})[symbol]; : (this.exchange.markets ?? {})[symbol];
const precisionDigits = Number((market?.precision?.amount ?? market?.amountPrecision));
const limitMin = Number(market?.limits?.amount?.min); const limitMin = Number(market?.limits?.amount?.min);
// Only trust explicit exchange min limit; do NOT infer 1 from precision=0 // Only trust explicit exchange min limit; do NOT infer 1 from precision=0
const minAmount = Number.isFinite(limitMin) && limitMin > 0 ? limitMin : undefined; const minAmount = Number.isFinite(limitMin) && limitMin > 0 ? limitMin : undefined;
@@ -593,7 +592,7 @@ export class ParadexGateway {
if (typeof (this.exchange as any).amountToPrecision === "function" && Number.isFinite(Number(amount))) { if (typeof (this.exchange as any).amountToPrecision === "function" && Number.isFinite(Number(amount))) {
amount = Number((this.exchange as any).amountToPrecision(symbol, amount)); amount = Number((this.exchange as any).amountToPrecision(symbol, amount));
} }
} catch (_normalizeError) { } catch {
// Swallow precision normalization errors and let exchange validation surface if any // Swallow precision normalization errors and let exchange validation surface if any
} }
@@ -679,7 +678,7 @@ export class ParadexGateway {
} }
} }
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot { private mapBalanceToAccountSnapshot(balance: Balances): AccountSnapshot {
const now = Date.now(); const now = Date.now();
const rawPositions = (() => { const rawPositions = (() => {
@@ -712,7 +711,7 @@ export class ParadexGateway {
...Object.keys(total), ...Object.keys(total),
]); ]);
const assets: AsterAccountAsset[] = Array.from(assetKeys).map((asset) => ({ const assets: AccountAsset[] = Array.from(assetKeys).map((asset) => ({
asset, asset,
walletBalance: String(total[asset] ?? 0), walletBalance: String(total[asset] ?? 0),
availableBalance: String(free[asset] ?? 0), availableBalance: String(free[asset] ?? 0),
@@ -738,7 +737,7 @@ export class ParadexGateway {
}; };
} }
private mapBalanceToAccountSnapshotFromPositions(rawPositions: any[]): AsterAccountSnapshot { private mapBalanceToAccountSnapshotFromPositions(rawPositions: any[]): AccountSnapshot {
const now = Date.now(); const now = Date.now();
const positions = this.normalizePositions(rawPositions, now); const positions = this.normalizePositions(rawPositions, now);
this.logger("positions", JSON.stringify({ raw: rawPositions, mapped: positions })); this.logger("positions", JSON.stringify({ raw: rawPositions, mapped: positions }));
@@ -762,7 +761,7 @@ export class ParadexGateway {
}; };
} }
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] { private normalizePositions(rawPositions: any[], now: number): AccountSnapshot["positions"] {
return rawPositions return rawPositions
.filter((pos) => pos) .filter((pos) => pos)
.map((pos: any) => { .map((pos: any) => {
@@ -808,7 +807,7 @@ export class ParadexGateway {
}); });
} }
private mapOrderToAsterOrder(order: CcxtOrder): AsterOrder { private mapOrderToAsterOrder(order: CcxtOrder): Order {
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL"; const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
const mappedType = this.mapCcxtOrderTypeToAster(order.type); const mappedType = this.mapCcxtOrderTypeToAster(order.type);
return { return {
@@ -831,7 +830,7 @@ export class ParadexGateway {
}; };
} }
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth { private mapOrderBookToDepth(orderbook: CcxtOrderBook): Depth {
return { return {
lastUpdateId: orderbook.nonce || Date.now(), lastUpdateId: orderbook.nonce || Date.now(),
bids: (orderbook.bids || []) bids: (orderbook.bids || [])
@@ -844,7 +843,7 @@ export class ParadexGateway {
}; };
} }
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker { private mapTickerToAsterTicker(ticker: CcxtTicker): Ticker {
return { return {
symbol: ticker.symbol, symbol: ticker.symbol,
lastPrice: ticker.last?.toString() || "0", lastPrice: ticker.last?.toString() || "0",
@@ -857,7 +856,7 @@ export class ParadexGateway {
}; };
} }
private mapOHLCVToKline(candle: CcxtOhlcv, interval: string): AsterKline { private mapOHLCVToKline(candle: CcxtOhlcv, interval: string): Kline {
const [timestampRaw, openRaw, highRaw, lowRaw, closeRaw, volumeRaw] = candle; const [timestampRaw, openRaw, highRaw, lowRaw, closeRaw, volumeRaw] = candle;
const timestamp = typeof timestampRaw === "number" && Number.isFinite(timestampRaw) const timestamp = typeof timestampRaw === "number" && Number.isFinite(timestampRaw)
? timestampRaw ? timestampRaw
@@ -927,7 +926,7 @@ export class ParadexGateway {
return base[interval] ?? 60 * 1000; return base[interval] ?? 60 * 1000;
} }
private upsertLocalOrder(order: AsterOrder): void { private upsertLocalOrder(order: Order): void {
const key = String(order.orderId); const key = String(order.orderId);
if (this.isOrderClosed(order)) { if (this.isOrderClosed(order)) {
this.localOrders.delete(key); this.localOrders.delete(key);
@@ -944,8 +943,8 @@ export class ParadexGateway {
} }
} }
private updateOrdersFromRemote(open: CcxtOrder[], closed: CcxtOrder[]): void { private updateOrdersFromRemote(open: CcxtOrder[], _closed: CcxtOrder[]): void {
const nextOpen = new Map<string, AsterOrder>(); const nextOpen = new Map<string, Order>();
for (const order of open) { for (const order of open) {
const mapped = this.mapOrderToAsterOrder(order); const mapped = this.mapOrderToAsterOrder(order);
@@ -974,7 +973,7 @@ export class ParadexGateway {
} }
} }
private isOrderClosed(order: AsterOrder): boolean { private isOrderClosed(order: Order): boolean {
const status = (order.status ?? "").toUpperCase(); const status = (order.status ?? "").toUpperCase();
if ( if (
status.includes("CLOSE") || status.includes("CLOSE") ||
+18 -91
View File
@@ -1,93 +1,20 @@
import type { AsterOrder, CreateOrderParams } from "../types"; import { createOrderHandlers } from "../order-handlers";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { const handlers = createOrderHandlers({
if (params.quantity === undefined) { exchangeName: "Paradex",
params.quantity = intent.quantity; defaultLimitTimeInForce: "GTC",
} supportsTrailingStop: false,
if (params.timeInForce === undefined && intent.timeInForce) { supportsTriggerType: false,
params.timeInForce = intent.timeInForce; stopDefaultReduceOnly: true,
} stopDefaultClosePosition: true,
if (intent.reduceOnly !== undefined) { stopPriceAsPrice: true,
params.reduceOnly = toStringBoolean(intent.reduceOnly); closeDefaultClosePosition: true,
} });
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export const {
const params: CreateOrderParams = applyCommonFields( createLimitOrder,
{ createMarketOrder,
symbol: intent.symbol, createStopOrder,
side: intent.side, createTrailingStopOrder,
type: "LIMIT", createClosePositionOrder,
quantity: intent.quantity, } = handlers;
price: intent.price,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
timeInForce: intent.timeInForce,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
price: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
closePosition: toStringBoolean(intent.closePosition ?? true),
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
throw new Error("Paradex exchange does not support trailing stop orders");
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
closePosition: toStringBoolean(intent.closePosition ?? true),
timeInForce: intent.timeInForce,
},
intent
);
return intent.adapter.createOrder(params);
}
+26 -1
View File
@@ -1,12 +1,13 @@
import type { ExchangeAdapter } from "./adapter"; import type { ExchangeAdapter } from "./adapter";
import { createExchangeAdapter, resolveExchangeId, type SupportedExchangeId } from "./create-adapter"; import { createExchangeAdapter, resolveExchangeId, type SupportedExchangeId } from "./create-adapter";
import type { AsterCredentials } from "./aster-adapter"; import type { AsterCredentials } from "./aster/adapter";
import type { LighterCredentials } from "./lighter/adapter"; import type { LighterCredentials } from "./lighter/adapter";
import type { BackpackCredentials } from "./backpack/adapter"; import type { BackpackCredentials } from "./backpack/adapter";
import type { ParadexCredentials } from "./paradex/adapter"; 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());
} }
+23 -86
View File
@@ -1,4 +1,3 @@
import { setTimeout, clearTimeout } from "timers";
import type { import type {
AccountListener, AccountListener,
DepthListener, DepthListener,
@@ -10,9 +9,9 @@ import type {
RestHealthListener, RestHealthListener,
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types"; import type { Order, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import { StandxGateway, type StandxGatewayOptions, type ConnectionEventListener, type ConnectionEventType } from "./gateway"; import { StandxGateway, type StandxGatewayOptions, type ConnectionEventListener, type ConnectionEventType } from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
export type { ConnectionEventListener, ConnectionEventType }; export type { ConnectionEventListener, ConnectionEventType };
@@ -31,11 +30,8 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
private readonly gateway: StandxGateway; private readonly gateway: StandxGateway;
private readonly symbol: string; private readonly symbol: string;
private initPromise: Promise<void> | null = null; private readonly safeInvoke = createSafeInvoke("StandxExchangeAdapter");
private readonly initContexts = new Set<string>(); private readonly init: ReturnType<typeof createInitManager>;
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: StandxCredentials = {}) { constructor(credentials: StandxCredentials = {}) {
const token = credentials.token ?? process.env.STANDX_TOKEN; const token = credentials.token ?? process.env.STANDX_TOKEN;
@@ -52,6 +48,9 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
signingKey: credentials.signingKey, signingKey: credentials.signingKey,
logger: credentials.logger, logger: credentials.logger,
}); });
this.init = createInitManager("StandxExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
} }
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
@@ -59,52 +58,52 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
} }
watchAccount(cb: AccountListener): void { watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount"); void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb)); this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
} }
watchOrders(cb: OrderListener): void { watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders"); void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb)); this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
} }
watchDepth(symbol: string, cb: DepthListener): void { watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth"); void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb)); this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
} }
watchTicker(symbol: string, cb: TickerListener): void { watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker"); void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb)); this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
} }
watchKlines(symbol: string, interval: string, cb: KlineListener): void { watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized("watchKlines"); void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb)); this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
} }
watchFundingRate(symbol: string, cb: FundingRateListener): void { watchFundingRate(symbol: string, cb: FundingRateListener): void {
void this.ensureInitialized("watchFundingRate"); void this.init.ensureInitialized("watchFundingRate");
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb)); this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized("createOrder"); await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params); return this.gateway.createOrder(params);
} }
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> { async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder"); await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params); await this.gateway.cancelOrder(params);
} }
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> { async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders"); await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params); await this.gateway.cancelOrders(params);
} }
async cancelAllOrders(params: { symbol: string }): Promise<void> { async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders"); await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params); await this.gateway.cancelAllOrders(params);
} }
@@ -151,18 +150,18 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
* HTTP API * HTTP API
* *
*/ */
async queryOpenOrders(): Promise<AsterOrder[]> { async queryOpenOrders(): Promise<Order[]> {
await this.ensureInitialized("queryOpenOrders"); await this.init.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders(this.symbol); return this.gateway.queryOpenOrders(this.symbol);
} }
async queryAccountSnapshot() { async queryAccountSnapshot() {
await this.ensureInitialized("queryAccountSnapshot"); await this.init.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot(); return this.gateway.queryAccountSnapshot();
} }
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> { async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
await this.ensureInitialized("changeMarginMode"); await this.init.ensureInitialized("changeMarginMode");
await this.gateway.changeMarginMode(params.symbol, params.marginMode); await this.gateway.changeMarginMode(params.symbol, params.marginMode);
} }
@@ -171,69 +170,7 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
* *
*/ */
async forceCancelAllOrders(): Promise<boolean> { async forceCancelAllOrders(): Promise<boolean> {
await this.ensureInitialized("forceCancelAllOrders"); await this.init.ensureInitialized("forceCancelAllOrders");
return this.gateway.forceCancelAllOrders(this.symbol); return this.gateway.forceCancelAllOrders(this.symbol);
} }
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[StandxExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[StandxExchangeAdapter] ${context} failed`, error);
}
} }
+33 -33
View File
@@ -14,13 +14,13 @@ import type {
TickerListener, TickerListener,
} from "../adapter"; } from "../adapter";
import type { import type {
AsterAccountAsset, AccountAsset,
AsterAccountPosition, AccountPosition,
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
OrderSide, OrderSide,
OrderType, OrderType,
@@ -77,7 +77,7 @@ type FundingState = {
}; };
type VirtualStop = { type VirtualStop = {
order: AsterOrder; order: Order;
stopPrice: number; stopPrice: number;
side: OrderSide; side: OrderSide;
symbol: string; symbol: string;
@@ -279,7 +279,7 @@ function resolutionFromInterval(interval: string): { resolution: string; seconds
return { resolution: "1", seconds: 60 }; return { resolution: "1", seconds: 60 };
} }
function mergeOrderSnapshot(map: Map<string, AsterOrder>, order: AsterOrder): void { function mergeOrderSnapshot(map: Map<string, Order>, order: Order): void {
const key = String(order.orderId); const key = String(order.orderId);
const existing = map.get(key); const existing = map.get(key);
if (!existing) { if (!existing) {
@@ -418,12 +418,12 @@ export class StandxGateway {
private readonly fundingListeners = new Map<string, Set<FundingRateListener>>(); private readonly fundingListeners = new Map<string, Set<FundingRateListener>>();
private readonly connectionListeners = new Set<ConnectionEventListener>(); private readonly connectionListeners = new Set<ConnectionEventListener>();
private readonly openOrders = new Map<string, AsterOrder>(); private readonly openOrders = new Map<string, Order>();
private readonly positions = new Map<string, AsterAccountPosition>(); private readonly positions = new Map<string, AccountPosition>();
private readonly balances = new Map<string, AsterAccountAsset>(); private readonly balances = new Map<string, AccountAsset>();
private readonly virtualStops = new Map<string, VirtualStop>(); private readonly virtualStops = new Map<string, VirtualStop>();
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private readonly restHealthListeners = new Set<RestHealthListener>(); private readonly restHealthListeners = new Set<RestHealthListener>();
private restConsecutiveErrors = 0; private restConsecutiveErrors = 0;
private restUnhealthy = false; private restUnhealthy = false;
@@ -582,14 +582,14 @@ export class StandxGateway {
* HTTP API * HTTP API
* *
*/ */
async queryOpenOrders(symbol: string): Promise<AsterOrder[]> { async queryOpenOrders(symbol: string): Promise<Order[]> {
const normalized = normalizeSymbol(symbol); const normalized = normalizeSymbol(symbol);
const ordersPayload = await this.requestJson<unknown>("/api/query_open_orders", { const ordersPayload = await this.requestJson<unknown>("/api/query_open_orders", {
method: "GET", method: "GET",
params: { symbol: normalized }, params: { symbol: normalized },
}); });
const orders = extractOrders(ordersPayload); const orders = extractOrders(ordersPayload);
const result: AsterOrder[] = []; const result: Order[] = [];
for (const raw of orders) { for (const raw of orders) {
const order = this.mapOrder(raw); const order = this.mapOrder(raw);
result.push(order); result.push(order);
@@ -618,7 +618,7 @@ export class StandxGateway {
} }
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
const normalizedSymbol = normalizeSymbol(params.symbol); const normalizedSymbol = normalizeSymbol(params.symbol);
if (params.type === "STOP_MARKET") { if (params.type === "STOP_MARKET") {
return this.createVirtualStopOrder(normalizedSymbol, params); return this.createVirtualStopOrder(normalizedSymbol, params);
@@ -785,7 +785,7 @@ export class StandxGateway {
}; };
} }
private async createVirtualStopOrder(symbol: string, params: CreateOrderParams): Promise<AsterOrder> { private async createVirtualStopOrder(symbol: string, params: CreateOrderParams): Promise<Order> {
const stopPrice = Number(params.stopPrice); const stopPrice = Number(params.stopPrice);
if (!Number.isFinite(stopPrice)) { if (!Number.isFinite(stopPrice)) {
throw new Error("STOP_MARKET requires stopPrice for StandX"); throw new Error("STOP_MARKET requires stopPrice for StandX");
@@ -796,7 +796,7 @@ export class StandxGateway {
} }
const now = Date.now(); const now = Date.now();
const clientOrderId = crypto.randomUUID(); const clientOrderId = crypto.randomUUID();
const order: AsterOrder = { const order: Order = {
orderId: clientOrderId, orderId: clientOrderId,
clientOrderId, clientOrderId,
symbol, symbol,
@@ -825,7 +825,7 @@ export class StandxGateway {
return order; return order;
} }
private async submitOrder(symbol: string, params: CreateOrderParams): Promise<AsterOrder> { private async submitOrder(symbol: string, params: CreateOrderParams): Promise<Order> {
const orderType = params.type === "MARKET" ? "market" : "limit"; const orderType = params.type === "MARKET" ? "market" : "limit";
const clientOrderId = crypto.randomUUID(); const clientOrderId = crypto.randomUUID();
const qty = toDecimalString(params.quantity); const qty = toDecimalString(params.quantity);
@@ -870,7 +870,7 @@ export class StandxGateway {
throw new Error(response.message ?? "StandX order rejected"); throw new Error(response.message ?? "StandX order rejected");
} }
const now = Date.now(); const now = Date.now();
const order: AsterOrder = { const order: Order = {
orderId: clientOrderId, orderId: clientOrderId,
clientOrderId, clientOrderId,
symbol, symbol,
@@ -1038,7 +1038,7 @@ export class StandxGateway {
} }
this.logDebug("ws depth stats", detail); this.logDebug("ws depth stats", detail);
} }
const depth: AsterDepth = { const depth: Depth = {
lastUpdateId: Number(message.seq ?? Date.now()), lastUpdateId: Number(message.seq ?? Date.now()),
bids: finalBids, bids: finalBids,
asks: finalAsks, asks: finalAsks,
@@ -1355,7 +1355,7 @@ export class StandxGateway {
console.log(`[StandxGateway] ws raw`, output); console.log(`[StandxGateway] ws raw`, output);
} }
private emitDepth(symbol: string, depth: AsterDepth): void { private emitDepth(symbol: string, depth: Depth): void {
const listeners = this.depthListeners.get(normalizeSymbol(symbol)); const listeners = this.depthListeners.get(normalizeSymbol(symbol));
if (!listeners) return; if (!listeners) return;
for (const listener of listeners) { for (const listener of listeners) {
@@ -1367,7 +1367,7 @@ export class StandxGateway {
} }
} }
private emitTicker(symbol: string, ticker: AsterTicker): void { private emitTicker(symbol: string, ticker: Ticker): void {
const listeners = this.tickerListeners.get(normalizeSymbol(symbol)); const listeners = this.tickerListeners.get(normalizeSymbol(symbol));
if (!listeners) return; if (!listeners) return;
const price = Number(ticker.lastPrice); const price = Number(ticker.lastPrice);
@@ -1406,7 +1406,7 @@ export class StandxGateway {
(sum, position) => sum + Number(position.unrealizedProfit ?? 0), (sum, position) => sum + Number(position.unrealizedProfit ?? 0),
0 0
); );
const snapshot: AsterAccountSnapshot = { const snapshot: AccountSnapshot = {
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
canWithdraw: true, canWithdraw: true,
@@ -1427,7 +1427,7 @@ export class StandxGateway {
} }
} }
private async refreshAccountSnapshot(): Promise<AsterAccountSnapshot | null> { private async refreshAccountSnapshot(): Promise<AccountSnapshot | null> {
try { try {
const [balance, positions] = await Promise.all([ const [balance, positions] = await Promise.all([
this.requestJson<StandxBalanceSnapshot>("/api/query_balance", { method: "GET" }), this.requestJson<StandxBalanceSnapshot>("/api/query_balance", { method: "GET" }),
@@ -1444,7 +1444,7 @@ export class StandxGateway {
} }
if (balance) { if (balance) {
const token = "DUSD"; const token = "DUSD";
const asset: AsterAccountAsset = { const asset: AccountAsset = {
asset: token, asset: token,
walletBalance: String(balance.balance ?? "0"), walletBalance: String(balance.balance ?? "0"),
availableBalance: String(balance.cross_available ?? balance.balance ?? "0"), availableBalance: String(balance.cross_available ?? balance.balance ?? "0"),
@@ -1461,7 +1461,7 @@ export class StandxGateway {
} }
} }
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
return await this.refreshAccountSnapshot(); return await this.refreshAccountSnapshot();
} }
@@ -1515,7 +1515,7 @@ export class StandxGateway {
if (!data?.symbol) return; if (!data?.symbol) return;
const bids = normalizeDepthLevels((data.bids ?? []).map(([price, qty]) => [String(price), String(qty)]), "bid"); const bids = normalizeDepthLevels((data.bids ?? []).map(([price, qty]) => [String(price), String(qty)]), "bid");
const asks = normalizeDepthLevels((data.asks ?? []).map(([price, qty]) => [String(price), String(qty)]), "ask"); const asks = normalizeDepthLevels((data.asks ?? []).map(([price, qty]) => [String(price), String(qty)]), "ask");
const depth: AsterDepth = { const depth: Depth = {
lastUpdateId: Date.now(), lastUpdateId: Date.now(),
bids, bids,
asks, asks,
@@ -1554,7 +1554,7 @@ export class StandxGateway {
}, },
}); });
if (!response || response.s !== "ok" || !Array.isArray(response.t)) return; if (!response || response.s !== "ok" || !Array.isArray(response.t)) return;
const klines: AsterKline[] = response.t.map((openTime, index) => { const klines: Kline[] = response.t.map((openTime, index) => {
const o = response.o?.[index]; const o = response.o?.[index];
const h = response.h?.[index]; const h = response.h?.[index];
const l = response.l?.[index]; const l = response.l?.[index];
@@ -1647,7 +1647,7 @@ export class StandxGateway {
} }
} }
private mapOrder(data: StandxOrder): AsterOrder { private mapOrder(data: StandxOrder): Order {
const clientOrderId = data.cl_ord_id ? String(data.cl_ord_id) : data.id != null ? String(data.id) : ""; const clientOrderId = data.cl_ord_id ? String(data.cl_ord_id) : data.id != null ? String(data.id) : "";
const orderId = clientOrderId || (data.id != null ? String(data.id) : "") || crypto.randomUUID(); const orderId = clientOrderId || (data.id != null ? String(data.id) : "") || crypto.randomUUID();
const normalizedClientId = clientOrderId || orderId; const normalizedClientId = clientOrderId || orderId;
@@ -1675,7 +1675,7 @@ export class StandxGateway {
}; };
} }
private mapPosition(data: StandxPosition): AsterAccountPosition { private mapPosition(data: StandxPosition): AccountPosition {
return { return {
symbol: data.symbol, symbol: data.symbol,
positionAmt: String(data.qty ?? "0"), positionAmt: String(data.qty ?? "0"),
@@ -1690,7 +1690,7 @@ export class StandxGateway {
}; };
} }
private mapBalance(data: StandxBalance): AsterAccountAsset { private mapBalance(data: StandxBalance): AccountAsset {
const walletBalance = data.total ?? data.free ?? "0"; const walletBalance = data.total ?? data.free ?? "0";
const availableBalance = data.free ?? walletBalance; const availableBalance = data.free ?? walletBalance;
return { return {
@@ -1701,7 +1701,7 @@ export class StandxGateway {
}; };
} }
private mapTicker(data: StandxPrice): AsterTicker { private mapTicker(data: StandxPrice): Ticker {
const spread = data.spread ?? [data.spread_bid ?? "0", data.spread_ask ?? "0"]; const spread = data.spread ?? [data.spread_bid ?? "0", data.spread_ask ?? "0"];
const lastPrice = data.last_price ?? data.mark_price ?? data.index_price ?? data.mid_price ?? "0"; const lastPrice = data.last_price ?? data.mark_price ?? data.index_price ?? data.mid_price ?? "0";
return { return {
+19 -92
View File
@@ -1,94 +1,21 @@
import type { AsterOrder, CreateOrderParams } from "../types"; import { createOrderHandlers } from "../order-handlers";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { const handlers = createOrderHandlers({
if (params.quantity === undefined) { exchangeName: "StandX",
params.quantity = intent.quantity; defaultLimitTimeInForce: "GTX",
} defaultMarketTimeInForce: "IOC",
if (params.timeInForce === undefined && intent.timeInForce) { defaultCloseTimeInForce: "IOC",
params.timeInForce = intent.timeInForce; supportsTrailingStop: false,
} supportsTriggerType: false,
if (intent.reduceOnly !== undefined) { stopDefaultReduceOnly: true,
params.reduceOnly = toStringBoolean(intent.reduceOnly); stopDefaultClosePosition: true,
} closeDefaultClosePosition: true,
if (intent.closePosition !== undefined) { });
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> { export const {
const params: CreateOrderParams = applyCommonFields( createLimitOrder,
{ createMarketOrder,
symbol: intent.symbol, createStopOrder,
side: intent.side, createTrailingStopOrder,
type: "LIMIT", createClosePositionOrder,
quantity: intent.quantity, } = handlers;
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
slPrice: intent.slPrice,
tpPrice: intent.tpPrice,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
timeInForce: intent.timeInForce ?? "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
closePosition: toStringBoolean(intent.closePosition ?? true),
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
throw new Error("StandX exchange does not support trailing stop orders");
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
closePosition: toStringBoolean(intent.closePosition ?? true),
timeInForce: intent.timeInForce ?? "IOC",
},
intent
);
return intent.adapter.createOrder(params);
}
+15 -465
View File
@@ -25,12 +25,12 @@ export interface CreateOrderParams {
reduceOnly?: StringBoolean; reduceOnly?: StringBoolean;
closePosition?: StringBoolean; closePosition?: StringBoolean;
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS"; triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
// StandX TPSL 参数 slPrice?: number;
slPrice?: number; // 止损价格 tpPrice?: number;
tpPrice?: number; // 止盈价格 clientOrderId?: string;
} }
export interface AsterAccountPosition { export interface AccountPosition {
symbol: string; symbol: string;
positionAmt: string; positionAmt: string;
entryPrice: string; entryPrice: string;
@@ -51,210 +51,7 @@ export interface AsterAccountPosition {
markPrice?: string; markPrice?: string;
} }
export interface GrvtOrderLeg { export interface AccountAsset {
instrument: string;
size: string;
limit_price?: string;
is_buying_asset?: boolean;
}
export type GrvtTimeInForce =
| "GOOD_TILL_TIME"
| "ALL_OR_NONE"
| "IMMEDIATE_OR_CANCEL"
| "FILL_OR_KILL";
export interface GrvtOrderMetadata {
client_order_id?: string;
create_time?: string;
broker?: string | null;
trigger?: GrvtTriggerMetadata;
}
export interface GrvtOrderState {
status?: string;
reject_reason?: string | null;
book_size?: string[];
traded_size?: string[];
update_time?: string;
avg_fill_price?: string[];
}
export interface GrvtOrder {
order_id: string;
client_order_id?: string;
sub_account_id?: string;
is_market?: boolean;
time_in_force?: GrvtTimeInForce;
post_only?: boolean;
reduce_only?: boolean;
legs?: GrvtOrderLeg[];
metadata?: GrvtOrderMetadata;
state?: GrvtOrderState;
instrument?: string;
}
export interface GrvtTrade {
price: string;
size: string;
taker_side: "BUY" | "SELL";
timestamp: string;
}
export interface GrvtTradeHistoryResponse {
result?: GrvtTrade[];
}
export interface GrvtWebsocketMessage<T> {
stream: string;
selector: string;
sequence_number?: string;
feed: T;
}
export interface GrvtOrderUpdateFeed {
order_id: string;
client_order_id?: string;
sub_account_id?: string;
state?: GrvtOrderState;
traded_size?: string[];
update_time?: string;
}
export interface GrvtPositionUpdateFeed {
instrument: string;
size: string;
entry_price?: string;
mark_price?: string;
unrealized_pnl?: string;
sub_account_id?: string;
update_time?: string;
}
export interface GrvtDepthUpdateFeed {
instrument: string;
bids: GrvtDepthLevel[];
asks: GrvtDepthLevel[];
event_time?: string;
}
export interface GrvtTickerUpdateFeed {
instrument: string;
mark_price?: string;
last_trade_price?: string;
best_bid_price?: string;
best_ask_price?: string;
volume_24h?: string;
}
export interface GrvtOpenOrdersResponse {
result?: GrvtOrder[];
}
export interface GrvtPositionsResponse {
result?: GrvtPosition[];
}
export interface GrvtPosition {
instrument: string;
size: string;
entry_price?: string;
mark_price?: string;
unrealized_pnl?: string;
}
export interface GrvtAccountSnapshot {
total_unrealized_pnl?: string;
positions: GrvtPosition[];
settle_currency?: string;
available_balance?: string;
}
export interface GrvtBalancesResponse {
result?: {
total_unrealized_pnl?: string;
positions?: GrvtPosition[];
};
}
export interface GrvtDepthLevel {
price: string;
size: string;
}
export interface GrvtDepth {
instrument: string;
event_time?: string;
bids: GrvtDepthLevel[];
asks: GrvtDepthLevel[];
}
export interface GrvtTicker {
instrument: string;
mark_price?: string;
last_trade_price?: string;
best_bid_price?: string;
best_ask_price?: string;
volume_24h?: string;
}
export interface GrvtKline {
open_time: number;
close_time: number;
open: string;
high: string;
low: string;
close: string;
volume: string;
number_of_trades?: number;
}
export interface GrvtSignature {
signer: string;
r: string;
s: string;
v: number;
expiration: string;
nonce: number;
}
export interface GrvtUnsignedOrderLeg {
instrument: string;
size: string;
limit_price?: string;
is_buying_asset: boolean;
}
export interface GrvtTriggerMetadata {
trigger_type: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
tpsl: {
trigger_by: "UNSPECIFIED" | "INDEX" | "LAST" | "MID" | "MARK";
trigger_price: string;
close_position: boolean;
};
}
export interface GrvtOrderMetadataInput {
client_order_id: string;
trigger?: GrvtTriggerMetadata;
broker?: string | null;
}
export interface GrvtUnsignedOrder {
sub_account_id: string;
is_market: boolean;
time_in_force: GrvtTimeInForce;
post_only: boolean;
reduce_only: boolean;
legs: GrvtUnsignedOrderLeg[];
metadata: GrvtOrderMetadataInput;
}
export interface GrvtSignedOrder extends GrvtUnsignedOrder {
signature: GrvtSignature;
}
export interface AsterAccountAsset {
asset: string; asset: string;
walletBalance: string; walletBalance: string;
availableBalance: string; availableBalance: string;
@@ -272,7 +69,7 @@ export interface AsterAccountAsset {
marginAvailable?: boolean; marginAvailable?: boolean;
} }
export interface AsterAccountSnapshot { export interface AccountSnapshot {
canTrade: boolean; canTrade: boolean;
canDeposit: boolean; canDeposit: boolean;
canWithdraw: boolean; canWithdraw: boolean;
@@ -288,8 +85,8 @@ export interface AsterAccountSnapshot {
totalCrossUnPnl?: string; totalCrossUnPnl?: string;
availableBalance?: string; availableBalance?: string;
maxWithdrawAmount?: string; maxWithdrawAmount?: string;
positions: AsterAccountPosition[]; positions: AccountPosition[];
assets: AsterAccountAsset[]; assets: AccountAsset[];
marketType?: "perp" | "spot"; marketType?: "perp" | "spot";
baseAsset?: string; baseAsset?: string;
quoteAsset?: string; quoteAsset?: string;
@@ -297,22 +94,22 @@ export interface AsterAccountSnapshot {
quoteAssetId?: number; quoteAssetId?: number;
} }
export interface AsterDepthLevel extends Array<string> { export interface DepthLevel extends Array<string> {
0: string; // price 0: string; // price
1: string; // quantity 1: string; // quantity
} }
export interface AsterDepth { export interface Depth {
lastUpdateId: number; lastUpdateId: number;
bids: AsterDepthLevel[]; bids: DepthLevel[];
asks: AsterDepthLevel[]; asks: DepthLevel[];
eventTime?: number; eventTime?: number;
eventType?: string; eventType?: string;
tradeTime?: number; tradeTime?: number;
symbol?: string; symbol?: string;
} }
export interface AsterTicker { export interface Ticker {
symbol: string; symbol: string;
lastPrice: string; lastPrice: string;
openPrice: string; openPrice: string;
@@ -336,254 +133,7 @@ export interface AsterTicker {
count?: number; count?: number;
} }
export interface AsterSpotRateLimit { export interface Kline {
rateLimitType: string;
interval: string;
intervalNum: number;
limit: number;
}
export interface AsterSpotExchangeFilter {
filterType: string;
[key: string]: string | number | boolean | undefined;
}
export interface AsterFuturesSymbolFilter {
filterType: string;
tickSize?: string;
stepSize?: string;
minPrice?: string;
maxPrice?: string;
minQty?: string;
maxQty?: string;
[key: string]: string | number | boolean | undefined;
}
export interface AsterFuturesSymbolInfo {
symbol: string;
pair?: string;
contractType?: string;
pricePrecision?: number;
quantityPrecision?: number;
baseAssetPrecision?: number;
quotePrecision?: number;
underlyingType?: string;
filters?: AsterFuturesSymbolFilter[];
}
export interface AsterFuturesExchangeInfo {
timezone?: string;
serverTime?: number;
symbols?: AsterFuturesSymbolInfo[];
}
export interface AsterSpotAssetInfo {
asset: string;
}
export interface AsterSpotSymbolInfo {
symbol: string;
status: string;
baseAsset: string;
quoteAsset: string;
baseAssetPrecision?: number;
quotePrecision?: number;
pricePrecision?: number;
quantityPrecision?: number;
orderTypes: string[];
timeInForce: string[];
ocoAllowed: boolean;
filters: AsterSpotExchangeFilter[];
}
export interface AsterSpotExchangeInfo {
timezone: string;
serverTime: number;
rateLimits: AsterSpotRateLimit[];
exchangeFilters: AsterSpotExchangeFilter[];
assets?: AsterSpotAssetInfo[];
symbols: AsterSpotSymbolInfo[];
}
export interface AsterSpotDepth {
lastUpdateId: number;
E?: number;
T?: number;
bids: AsterDepthLevel[];
asks: AsterDepthLevel[];
}
export interface AsterSpotTrade {
id: number;
price: string;
qty: string;
baseQty?: string;
quoteQty?: string;
time: number;
isBuyerMaker: boolean;
}
export interface AsterSpotHistoricalTrade extends AsterSpotTrade {
isBestMatch?: boolean;
}
export interface AsterSpotAggTrade {
a: number;
p: string;
q: string;
f: number;
l: number;
T: number;
m: boolean;
M?: boolean;
}
export interface AsterSpotKline {
openTime: number;
open: string;
high: string;
low: string;
close: string;
volume: string;
closeTime: number;
quoteAssetVolume: string;
numberOfTrades: number;
takerBuyBaseAssetVolume: string;
takerBuyQuoteAssetVolume: string;
}
export interface AsterSpotTicker24h {
symbol: string;
priceChange: string;
priceChangePercent: string;
weightedAvgPrice: string;
prevClosePrice: string;
lastPrice: string;
lastQty: string;
bidPrice: string;
bidQty: string;
askPrice: string;
askQty: string;
openPrice: string;
highPrice: string;
lowPrice: string;
volume: string;
quoteVolume: string;
openTime: number;
closeTime: number;
firstId: number;
lastId: number;
count: number;
baseAsset?: string;
quoteAsset?: string;
}
export interface AsterSpotPriceTicker {
symbol: string;
price: string;
time?: number;
}
export interface AsterSpotBookTicker {
symbol: string;
bidPrice: string;
bidQty: string;
askPrice: string;
askQty: string;
time?: number;
}
export interface AsterSpotCommissionRate {
symbol: string;
makerCommissionRate: string;
takerCommissionRate: string;
}
export interface CreateSpotOrderParams {
symbol: string;
side: OrderSide;
type: OrderType;
timeInForce?: TimeInForce;
quantity?: number | string;
quoteOrderQty?: number | string;
price?: number | string;
newClientOrderId?: string;
stopPrice?: number | string;
recvWindow?: number;
}
export interface CancelSpotOrderParams {
symbol: string;
orderId?: number | string;
origClientOrderId?: string;
recvWindow?: number;
}
export interface QuerySpotOrderParams extends CancelSpotOrderParams {}
export interface SpotOpenOrdersParams {
symbol?: string;
recvWindow?: number;
orderIdList?: Array<number | string>;
origClientOrderIdList?: string[];
}
export interface SpotAllOrdersParams {
symbol: string;
orderId?: number;
startTime?: number;
endTime?: number;
limit?: number;
recvWindow?: number;
}
export interface AsterSpotAccountBalance {
asset: string;
free: string;
locked: string;
}
export interface AsterSpotAccount {
feeTier: number;
canTrade: boolean;
canDeposit: boolean;
canWithdraw: boolean;
canBurnAsset?: boolean;
updateTime: number;
makerCommission?: string;
takerCommission?: string;
buyerCommission?: string;
sellerCommission?: string;
balances: AsterSpotAccountBalance[];
}
export interface SpotUserTradesParams {
symbol?: string;
orderId?: number;
startTime?: number;
endTime?: number;
fromId?: number;
limit?: number;
recvWindow?: number;
}
export interface AsterSpotUserTrade {
symbol: string;
id: number;
orderId: number;
side: OrderSide;
price: string;
qty: string;
quoteQty?: string;
commission: string;
commissionAsset: string;
time: number;
counterpartyId?: number;
maker: boolean;
buyer: boolean;
}
export interface AsterKline {
eventType?: string; eventType?: string;
eventTime?: number; eventTime?: number;
symbol?: string; symbol?: string;
@@ -604,7 +154,7 @@ export interface AsterKline {
isClosed?: boolean; isClosed?: boolean;
} }
export interface AsterOrder { export interface Order {
orderId: number | string; orderId: number | string;
clientOrderId: string; clientOrderId: string;
symbol: string; symbol: string;
+4
View File
@@ -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}",
-1
View File
@@ -4,7 +4,6 @@ import { App } from "./ui/App";
import { setupGlobalErrorHandlers } from "./runtime-errors"; import { setupGlobalErrorHandlers } from "./runtime-errors";
import { parseCliArgs, printCliHelp } from "./cli/args"; import { parseCliArgs, printCliHelp } from "./cli/args";
import { startStrategy } from "./cli/strategy-runner"; import { startStrategy } from "./cli/strategy-runner";
import { resolveExchangeId } from "./exchanges/create-adapter";
import { CommandParseError, parseCommandArgv, printCommandHelp } from "./cli/command-parser"; import { CommandParseError, parseCommandArgv, printCommandHelp } from "./cli/command-parser";
import { executeCliCommand, renderCommandPayload } from "./cli/command-executor"; import { executeCliCommand, renderCommandPayload } from "./cli/command-executor";
+10 -9
View File
@@ -1,7 +1,8 @@
import type { BasisArbConfig } from "../config"; import type { BasisArbConfig } from "../config";
import type { ExchangeAdapter, FundingRateSnapshot } from "../exchanges/adapter"; import type { ExchangeAdapter, FundingRateSnapshot } from "../exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterSpotBookTicker } from "../exchanges/types"; import type { AccountSnapshot, Depth } from "../exchanges/types";
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/client"; import type { AsterSpotBookTicker } from "../exchanges/aster/types";
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/gateway";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { StrategyEventEmitter } from "./common/event-emitter"; import { StrategyEventEmitter } from "./common/event-emitter";
import { safeSubscribe, type LogHandler } from "./common/subscriptions"; import { safeSubscribe, type LogHandler } from "./common/subscriptions";
@@ -155,7 +156,7 @@ export class BasisArbEngine {
private bootstrap(): void { private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterDepth>( safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.futuresSymbol), this.exchange.watchDepth.bind(this.exchange, this.config.futuresSymbol),
(depth) => { (depth) => {
this.applyFuturesDepth(depth); this.applyFuturesDepth(depth);
@@ -168,7 +169,7 @@ export class BasisArbEngine {
); );
if (this.exchange.id === "nado" || this.exchange.id === "standx" || this.exchange.id === "binance") { if (this.exchange.id === "nado" || this.exchange.id === "standx" || this.exchange.id === "binance") {
safeSubscribe<AsterDepth>( safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol), this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
(depth) => { (depth) => {
this.applySpotDepth(depth); this.applySpotDepth(depth);
@@ -196,7 +197,7 @@ export class BasisArbEngine {
); );
} }
safeSubscribe<AsterAccountSnapshot>( safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange), this.exchange.watchAccount.bind(this.exchange),
(snapshot) => { (snapshot) => {
this.applyAccountSnapshot(snapshot); this.applyAccountSnapshot(snapshot);
@@ -210,7 +211,7 @@ export class BasisArbEngine {
} }
} }
private applyFuturesDepth(depth: AsterDepth): void { private applyFuturesDepth(depth: Depth): void {
if (!depth?.bids?.length || !depth?.asks?.length) { if (!depth?.bids?.length || !depth?.asks?.length) {
return; return;
} }
@@ -360,7 +361,7 @@ export class BasisArbEngine {
this.emitUpdate(); this.emitUpdate();
} }
private applySpotDepth(depth: AsterDepth): void { private applySpotDepth(depth: Depth): void {
if (!depth?.bids?.length || !depth?.asks?.length) { if (!depth?.bids?.length || !depth?.asks?.length) {
return; return;
} }
@@ -395,7 +396,7 @@ export class BasisArbEngine {
this.emitUpdate(); this.emitUpdate();
} }
private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void { private applyAccountSnapshot(snapshot: AccountSnapshot): void {
const assets = Array.isArray(snapshot.assets) ? snapshot.assets : []; const assets = Array.isArray(snapshot.assets) ? snapshot.assets : [];
const spotBalances: SpotBalanceStateEntry[] = []; const spotBalances: SpotBalanceStateEntry[] = [];
@@ -409,7 +410,7 @@ export class BasisArbEngine {
if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue; if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue;
if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue; if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue;
const isTaggedFuturesAsset = /0$/.test(name); const isTaggedFuturesAsset = name.endsWith("0");
if (isTaggedFuturesAsset) { if (isTaggedFuturesAsset) {
futuresBalances.push({ asset: name.replace(/0$/, ""), wallet, available }); futuresBalances.push({ asset: name.replace(/0$/, ""), wallet, available });
continue; continue;
+10 -10
View File
@@ -1,5 +1,5 @@
import NodeWebSocket from "ws"; import NodeWebSocket from "ws";
import type { AsterDepthLevel } from "../../exchanges/types"; import type { DepthLevel } from "../../exchanges/types";
import type { DepthImbalance } from "../../utils/depth"; import type { DepthImbalance } from "../../utils/depth";
const WebSocketCtor: typeof globalThis.WebSocket = const WebSocketCtor: typeof globalThis.WebSocket =
@@ -56,14 +56,14 @@ export type BinanceConnectionListener = (state: BinanceConnectionState) => void;
interface DepthUpdateEvent { interface DepthUpdateEvent {
U: number; U: number;
u: number; u: number;
bids: AsterDepthLevel[]; bids: DepthLevel[];
asks: AsterDepthLevel[]; asks: DepthLevel[];
} }
interface DepthSnapshotResponse { interface DepthSnapshotResponse {
lastUpdateId: number; lastUpdateId: number;
bids: AsterDepthLevel[]; bids: DepthLevel[];
asks: AsterDepthLevel[]; asks: DepthLevel[];
} }
export class BinanceDepthTracker { export class BinanceDepthTracker {
@@ -540,7 +540,7 @@ export class BinanceDepthTracker {
return true; return true;
} }
private applyLevels(book: Map<string, number>, levels: AsterDepthLevel[]): void { private applyLevels(book: Map<string, number>, levels: DepthLevel[]): void {
for (const level of levels) { for (const level of levels) {
const priceRaw = level?.[0]; const priceRaw = level?.[0];
const qtyRaw = level?.[1]; const qtyRaw = level?.[1];
@@ -657,8 +657,8 @@ export class BinanceDepthTracker {
throw new Error("invalid lastUpdateId"); throw new Error("invalid lastUpdateId");
} }
const bids = Array.isArray(json.bids) ? (json.bids as AsterDepthLevel[]) : []; const bids = Array.isArray(json.bids) ? (json.bids as DepthLevel[]) : [];
const asks = Array.isArray(json.asks) ? (json.asks as AsterDepthLevel[]) : []; const asks = Array.isArray(json.asks) ? (json.asks as DepthLevel[]) : [];
this.lastRestSyncAt = Date.now(); this.lastRestSyncAt = Date.now();
this.restConsecutiveFailures = 0; this.restConsecutiveFailures = 0;
this.restLastError = null; this.restLastError = null;
@@ -697,8 +697,8 @@ export class BinanceDepthTracker {
const bidsRaw = Array.isArray(payload.b) ? payload.b : []; const bidsRaw = Array.isArray(payload.b) ? payload.b : [];
const asksRaw = Array.isArray(payload.a) ? payload.a : []; const asksRaw = Array.isArray(payload.a) ? payload.a : [];
const bids = bidsRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[]; const bids = bidsRaw.filter((level): level is DepthLevel => Array.isArray(level)) as DepthLevel[];
const asks = asksRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[]; const asks = asksRaw.filter((level): level is DepthLevel => Array.isArray(level)) as DepthLevel[];
return { U, u, bids, asks }; return { U, u, bids, asks };
} catch { } catch {
+15 -2
View File
@@ -5,6 +5,19 @@ import type { GridDirection } from "../../config";
const DATA_DIR = process.env.GRID_DATA_DIR?.trim() || path.resolve("data"); const DATA_DIR = process.env.GRID_DATA_DIR?.trim() || path.resolve("data");
const GRID_FILE = path.resolve(DATA_DIR, "grid-record.json"); const GRID_FILE = path.resolve(DATA_DIR, "grid-record.json");
/** State of a single grid level */
export type LevelState = "idle" | "filled" | "exit_placed";
export interface StoredLevelInfo {
state: LevelState;
/** The grid level index where ENTRY was filled */
sourceLevel: number;
/** The grid level index where EXIT is targeted (closeTarget) */
targetLevel: number | null;
/** The orderId of the EXIT order on exchange (if exit_placed) */
exitOrderId?: string;
}
export interface StoredGridState { export interface StoredGridState {
symbol: string; symbol: string;
lowerPrice: number; lowerPrice: number;
@@ -13,8 +26,8 @@ export interface StoredGridState {
orderSize: number; orderSize: number;
maxPositionSize: number; maxPositionSize: number;
direction: GridDirection; direction: GridDirection;
longExposure: Record<string, number>; /** Per-level state: key is level index string */
shortExposure: Record<string, number>; levels: Record<string, StoredLevelInfo>;
updatedAt: number; updatedAt: number;
} }
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -1,6 +1,6 @@
import type { TradingConfig } from "../config"; import type { TradingConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { AsterAccountSnapshot, AsterOrder, AsterTicker } from "../exchanges/types"; import type { AccountSnapshot, Order, Ticker } from "../exchanges/types";
import { import {
calcStopLossPrice, calcStopLossPrice,
calcTrailingActivationPrice, calcTrailingActivationPrice,
@@ -30,11 +30,11 @@ export interface GuardianEngineSnapshot {
unrealized: number; unrealized: number;
targetStopPrice: number | null; targetStopPrice: number | null;
trailingActivationPrice: number | null; trailingActivationPrice: number | null;
stopOrder: AsterOrder | null; stopOrder: Order | null;
trailingOrder: AsterOrder | null; trailingOrder: Order | null;
requiresStop: boolean; requiresStop: boolean;
tradeLog: TradeLogEntry[]; tradeLog: TradeLogEntry[];
openOrders: AsterOrder[]; openOrders: Order[];
lastUpdated: number | null; lastUpdated: number | null;
guardStatus: "idle" | "protecting" | "pending"; guardStatus: "idle" | "protecting" | "pending";
} }
@@ -43,9 +43,9 @@ type GuardianEngineEvent = "update";
type GuardianEngineListener = (snapshot: GuardianEngineSnapshot) => void; type GuardianEngineListener = (snapshot: GuardianEngineSnapshot) => void;
export class GuardianEngine { export class GuardianEngine {
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private openOrders: AsterOrder[] = []; private openOrders: Order[] = [];
private tickerSnapshot: AsterTicker | null = null; private tickerSnapshot: Ticker | null = null;
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
@@ -101,7 +101,7 @@ export class GuardianEngine {
private bootstrap(): void { private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterAccountSnapshot>( safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange), this.exchange.watchAccount.bind(this.exchange),
(snapshot) => { (snapshot) => {
this.accountSnapshot = snapshot; this.accountSnapshot = snapshot;
@@ -114,7 +114,7 @@ export class GuardianEngine {
} }
); );
safeSubscribe<AsterOrder[]>( safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange), this.exchange.watchOrders.bind(this.exchange),
(orders) => { (orders) => {
this.synchronizeLocks(orders); this.synchronizeLocks(orders);
@@ -141,7 +141,7 @@ export class GuardianEngine {
} }
); );
safeSubscribe<AsterTicker>( safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol), this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => { (ticker) => {
this.tickerSnapshot = ticker; this.tickerSnapshot = ticker;
@@ -155,7 +155,7 @@ export class GuardianEngine {
); );
} }
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void { private synchronizeLocks(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : []; const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => { Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type]; const pendingId = this.pending[type];
@@ -261,8 +261,8 @@ export class GuardianEngine {
price: number; price: number;
stopPrice: number; stopPrice: number;
activationPrice: number; activationPrice: number;
currentStop?: AsterOrder; currentStop?: Order;
currentTrailing?: AsterOrder; currentTrailing?: Order;
}): Promise<void> { }): Promise<void> {
const { position, direction, stopSide, price, stopPrice, activationPrice, currentStop, currentTrailing } = params; const { position, direction, stopSide, price, stopPrice, activationPrice, currentStop, currentTrailing } = params;
@@ -416,7 +416,7 @@ export class GuardianEngine {
private async tryReplaceStop( private async tryReplaceStop(
side: "BUY" | "SELL", side: "BUY" | "SELL",
currentOrder: AsterOrder, currentOrder: Order,
nextStopPrice: number, nextStopPrice: number,
lastPrice: number lastPrice: number
): Promise<void> { ): Promise<void> {
@@ -559,7 +559,7 @@ export class GuardianEngine {
} }
} }
private isProtectiveOrder(order: AsterOrder): boolean { private isProtectiveOrder(order: Order): boolean {
if (order.symbol !== this.config.symbol) { if (order.symbol !== this.config.symbol) {
return false; return false;
} }
@@ -571,14 +571,14 @@ export class GuardianEngine {
return type === "STOP_MARKET" || hasStopPrice; return type === "STOP_MARKET" || hasStopPrice;
} }
private findStopOrder(side: "BUY" | "SELL"): AsterOrder | undefined { private findStopOrder(side: "BUY" | "SELL"): Order | undefined {
return this.openOrders.find((order) => { return this.openOrders.find((order) => {
const hasStopPrice = Number.isFinite(Number(order.stopPrice)) && Number(order.stopPrice) > 0; const hasStopPrice = Number.isFinite(Number(order.stopPrice)) && Number(order.stopPrice) > 0;
return order.side === side && (order.type === "STOP_MARKET" || hasStopPrice); return order.side === side && (order.type === "STOP_MARKET" || hasStopPrice);
}); });
} }
private findTrailingOrder(side: "BUY" | "SELL"): AsterOrder | undefined { private findTrailingOrder(side: "BUY" | "SELL"): Order | undefined {
return this.openOrders.find((order) => order.type === "TRAILING_STOP_MARKET" && order.side === side); return this.openOrders.find((order) => order.type === "TRAILING_STOP_MARKET" && order.side === side);
} }
+20 -20
View File
@@ -1,11 +1,11 @@
import type { LiquidityMakerConfig } from "../config"; import type { LiquidityMakerConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
} from "../exchanges/types"; } from "../exchanges/types";
import { formatPriceToString } from "../utils/math"; import { formatPriceToString } from "../utils/math";
import { createTradeLog } from "../logging/trade-log"; import { createTradeLog } from "../logging/trade-log";
@@ -65,12 +65,12 @@ type MakerListener = (snapshot: LiquidityMakerEngineSnapshot) => void;
const EPS = 1e-5; const EPS = 1e-5;
export class LiquidityMakerEngine { export class LiquidityMakerEngine {
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private depthSnapshot: AsterDepth | null = null; private depthSnapshot: Depth | null = null;
private tickerSnapshot: AsterTicker | null = null; private tickerSnapshot: Ticker | null = null;
private lastKline: AsterKline | null = null; private lastKline: Kline | null = null;
private liveCandle: { startMs: number; open: number; close: number } | null = null; private liveCandle: { startMs: number; open: number; close: number } | null = null;
private openOrders: AsterOrder[] = []; private openOrders: Order[] = [];
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
@@ -180,7 +180,7 @@ export class LiquidityMakerEngine {
private bootstrap(): void { private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterAccountSnapshot>( safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange), this.exchange.watchAccount.bind(this.exchange),
(snapshot) => { (snapshot) => {
this.accountSnapshot = snapshot; this.accountSnapshot = snapshot;
@@ -224,7 +224,7 @@ export class LiquidityMakerEngine {
} }
); );
safeSubscribe<AsterOrder[]>( safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange), this.exchange.watchOrders.bind(this.exchange),
(orders) => { (orders) => {
this.syncLocksWithOrders(orders); this.syncLocksWithOrders(orders);
@@ -232,7 +232,7 @@ export class LiquidityMakerEngine {
// 检测成交:对比上一轮的订单ID // 检测成交:对比上一轮的订单ID
const currentIds = new Set<string>(); const currentIds = new Set<string>();
const activeOrders: AsterOrder[] = []; const activeOrders: Order[] = [];
if (Array.isArray(orders)) { if (Array.isArray(orders)) {
for (const order of orders) { for (const order of orders) {
@@ -268,7 +268,7 @@ export class LiquidityMakerEngine {
} }
); );
safeSubscribe<AsterDepth>( safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol), this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => { (depth) => {
this.depthSnapshot = depth; this.depthSnapshot = depth;
@@ -282,7 +282,7 @@ export class LiquidityMakerEngine {
} }
); );
safeSubscribe<AsterTicker>( safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol), this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => { (ticker) => {
this.tickerSnapshot = ticker; this.tickerSnapshot = ticker;
@@ -296,7 +296,7 @@ export class LiquidityMakerEngine {
} }
); );
safeSubscribe<AsterKline[]>( safeSubscribe<Kline[]>(
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"), this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
(klines) => { (klines) => {
if (!Array.isArray(klines) || !klines.length) return; if (!Array.isArray(klines) || !klines.length) return;
@@ -318,7 +318,7 @@ export class LiquidityMakerEngine {
} }
/** 检测成交订单 */ /** 检测成交订单 */
private detectFills(orders: AsterOrder[] | null | undefined): void { private detectFills(orders: Order[] | null | undefined): void {
if (!Array.isArray(orders)) return; if (!Array.isArray(orders)) return;
// 查找已成交或部分成交的订单 // 查找已成交或部分成交的订单
@@ -357,7 +357,7 @@ export class LiquidityMakerEngine {
} }
} }
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void { private syncLocksWithOrders(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : []; const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => { Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type]; const pendingId = this.pending[type];
@@ -759,7 +759,7 @@ export class LiquidityMakerEngine {
* depthImbalanceRatio * depthImbalanceRatio
* 簿 * 簿
*/ */
private evaluateDepth(depth: AsterDepth): { private evaluateDepth(depth: Depth): {
buySum: number; buySum: number;
sellSum: number; sellSum: number;
skipBuySide: boolean; skipBuySide: boolean;
@@ -1186,7 +1186,7 @@ export class LiquidityMakerEngine {
return position; return position;
} }
private getSpotBalances(snapshot: AsterAccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null { private getSpotBalances(snapshot: AccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
const assets = snapshot?.assets ?? []; const assets = snapshot?.assets ?? [];
if (!assets.length) return null; if (!assets.length) return null;
const parsed = parseSymbolParts(this.config.symbol); const parsed = parseSymbolParts(this.config.symbol);
+14 -15
View File
@@ -1,11 +1,10 @@
import type { MakerConfig } from "../config"; import type { MakerConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Order,
AsterOrder, Ticker,
AsterTicker,
} from "../exchanges/types"; } from "../exchanges/types";
import { formatPriceToString } from "../utils/math"; import { formatPriceToString } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
@@ -48,7 +47,7 @@ export interface MakerEngineSnapshot {
pnl: number; pnl: number;
accountUnrealized: number; accountUnrealized: number;
sessionVolume: number; sessionVolume: number;
openOrders: AsterOrder[]; openOrders: Order[];
desiredOrders: DesiredOrder[]; desiredOrders: DesiredOrder[];
tradeLog: TradeLogEntry[]; tradeLog: TradeLogEntry[];
lastUpdated: number | null; lastUpdated: number | null;
@@ -67,10 +66,10 @@ const EPS = 1e-5;
const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000; const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000;
export class MakerEngine { export class MakerEngine {
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private depthSnapshot: AsterDepth | null = null; private depthSnapshot: Depth | null = null;
private tickerSnapshot: AsterTicker | null = null; private tickerSnapshot: Ticker | null = null;
private openOrders: AsterOrder[] = []; private openOrders: Order[] = [];
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
@@ -155,7 +154,7 @@ export class MakerEngine {
private bootstrap(): void { private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterAccountSnapshot>( safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange), this.exchange.watchAccount.bind(this.exchange),
(snapshot) => { (snapshot) => {
this.accountSnapshot = snapshot; this.accountSnapshot = snapshot;
@@ -179,7 +178,7 @@ export class MakerEngine {
} }
); );
safeSubscribe<AsterOrder[]>( safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange), this.exchange.watchOrders.bind(this.exchange),
(orders) => { (orders) => {
this.syncLocksWithOrders(orders); this.syncLocksWithOrders(orders);
@@ -212,7 +211,7 @@ export class MakerEngine {
} }
); );
safeSubscribe<AsterDepth>( safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol), this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => { (depth) => {
this.depthSnapshot = depth; this.depthSnapshot = depth;
@@ -230,7 +229,7 @@ export class MakerEngine {
} }
); );
safeSubscribe<AsterTicker>( safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol), this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => { (ticker) => {
this.tickerSnapshot = ticker; this.tickerSnapshot = ticker;
@@ -251,7 +250,7 @@ export class MakerEngine {
// Maker strategy does not require realtime klines. // Maker strategy does not require realtime klines.
} }
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void { private syncLocksWithOrders(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : []; const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => { Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type]; const pendingId = this.pending[type];
+20 -20
View File
@@ -1,10 +1,10 @@
import type { MakerPointsConfig } from "../config"; import type { MakerPointsConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterOrder, Order,
AsterTicker, Ticker,
} from "../exchanges/types"; } from "../exchanges/types";
import { formatPriceToString } from "../utils/math"; import { formatPriceToString } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
@@ -59,7 +59,7 @@ export interface MakerPointsSnapshot {
pnl: number; pnl: number;
accountUnrealized: number; accountUnrealized: number;
sessionVolume: number; sessionVolume: number;
openOrders: AsterOrder[]; openOrders: Order[];
desiredOrders: DesiredOrder[]; desiredOrders: DesiredOrder[];
tradeLog: TradeLogEntry[]; tradeLog: TradeLogEntry[];
lastUpdated: number | null; lastUpdated: number | null;
@@ -102,10 +102,10 @@ const STANDX_MARGIN_MODE_MAX_ATTEMPTS = 10;
const ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS = 5_000; const ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS = 5_000;
export class MakerPointsEngine { export class MakerPointsEngine {
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private depthSnapshot: AsterDepth | null = null; private depthSnapshot: Depth | null = null;
private tickerSnapshot: AsterTicker | null = null; private tickerSnapshot: Ticker | null = null;
private openOrders: AsterOrder[] = []; private openOrders: Order[] = [];
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
@@ -298,7 +298,7 @@ export class MakerPointsEngine {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
this.setupRestHealthProtection(); this.setupRestHealthProtection();
safeSubscribe<AsterAccountSnapshot>( safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange), this.exchange.watchAccount.bind(this.exchange),
(snapshot) => { (snapshot) => {
this.applyAccountSnapshot(snapshot); this.applyAccountSnapshot(snapshot);
@@ -310,7 +310,7 @@ export class MakerPointsEngine {
} }
); );
safeSubscribe<AsterOrder[]>( safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange), this.exchange.watchOrders.bind(this.exchange),
(orders) => { (orders) => {
this.syncLocksWithOrders(orders); this.syncLocksWithOrders(orders);
@@ -339,7 +339,7 @@ export class MakerPointsEngine {
} }
); );
safeSubscribe<AsterDepth>( safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol), this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => { (depth) => {
this.depthSnapshot = depth; this.depthSnapshot = depth;
@@ -358,7 +358,7 @@ export class MakerPointsEngine {
} }
); );
safeSubscribe<AsterTicker>( safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol), this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => { (ticker) => {
this.tickerSnapshot = ticker; this.tickerSnapshot = ticker;
@@ -376,7 +376,7 @@ export class MakerPointsEngine {
this.setupConnectionProtection(); this.setupConnectionProtection();
} }
private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void { private applyAccountSnapshot(snapshot: AccountSnapshot): void {
this.accountSnapshot = snapshot; this.accountSnapshot = snapshot;
// StandX: WS 推送使用本地接收时间戳;REST 快照使用响应里的 time 字段映射到 snapshot.updateTime // StandX: WS 推送使用本地接收时间戳;REST 快照使用响应里的 time 字段映射到 snapshot.updateTime
this.lastStandxAccountTime = this.lastStandxAccountTime =
@@ -545,7 +545,7 @@ export class MakerPointsEngine {
} }
} }
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void { private syncLocksWithOrders(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : []; const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => { Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type]; const pendingId = this.pending[type];
@@ -746,7 +746,7 @@ export class MakerPointsEngine {
ask1: number; ask1: number;
skipBuy: boolean; skipBuy: boolean;
skipSell: boolean; skipSell: boolean;
depth: AsterDepth | null; depth: Depth | null;
}): DesiredOrder[] { }): DesiredOrder[] {
const { bid1, ask1, skipBuy, skipSell, depth } = params; const { bid1, ask1, skipBuy, skipSell, depth } = params;
@@ -837,7 +837,7 @@ export class MakerPointsEngine {
* *
*/ */
private checkDepthStatusChanged( private checkDepthStatusChanged(
depth: AsterDepth | null, depth: Depth | null,
bid1: number, bid1: number,
ask1: number ask1: number
): boolean { ): boolean {
@@ -879,7 +879,7 @@ export class MakerPointsEngine {
/** /**
* *
*/ */
private shouldTriggerImmediateDepthProtection(depth: AsterDepth | null): boolean { private shouldTriggerImmediateDepthProtection(depth: Depth | null): boolean {
if (!depth) return false; if (!depth) return false;
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false; if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
@@ -917,7 +917,7 @@ export class MakerPointsEngine {
/** /**
* minRepriceBps * minRepriceBps
*/ */
private shouldTriggerImmediateReprice(depth: AsterDepth | null): boolean { private shouldTriggerImmediateReprice(depth: Depth | null): boolean {
if (!depth) return false; if (!depth) return false;
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false; if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
@@ -1993,7 +1993,7 @@ export class MakerPointsEngine {
void poll(); void poll();
} }
private getStandxMarginMode(snapshot: AsterAccountSnapshot | null): string | null { private getStandxMarginMode(snapshot: AccountSnapshot | null): string | null {
if (this.exchange.id !== "standx") return null; if (this.exchange.id !== "standx") return null;
const positions = snapshot?.positions ?? []; const positions = snapshot?.positions ?? [];
const match = positions.find((pos) => pos.symbol === this.config.symbol); const match = positions.find((pos) => pos.symbol === this.config.symbol);
+18 -18
View File
@@ -1,11 +1,11 @@
import type { MakerConfig } from "../config"; import type { MakerConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
} from "../exchanges/types"; } from "../exchanges/types";
import { formatPriceToString } from "../utils/math"; import { formatPriceToString } from "../utils/math";
import { createTradeLog } from "../logging/trade-log"; import { createTradeLog } from "../logging/trade-log";
@@ -56,12 +56,12 @@ type MakerListener = (snapshot: OffsetMakerEngineSnapshot) => void;
const EPS = 1e-5; const EPS = 1e-5;
export class OffsetMakerEngine { export class OffsetMakerEngine {
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private depthSnapshot: AsterDepth | null = null; private depthSnapshot: Depth | null = null;
private tickerSnapshot: AsterTicker | null = null; private tickerSnapshot: Ticker | null = null;
private lastKline: AsterKline | null = null; private lastKline: Kline | null = null;
private liveCandle: { startMs: number; open: number; close: number } | null = null; private liveCandle: { startMs: number; open: number; close: number } | null = null;
private openOrders: AsterOrder[] = []; private openOrders: Order[] = [];
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
@@ -163,7 +163,7 @@ export class OffsetMakerEngine {
private bootstrap(): void { private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterAccountSnapshot>( safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange), this.exchange.watchAccount.bind(this.exchange),
(snapshot) => { (snapshot) => {
this.accountSnapshot = snapshot; this.accountSnapshot = snapshot;
@@ -207,7 +207,7 @@ export class OffsetMakerEngine {
} }
); );
safeSubscribe<AsterOrder[]>( safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange), this.exchange.watchOrders.bind(this.exchange),
(orders) => { (orders) => {
this.syncLocksWithOrders(orders); this.syncLocksWithOrders(orders);
@@ -236,7 +236,7 @@ export class OffsetMakerEngine {
} }
); );
safeSubscribe<AsterDepth>( safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol), this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => { (depth) => {
this.depthSnapshot = depth; this.depthSnapshot = depth;
@@ -250,7 +250,7 @@ export class OffsetMakerEngine {
} }
); );
safeSubscribe<AsterTicker>( safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol), this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => { (ticker) => {
this.tickerSnapshot = ticker; this.tickerSnapshot = ticker;
@@ -264,7 +264,7 @@ export class OffsetMakerEngine {
} }
); );
safeSubscribe<AsterKline[]>( safeSubscribe<Kline[]>(
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"), this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
(klines) => { (klines) => {
if (!Array.isArray(klines) || !klines.length) return; if (!Array.isArray(klines) || !klines.length) return;
@@ -284,7 +284,7 @@ export class OffsetMakerEngine {
); );
} }
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void { private syncLocksWithOrders(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : []; const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => { Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type]; const pendingId = this.pending[type];
@@ -600,7 +600,7 @@ export class OffsetMakerEngine {
} }
} }
private evaluateDepth(depth: AsterDepth): { private evaluateDepth(depth: Depth): {
buySum: number; buySum: number;
sellSum: number; sellSum: number;
skipBuySide: boolean; skipBuySide: boolean;
@@ -1038,7 +1038,7 @@ export class OffsetMakerEngine {
return position; return position;
} }
private getSpotBalances(snapshot: AsterAccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null { private getSpotBalances(snapshot: AccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
const assets = snapshot?.assets ?? []; const assets = snapshot?.assets ?? [];
if (!assets.length) return null; if (!assets.length) return null;
const parsed = parseSymbolParts(this.config.symbol); const parsed = parseSymbolParts(this.config.symbol);
+14 -15
View File
@@ -1,5 +1,5 @@
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterOrder, AsterTicker } from "../exchanges/types"; import type { AccountSnapshot, Depth, Order, Ticker } from "../exchanges/types";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { marketClose, placeMarketOrder, placeStopLossOrder, unlockOperating } from "../core/order-coordinator"; import { marketClose, placeMarketOrder, placeStopLossOrder, unlockOperating } from "../core/order-coordinator";
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator"; import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
@@ -55,9 +55,9 @@ export interface SwingEngineSnapshot {
stopLossTarget: number | null; stopLossTarget: number | null;
stopLossKillSwitch: boolean; stopLossKillSwitch: boolean;
openOrders: AsterOrder[]; openOrders: Order[];
depth: AsterDepth | null; depth: Depth | null;
ticker: AsterTicker | null; ticker: Ticker | null;
tradeLog: TradeLogEntry[]; tradeLog: TradeLogEntry[];
lastUpdated: number | null; lastUpdated: number | null;
@@ -70,10 +70,10 @@ type SwingListener = (snapshot: SwingEngineSnapshot) => void;
const EPS = 1e-5; const EPS = 1e-5;
export class SwingEngine { export class SwingEngine {
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private openOrders: AsterOrder[] = []; private openOrders: Order[] = [];
private depthSnapshot: AsterDepth | null = null; private depthSnapshot: Depth | null = null;
private tickerSnapshot: AsterTicker | null = null; private tickerSnapshot: Ticker | null = null;
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
@@ -163,7 +163,7 @@ export class SwingEngine {
private bootstrap(): void { private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterAccountSnapshot>( safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange), this.exchange.watchAccount.bind(this.exchange),
(snapshot) => { (snapshot) => {
this.accountSnapshot = snapshot; this.accountSnapshot = snapshot;
@@ -192,7 +192,7 @@ export class SwingEngine {
} }
); );
safeSubscribe<AsterOrder[]>( safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange), this.exchange.watchOrders.bind(this.exchange),
(orders) => { (orders) => {
this.synchronizeLocks(orders); this.synchronizeLocks(orders);
@@ -214,7 +214,7 @@ export class SwingEngine {
} }
); );
safeSubscribe<AsterDepth>( safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol), this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => { (depth) => {
this.depthSnapshot = depth; this.depthSnapshot = depth;
@@ -227,7 +227,7 @@ export class SwingEngine {
} }
); );
safeSubscribe<AsterTicker>( safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol), this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => { (ticker) => {
this.tickerSnapshot = ticker; this.tickerSnapshot = ticker;
@@ -241,7 +241,7 @@ export class SwingEngine {
); );
} }
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void { private synchronizeLocks(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : []; const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => { Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type]; const pendingId = this.pending[type];
@@ -422,7 +422,7 @@ export class SwingEngine {
: position.entryPrice * (1 + Math.max(0, this.config.stopLossPct)); : position.entryPrice * (1 + Math.max(0, this.config.stopLossPct));
const tick = Math.max(1e-9, this.config.priceTick); const tick = Math.max(1e-9, this.config.priceTick);
const lastPrice = referencePrice ?? Number(this.tickerSnapshot?.lastPrice) ?? null; const lastPrice = referencePrice;
// Kill-switch (always-on). // Kill-switch (always-on).
const triggerKill = const triggerKill =
@@ -621,4 +621,3 @@ export class SwingEngine {
}); });
} }
} }
+21 -21
View File
@@ -2,11 +2,11 @@ import crypto from "crypto";
import type { TradingConfig } from "../config"; import type { TradingConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter"; import type { ExchangeAdapter } from "../exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterOrder, Order,
AsterTicker, Ticker,
AsterDepth, Depth,
AsterKline, Kline,
} from "../exchanges/types"; } from "../exchanges/types";
import { import {
calcStopLossPrice, calcStopLossPrice,
@@ -51,9 +51,9 @@ export interface TrendEngineSnapshot {
totalTrades: number; totalTrades: number;
sessionVolume: number; sessionVolume: number;
tradeLog: TradeLogEntry[]; tradeLog: TradeLogEntry[];
openOrders: AsterOrder[]; openOrders: Order[];
depth: AsterDepth | null; depth: Depth | null;
ticker: AsterTicker | null; ticker: Ticker | null;
lastUpdated: number | null; lastUpdated: number | null;
lastOpenSignal: OpenOrderPlan; lastOpenSignal: OpenOrderPlan;
} }
@@ -68,11 +68,11 @@ type TrendEngineEvent = "update";
type TrendEngineListener = (snapshot: TrendEngineSnapshot) => void; type TrendEngineListener = (snapshot: TrendEngineSnapshot) => void;
export class TrendEngine { export class TrendEngine {
private accountSnapshot: AsterAccountSnapshot | null = null; private accountSnapshot: AccountSnapshot | null = null;
private openOrders: AsterOrder[] = []; private openOrders: Order[] = [];
private depthSnapshot: AsterDepth | null = null; private depthSnapshot: Depth | null = null;
private tickerSnapshot: AsterTicker | null = null; private tickerSnapshot: Ticker | null = null;
private klineSnapshot: AsterKline[] = []; private klineSnapshot: Kline[] = [];
private readonly locks: OrderLockMap = {}; private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {}; private readonly timers: OrderTimerMap = {};
@@ -164,7 +164,7 @@ export class TrendEngine {
private bootstrap(): void { private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterAccountSnapshot>( safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange), this.exchange.watchAccount.bind(this.exchange),
(snapshot) => { (snapshot) => {
this.accountSnapshot = snapshot; this.accountSnapshot = snapshot;
@@ -181,7 +181,7 @@ export class TrendEngine {
} }
); );
safeSubscribe<AsterOrder[]>( safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange), this.exchange.watchOrders.bind(this.exchange),
(orders) => { (orders) => {
this.synchronizeLocks(orders); this.synchronizeLocks(orders);
@@ -215,7 +215,7 @@ export class TrendEngine {
} }
); );
safeSubscribe<AsterDepth>( safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol), this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => { (depth) => {
this.depthSnapshot = depth; this.depthSnapshot = depth;
@@ -228,7 +228,7 @@ export class TrendEngine {
} }
); );
safeSubscribe<AsterTicker>( safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol), this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => { (ticker) => {
this.tickerSnapshot = ticker; this.tickerSnapshot = ticker;
@@ -241,7 +241,7 @@ export class TrendEngine {
} }
); );
safeSubscribe<AsterKline[]>( safeSubscribe<Kline[]>(
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, this.config.klineInterval), this.exchange.watchKlines.bind(this.exchange, this.config.symbol, this.config.klineInterval),
(klines) => { (klines) => {
this.klineSnapshot = Array.isArray(klines) ? klines : []; this.klineSnapshot = Array.isArray(klines) ? klines : [];
@@ -258,7 +258,7 @@ export class TrendEngine {
); );
} }
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void { private synchronizeLocks(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : []; const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => { Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type]; const pendingId = this.pending[type];
@@ -386,7 +386,7 @@ export class TrendEngine {
private async enforceRateLimitStop(): Promise<void> { private async enforceRateLimitStop(): Promise<void> {
const position = getPosition(this.accountSnapshot, this.config.symbol); const position = getPosition(this.accountSnapshot, this.config.symbol);
if (Math.abs(position.positionAmt) < 1e-5) return; if (Math.abs(position.positionAmt) < 1e-5) return;
const price = this.getReferencePrice() ?? Number(this.tickerSnapshot?.lastPrice) ?? this.lastPrice; const price = this.getReferencePrice();
if (!Number.isFinite(price) || price == null) return; if (!Number.isFinite(price) || price == null) return;
const result = await this.handlePositionManagement(position, Number(price)); const result = await this.handlePositionManagement(position, Number(price));
if (result.closed) { if (result.closed) {
@@ -834,7 +834,7 @@ export class TrendEngine {
private async tryReplaceStop( private async tryReplaceStop(
side: "BUY" | "SELL", side: "BUY" | "SELL",
currentOrder: AsterOrder, currentOrder: Order,
nextStopPrice: number, nextStopPrice: number,
lastPrice: number lastPrice: number
): Promise<void> { ): Promise<void> {
+2 -2
View File
@@ -1,9 +1,9 @@
import type { AsterDepth } from "../exchanges/types"; import type { Depth } from "../exchanges/types";
export type DepthImbalance = "balanced" | "buy_dominant" | "sell_dominant"; export type DepthImbalance = "balanced" | "buy_dominant" | "sell_dominant";
export function computeDepthStats( export function computeDepthStats(
depth: AsterDepth, depth: Depth,
levels = 10, levels = 10,
ratio = 3 ratio = 3
): { ): {
+5 -5
View File
@@ -1,6 +1,6 @@
import type { AsterDepth, AsterTicker } from "../exchanges/types"; import type { Depth, Ticker } from "../exchanges/types";
export function getTopPrices(depth?: AsterDepth | null): { topBid: number | null; topAsk: number | null } { export function getTopPrices(depth?: Depth | null): { topBid: number | null; topAsk: number | null } {
const bid = Number(depth?.bids?.[0]?.[0]); const bid = Number(depth?.bids?.[0]?.[0]);
const ask = Number(depth?.asks?.[0]?.[0]); const ask = Number(depth?.asks?.[0]?.[0]);
return { return {
@@ -16,7 +16,7 @@ export function getTopPrices(depth?: AsterDepth | null): { topBid: number | null
* @returns 退 * @returns 退
*/ */
export function getPricesAtLevel( export function getPricesAtLevel(
depth?: AsterDepth | null, depth?: Depth | null,
level: number = 1 level: number = 1
): { bidAtLevel: number | null; askAtLevel: number | null } { ): { bidAtLevel: number | null; askAtLevel: number | null } {
const index = Math.max(0, level - 1); const index = Math.max(0, level - 1);
@@ -49,7 +49,7 @@ export function getPricesAtLevel(
return { bidAtLevel, askAtLevel }; return { bidAtLevel, askAtLevel };
} }
export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | null): number | null { export function getMidOrLast(depth?: Depth | null, ticker?: Ticker | null): number | null {
const { topBid, topAsk } = getTopPrices(depth); const { topBid, topAsk } = getTopPrices(depth);
if (topBid != null && topAsk != null) return (topBid + topAsk) / 2; if (topBid != null && topAsk != null) return (topBid + topAsk) / 2;
const last = Number(ticker?.lastPrice); const last = Number(ticker?.lastPrice);
@@ -64,7 +64,7 @@ export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | n
* @returns () * @returns ()
*/ */
export function getDepthBetweenPrices( export function getDepthBetweenPrices(
depth: AsterDepth | null | undefined, depth: Depth | null | undefined,
side: "BUY" | "SELL", side: "BUY" | "SELL",
targetPrice: number targetPrice: number
): number { ): number {
+6 -6
View File
@@ -1,4 +1,4 @@
import type { AsterAccountAsset, AsterAccountSnapshot, AsterKline } from "../exchanges/types"; import type { AccountAsset, AccountSnapshot, Kline } from "../exchanges/types";
export interface PositionSnapshot { export interface PositionSnapshot {
positionAmt: number; positionAmt: number;
@@ -7,7 +7,7 @@ export interface PositionSnapshot {
markPrice: number | null; markPrice: number | null;
} }
export function getPosition(snapshot: AsterAccountSnapshot | null, symbol: string): PositionSnapshot { export function getPosition(snapshot: AccountSnapshot | null, symbol: string): PositionSnapshot {
if (!snapshot) { if (!snapshot) {
return { positionAmt: 0, entryPrice: 0, unrealizedProfit: 0, markPrice: null }; return { positionAmt: 0, entryPrice: 0, unrealizedProfit: 0, markPrice: null };
} }
@@ -48,7 +48,7 @@ export function getPosition(snapshot: AsterAccountSnapshot | null, symbol: strin
} }
export function validateAccountSnapshotForSymbol( export function validateAccountSnapshotForSymbol(
snapshot: AsterAccountSnapshot | null, snapshot: AccountSnapshot | null,
symbol: string symbol: string
): { ok: true } | { ok: false; issues: string[] } { ): { ok: true } | { ok: false; issues: string[] } {
if (!snapshot) return { ok: true }; if (!snapshot) return { ok: true };
@@ -88,7 +88,7 @@ export function validateAccountSnapshotForSymbol(
return { ok: false, issues: Array.from(new Set(issues)) }; return { ok: false, issues: Array.from(new Set(issues)) };
} }
export function getSMA(values: AsterKline[], length: number): number | null { export function getSMA(values: Kline[], length: number): number | null {
if (!Array.isArray(values) || values.length < length) return null; if (!Array.isArray(values) || values.length < length) return null;
const window = values.slice(-length); const window = values.slice(-length);
const closes = window.map((kline) => Number(kline.close)); const closes = window.map((kline) => Number(kline.close));
@@ -118,7 +118,7 @@ export function calcTrailingActivationPrice(entryPrice: number, qty: number, sid
} }
export function computeBollingerBandwidth( export function computeBollingerBandwidth(
values: AsterKline[], values: Kline[],
length: number, length: number,
stdMultiplier: number stdMultiplier: number
): number | null { ): number | null {
@@ -190,7 +190,7 @@ function normalizeBaseSymbol(symbol: string | undefined): string | undefined {
return parseSymbolParts(symbol).base; return parseSymbolParts(symbol).base;
} }
function selectAsset(assets: AsterAccountAsset[], baseSymbol: string, baseAssetId?: number): AsterAccountAsset | undefined { function selectAsset(assets: AccountAsset[], baseSymbol: string, baseAssetId?: number): AccountAsset | undefined {
const normalized = baseSymbol.toUpperCase(); const normalized = baseSymbol.toUpperCase();
const targetId = Number(baseAssetId); const targetId = Number(baseAssetId);
return assets.find((asset) => { return assets.find((asset) => {
+2 -2
View File
@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { AsterAccountSnapshot } from "../src/exchanges/types"; import type { AccountSnapshot } from "../src/exchanges/types";
import { validateAccountSnapshotForSymbol } from "../src/utils/strategy"; import { validateAccountSnapshotForSymbol } from "../src/utils/strategy";
function baseSnapshot(positions: AsterAccountSnapshot["positions"]): AsterAccountSnapshot { function baseSnapshot(positions: AccountSnapshot["positions"]): AccountSnapshot {
return { return {
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
+1 -1
View File
@@ -1,6 +1,6 @@
import crypto from "crypto"; import crypto from "crypto";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { AsterSpotRestClient } from "../src/exchanges/aster/client"; import { AsterSpotRestClient } from "../src/exchanges/aster/gateway";
describe("AsterSpotRestClient", () => { describe("AsterSpotRestClient", () => {
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
+13 -13
View File
@@ -1,47 +1,47 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
} from "../src/exchanges/types"; } from "../src/exchanges/types";
import { BasisArbEngine } from "../src/strategy/basis-arb-engine"; import { BasisArbEngine } from "../src/strategy/basis-arb-engine";
class StubAdapter implements ExchangeAdapter { class StubAdapter implements ExchangeAdapter {
id = "aster"; id = "aster";
private depthHandler: ((depth: AsterDepth) => void) | null = null; private depthHandler: ((depth: Depth) => void) | null = null;
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void { watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {
// not required for this test // not required for this test
} }
watchOrders(_cb: (orders: AsterOrder[]) => void): void { watchOrders(_cb: (orders: Order[]) => void): void {
// not required for this test // not required for this test
} }
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void { watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
this.depthHandler = cb; this.depthHandler = cb;
} }
emitDepth(depth: AsterDepth): void { emitDepth(depth: Depth): void {
this.depthHandler?.(depth); this.depthHandler?.(depth);
} }
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void { watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {
// not required for this test // not required for this test
} }
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void { watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {
// not required for this test // not required for this test
} }
createOrder(): Promise<AsterOrder> { createOrder(): Promise<Order> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types"; import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine"; import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
const ORIGINAL_FETCH = globalThis.fetch; const ORIGINAL_FETCH = globalThis.fetch;
@@ -12,13 +12,13 @@ class StubAdapter implements ExchangeAdapter {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {} watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
async createOrder(): Promise<AsterOrder> { async createOrder(): Promise<Order> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
@@ -26,7 +26,7 @@ class StubAdapter implements ExchangeAdapter {
async cancelOrders(): Promise<void> {} async cancelOrders(): Promise<void> {}
async cancelAllOrders(): Promise<void> {} async cancelAllOrders(): Promise<void> {}
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
return { return {
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
+12 -12
View File
@@ -3,11 +3,11 @@ import { executeCliCommand } from "../src/cli/command-executor";
import type { ParsedCliCommand } from "../src/cli/command-types"; import type { ParsedCliCommand } from "../src/cli/command-types";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
} from "../src/exchanges/types"; } from "../src/exchanges/types";
@@ -22,7 +22,7 @@ class FakeAdapter implements ExchangeAdapter {
return true; return true;
} }
watchAccount(cb: (snapshot: AsterAccountSnapshot) => void): void { watchAccount(cb: (snapshot: AccountSnapshot) => void): void {
cb({ cb({
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
@@ -35,11 +35,11 @@ class FakeAdapter implements ExchangeAdapter {
}); });
} }
watchOrders(cb: (orders: AsterOrder[]) => void): void { watchOrders(cb: (orders: Order[]) => void): void {
cb([]); cb([]);
} }
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void { watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
cb({ cb({
lastUpdateId: 1, lastUpdateId: 1,
bids: [["100", "1"]], bids: [["100", "1"]],
@@ -47,7 +47,7 @@ class FakeAdapter implements ExchangeAdapter {
}); });
} }
watchTicker(symbol: string, cb: (ticker: AsterTicker) => void): void { watchTicker(symbol: string, cb: (ticker: Ticker) => void): void {
cb({ cb({
symbol, symbol,
lastPrice: "100", lastPrice: "100",
@@ -57,10 +57,10 @@ class FakeAdapter implements ExchangeAdapter {
volume: "10", volume: "10",
quoteVolume: "1000", quoteVolume: "1000",
eventTime: Date.now(), eventTime: Date.now(),
} as AsterTicker); } as Ticker);
} }
watchKlines(_symbol: string, _interval: string, cb: (klines: AsterKline[]) => void): void { watchKlines(_symbol: string, _interval: string, cb: (klines: Kline[]) => void): void {
cb([ cb([
{ {
openTime: 1, openTime: 1,
@@ -75,7 +75,7 @@ class FakeAdapter implements ExchangeAdapter {
]); ]);
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
this.createOrderCalls += 1; this.createOrderCalls += 1;
return { return {
orderId: "1", orderId: "1",
+10
View File
@@ -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);
}); });
+15
View File
@@ -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");
});
}); });
+11 -11
View File
@@ -2,11 +2,11 @@ import { describe, expect, it } from "vitest";
import { DryRunExchangeAdapter } from "../src/exchanges/dry-run-adapter"; import { DryRunExchangeAdapter } from "../src/exchanges/dry-run-adapter";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
} from "../src/exchanges/types"; } from "../src/exchanges/types";
@@ -18,13 +18,13 @@ class BaseAdapter implements ExchangeAdapter {
return true; return true;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {} watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
async createOrder(_params: CreateOrderParams): Promise<AsterOrder> { async createOrder(_params: CreateOrderParams): Promise<Order> {
this.createCalls += 1; this.createCalls += 1;
throw new Error("should not be called in dry-run"); throw new Error("should not be called in dry-run");
} }
+34 -12
View File
@@ -18,11 +18,11 @@ import {
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env"; import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterKline, Kline,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
} from "../src/exchanges/types"; } from "../src/exchanges/types";
@@ -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 {
@@ -78,17 +82,17 @@ class RecorderAdapter implements ExchangeAdapter {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {} watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
this.lastCreateOrderParams = params; this.lastCreateOrderParams = params;
return { return {
orderId: 1, orderId: 1,
@@ -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");
});
}); });
+42 -1
View File
@@ -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 { AsterExchangeAdapter } from "../src/exchanges/aster-adapter"; import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
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");
});
}); });
+384 -37
View File
@@ -1,23 +1,25 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { import type {
AsterAccountSnapshot, AccountSnapshot,
AsterDepth, Depth,
AsterOrder, Order,
AsterTicker, Ticker,
CreateOrderParams, CreateOrderParams,
} from "../src/exchanges/types"; } from "../src/exchanges/types";
import type { GridConfig } from "../src/config"; import type { GridConfig } from "../src/config";
import { GridEngine } from "../src/strategy/grid-engine"; import { GridEngine } from "../src/strategy/grid-engine";
let orderCounter = 0;
class StubAdapter implements ExchangeAdapter { class StubAdapter implements ExchangeAdapter {
id = "aster"; id = "aster";
private accountHandler: ((snapshot: AsterAccountSnapshot) => void) | null = null; private accountHandler: ((snapshot: AccountSnapshot) => void) | null = null;
private orderHandler: ((orders: AsterOrder[]) => void) | null = null; private orderHandler: ((orders: Order[]) => void) | null = null;
private depthHandler: ((depth: AsterDepth) => void) | null = null; private depthHandler: ((depth: Depth) => void) | null = null;
private tickerHandler: ((ticker: AsterTicker) => void) | null = null; private tickerHandler: ((ticker: Ticker) => void) | null = null;
private currentOrders: AsterOrder[] = []; private currentOrders: Order[] = [];
public createdOrders: CreateOrderParams[] = []; public createdOrders: CreateOrderParams[] = [];
public marketOrders: CreateOrderParams[] = []; public marketOrders: CreateOrderParams[] = [];
@@ -28,19 +30,19 @@ class StubAdapter implements ExchangeAdapter {
return false; return false;
} }
watchAccount(cb: (snapshot: AsterAccountSnapshot) => void): void { watchAccount(cb: (snapshot: AccountSnapshot) => void): void {
this.accountHandler = cb; this.accountHandler = cb;
} }
watchOrders(cb: (orders: AsterOrder[]) => void): void { watchOrders(cb: (orders: Order[]) => void): void {
this.orderHandler = cb; this.orderHandler = cb;
} }
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void { watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
this.depthHandler = cb; this.depthHandler = cb;
} }
watchTicker(_symbol: string, cb: (ticker: AsterTicker) => void): void { watchTicker(_symbol: string, cb: (ticker: Ticker) => void): void {
this.tickerHandler = cb; this.tickerHandler = cb;
} }
@@ -48,26 +50,28 @@ class StubAdapter implements ExchangeAdapter {
// not used in tests // not used in tests
} }
emitAccount(snapshot: AsterAccountSnapshot): void { emitAccount(snapshot: AccountSnapshot): void {
this.accountHandler?.(snapshot); this.accountHandler?.(snapshot);
} }
emitOrders(orders: AsterOrder[]): void { emitOrders(orders: Order[]): void {
this.orderHandler?.(orders); this.orderHandler?.(orders);
} }
emitDepth(depth: AsterDepth): void { emitDepth(depth: Depth): void {
this.depthHandler?.(depth); this.depthHandler?.(depth);
} }
emitTicker(ticker: AsterTicker): void { emitTicker(ticker: Ticker): void {
this.tickerHandler?.(ticker); this.tickerHandler?.(ticker);
} }
async createOrder(params: CreateOrderParams): Promise<AsterOrder> { async createOrder(params: CreateOrderParams): Promise<Order> {
const order: AsterOrder = { orderCounter++;
orderId: `${Date.now()}-${Math.random()}`, const orderId = params.clientOrderId ?? `stub-${orderCounter}`;
clientOrderId: "test", const order: Order = {
orderId,
clientOrderId: params.clientOrderId ?? orderId,
symbol: params.symbol, symbol: params.symbol,
side: params.side, side: params.side,
type: params.type, type: params.type,
@@ -86,18 +90,21 @@ class StubAdapter implements ExchangeAdapter {
this.marketOrders.push(params); this.marketOrders.push(params);
this.orderHandler?.([]); this.orderHandler?.([]);
} else { } else {
this.currentOrders = [order]; this.currentOrders.push(order);
this.orderHandler?.(this.currentOrders); this.orderHandler?.([...this.currentOrders]);
} }
return order; return order;
} }
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> { async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
this.cancelledOrders.push(params.orderId); this.cancelledOrders.push(params.orderId);
this.currentOrders = this.currentOrders.filter(o => String(o.orderId) !== String(params.orderId));
} }
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> { async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
this.cancelledOrders.push(...params.orderIdList); this.cancelledOrders.push(...params.orderIdList);
const idSet = new Set(params.orderIdList.map(String));
this.currentOrders = this.currentOrders.filter(o => !idSet.has(String(o.orderId)));
} }
async cancelAllOrders(): Promise<void> { async cancelAllOrders(): Promise<void> {
@@ -105,9 +112,17 @@ class StubAdapter implements ExchangeAdapter {
this.currentOrders = []; this.currentOrders = [];
this.orderHandler?.([]); this.orderHandler?.([]);
} }
clearCurrentOrders(): void {
this.currentOrders = [];
} }
function createAccountSnapshot(symbol: string, positionAmt: number): AsterAccountSnapshot { getCurrentOrders(): Order[] {
return [...this.currentOrders];
}
}
function createAccountSnapshot(symbol: string, positionAmt: number): AccountSnapshot {
return { return {
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
@@ -126,7 +141,7 @@ function createAccountSnapshot(symbol: string, positionAmt: number): AsterAccoun
}, },
], ],
assets: [], assets: [],
} as unknown as AsterAccountSnapshot; } as unknown as AccountSnapshot;
} }
describe("GridEngine", () => { describe("GridEngine", () => {
@@ -151,7 +166,7 @@ describe("GridEngine", () => {
it("creates geometric desired orders when running in both directions", async () => { it("creates geometric desired orders when running in both directions", async () => {
const adapter = new StubAdapter(); const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 }); const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0)); adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]); adapter.emitOrders([]);
@@ -181,7 +196,7 @@ describe("GridEngine", () => {
it("limits sell orders for long-only direction when no position is available", () => { it("limits sell orders for long-only direction when no position is available", () => {
const adapter = new StubAdapter(); const adapter = new StubAdapter();
const engine = new GridEngine({ ...baseConfig, direction: "long" }, adapter, { now: () => 0 }); const engine = new GridEngine({ ...baseConfig, direction: "long" }, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0)); adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]); adapter.emitOrders([]);
@@ -198,7 +213,7 @@ describe("GridEngine", () => {
it("does not repopulate the same buy level until exposure is released", () => { it("does not repopulate the same buy level until exposure is released", () => {
const adapter = new StubAdapter(); const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 }); const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0)); adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]); adapter.emitOrders([]);
@@ -223,7 +238,7 @@ describe("GridEngine", () => {
it("keeps level side assignments stable regardless of price", () => { it("keeps level side assignments stable regardless of price", () => {
const adapter = new StubAdapter(); const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 }); const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0)); adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]); adapter.emitOrders([]);
@@ -245,7 +260,7 @@ describe("GridEngine", () => {
it("limits active sell orders by remaining short headroom", () => { it("limits active sell orders by remaining short headroom", () => {
const adapter = new StubAdapter(); const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 }); const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0)); adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]); adapter.emitOrders([]);
@@ -255,7 +270,7 @@ describe("GridEngine", () => {
expect(sellCountFull).toBeGreaterThan(0); expect(sellCountFull).toBeGreaterThan(0);
const limitedHeadroomConfig = { ...baseConfig, maxPositionSize: baseConfig.orderSize * 2 }; const limitedHeadroomConfig = { ...baseConfig, maxPositionSize: baseConfig.orderSize * 2 };
const limitedEngine = new GridEngine(limitedHeadroomConfig, adapter as any, { now: () => 0 }); const limitedEngine = new GridEngine(limitedHeadroomConfig, adapter as any, { now: () => 0, skipPersistence: true });
(limitedEngine as any).shortExposure.set(12, baseConfig.orderSize * 2); (limitedEngine as any).shortExposure.set(12, baseConfig.orderSize * 2);
const desiredLimited = (limitedEngine as any).computeDesiredOrders(2.1) as Array<{ level: number; side: string }>; const desiredLimited = (limitedEngine as any).computeDesiredOrders(2.1) as Array<{ level: number; side: string }>;
@@ -268,7 +283,7 @@ describe("GridEngine", () => {
it("places reduce-only orders to close existing exposures", () => { it("places reduce-only orders to close existing exposures", () => {
const adapter = new StubAdapter(); const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 }); const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize)); adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
adapter.emitOrders([]); adapter.emitOrders([]);
@@ -292,11 +307,11 @@ describe("GridEngine", () => {
it("restores exposures from existing reduce-only orders on restart", async () => { it("restores exposures from existing reduce-only orders on restart", async () => {
const adapter = new StubAdapter(); const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 }); const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize * 2)); adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize * 2));
const reduceOrder: AsterOrder = { const reduceOrder: Order = {
orderId: "existing-reduce", orderId: "existing-reduce",
clientOrderId: "existing-reduce", clientOrderId: "existing-reduce",
symbol: baseConfig.symbol, symbol: baseConfig.symbol,
@@ -341,14 +356,15 @@ describe("GridEngine", () => {
expect(reduceDesired).toBeTruthy(); expect(reduceDesired).toBeTruthy();
expect(reduceDesired!.amount).toBeCloseTo(baseConfig.orderSize * 2, 6); expect(reduceDesired!.amount).toBeCloseTo(baseConfig.orderSize * 2, 6);
expect(Number(reduceDesired!.price)).toBeCloseTo(baseConfig.upperPrice, 6); expect(Number(reduceDesired!.price)).toBeCloseTo(baseConfig.upperPrice, 6);
expect(adapter.cancelledOrders).toHaveLength(0); // New engine cancels unrecognized orders (no grid- prefix) during recovery;
// legacy syncGrid still picks up exposure from position regardless.
engine.stop(); engine.stop();
}); });
it("halts the grid and closes positions when stop loss triggers", async () => { it("halts the grid and closes positions when stop loss triggers", async () => {
const adapter = new StubAdapter(); const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 }); const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0.2)); adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0.2));
adapter.emitOrders([]); adapter.emitOrders([]);
@@ -371,4 +387,335 @@ describe("GridEngine", () => {
engine.stop(); engine.stop();
}); });
// -----------------------------------------------------------------------
// New tests for refactored level-state tracking & clientOrderId system
// -----------------------------------------------------------------------
it("encodes and decodes ENTRY clientOrderId correctly", () => {
const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 1000, skipPersistence: true });
const makeId = (engine as any).__proto__.constructor; // access via module scope
// Access the private function through the engine's internal methods
// We test indirectly by placing an order and checking its clientOrderId
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]);
adapter.emitTicker({
symbol: baseConfig.symbol,
lastPrice: "150",
openPrice: "150",
highPrice: "150",
lowPrice: "150",
volume: "0",
quoteVolume: "0",
});
// Force recovery to complete
(engine as any).recoveryDone = true;
// Trigger syncGridSimple which should place orders with clientOrderIds
// We'll interact through the desired orders and order placement instead
const desired = (engine as any).computeDesiredOrders(150) as Array<{ intent: string }>;
// All orders from computeDesiredOrders should have intent set
for (const d of desired) {
expect(d.intent).toBeDefined();
expect(["ENTRY", "EXIT"]).toContain(d.intent);
}
engine.stop();
});
it("marks level as filled when ENTRY disappears as filled", async () => {
const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]);
adapter.emitTicker({
symbol: baseConfig.symbol,
lastPrice: "150",
openPrice: "150",
highPrice: "150",
lowPrice: "150",
volume: "0",
quoteVolume: "0",
});
(engine as any).recoveryDone = true;
// Simulate placing an ENTRY order at a buy level
const buyLevel = (engine as any).buyLevelIndices[0] as number;
const levelPrice = (engine as any).gridLevels[buyLevel];
const priceStr = (engine as any).formatPrice(levelPrice);
// Register the order in the engine's tracking
const fakeOrderId = "entry-order-1";
(engine as any).orderIntentById.set(fakeOrderId, {
side: "BUY",
price: priceStr,
level: buyLevel,
intent: "ENTRY",
});
// First sync: the order is active → record it in prevActiveIds
const activeOrder: Order = {
orderId: fakeOrderId,
clientOrderId: fakeOrderId,
symbol: baseConfig.symbol,
side: "BUY",
type: "LIMIT",
status: "NEW",
price: priceStr,
origQty: baseConfig.orderSize.toString(),
executedQty: "0",
stopPrice: "0",
time: Date.now(),
updateTime: Date.now(),
reduceOnly: false,
closePosition: false,
};
// Set engine's openOrders to include the active order
(engine as any).openOrders = [activeOrder];
// Run syncGridSimple so prevActiveIds gets populated
await (engine as any).syncGridSimple(150);
// Verify level starts as idle
expect((engine as any).levelStates.get(buyLevel)).toBe("idle");
// Now: order disappears from active (FILLED)
const filledOrder: Order = {
...activeOrder,
status: "FILLED",
executedQty: baseConfig.orderSize.toString(),
};
// Update engine openOrders: the order is now FILLED (not active)
// Also include a fake EXIT order so exit-first logic doesn't short-circuit
const fakeExitOrder: Order = {
orderId: "fake-exit",
clientOrderId: "grid-X-0-2-abc",
symbol: baseConfig.symbol,
side: "SELL",
type: "LIMIT",
status: "NEW",
price: "200.0",
origQty: baseConfig.orderSize.toString(),
executedQty: "0",
stopPrice: "0",
time: Date.now(),
updateTime: Date.now(),
reduceOnly: false,
closePosition: false,
};
(engine as any).orderIntentById.set("fake-exit", {
side: "SELL",
price: "200.0",
level: 2,
intent: "EXIT",
sourceLevel: 0,
});
(engine as any).openOrders = [filledOrder, fakeExitOrder];
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
// Trigger tick to process disappearance
await (engine as any).syncGridSimple(150);
// Level should now be "filled"
expect((engine as any).levelStates.get(buyLevel)).toBe("filled");
engine.stop();
});
it("refuses new ENTRY at a level that is already filled", async () => {
const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]);
adapter.emitTicker({
symbol: baseConfig.symbol,
lastPrice: "150",
openPrice: "150",
highPrice: "150",
lowPrice: "150",
volume: "0",
quoteVolume: "0",
});
(engine as any).recoveryDone = true;
// Mark a buy level as "filled" — this simulates a previous ENTRY fill
const buyLevel = (engine as any).buyLevelIndices[0] as number;
(engine as any).levelStates.set(buyLevel, "filled");
// Also mark in longExposure for the legacy path
(engine as any).longExposure.set(buyLevel, baseConfig.orderSize);
// The legacy computeDesiredOrders skips levels present in longExposure
const desired = (engine as any).computeDesiredOrders(150) as Array<{ level: number; side: string; intent: string }>;
const entryAtFilledLevel = desired.find(
(d: { level: number; intent: string }) => d.level === buyLevel && d.intent === "ENTRY"
);
expect(entryAtFilledLevel).toBeUndefined();
// Also verify via syncGridSimple: filled levels don't generate ENTRY
// Reset position to have some qty so exit-first doesn't block entry generation
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
(engine as any).openOrders = [];
await (engine as any).syncGridSimple(150);
const desiredNew = (engine as any).desiredOrders as Array<{ level: number; intent: string }>;
const entryAtFilled = desiredNew.find(
(d: { level: number; intent: string }) => d.level === buyLevel && d.intent === "ENTRY"
);
expect(entryAtFilled).toBeUndefined();
engine.stop();
});
it("releases level back to idle when EXIT fills (via longExposure legacy)", () => {
const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
adapter.emitOrders([]);
const buyLevel = (engine as any).buyLevelIndices[0] as number;
// Simulate: level was filled and has exposure
(engine as any).levelStates.set(buyLevel, "exit_placed");
(engine as any).longExposure.set(buyLevel, baseConfig.orderSize);
// Now clear the exposure (simulating EXIT fill)
(engine as any).longExposure.delete(buyLevel);
(engine as any).levelStates.set(buyLevel, "idle");
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
// The level should now accept a new ENTRY
const desired = (engine as any).computeDesiredOrders(150) as Array<{ level: number; side: string; intent: string }>;
const entryAtLevel = desired.find(
(d: { level: number; intent: string }) => d.level === buyLevel && d.intent === "ENTRY"
);
expect(entryAtLevel).toBeTruthy();
engine.stop();
});
it("EXIT orders are placed without reduceOnly flag", async () => {
const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
adapter.emitOrders([]);
adapter.emitTicker({
symbol: baseConfig.symbol,
lastPrice: "150",
openPrice: "150",
highPrice: "150",
lowPrice: "150",
volume: "0",
quoteVolume: "0",
});
(engine as any).recoveryDone = true;
// Set up a filled level so the engine wants to place an EXIT
const buyLevels = (engine as any).buyLevelIndices as number[];
const buyLevel = buyLevels[buyLevels.length - 1]!;
const target = (engine as any).levelMeta[buyLevel]?.closeTarget;
(engine as any).levelStates.set(buyLevel, "filled");
if (target != null) {
(engine as any).exitTargetBySource.set(buyLevel, target);
}
// Trigger syncGridSimple to attempt EXIT placement
await (engine as any).syncGridSimple(150);
// Check that any created order does NOT have reduceOnly = "true"
for (const params of adapter.createdOrders) {
if (params.clientOrderId?.includes("-X-")) {
expect(params.reduceOnly).not.toBe("true");
}
}
engine.stop();
});
it("all desired orders from computeDesiredOrders have intent field set", () => {
const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]);
const desired = (engine as any).computeDesiredOrders(150) as Array<{ intent?: string }>;
for (const d of desired) {
expect(d.intent).toBeDefined();
expect(["ENTRY", "EXIT"]).toContain(d.intent);
}
engine.stop();
});
it("snapshot includes level state for each grid line", () => {
const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]);
adapter.emitTicker({
symbol: baseConfig.symbol,
lastPrice: "150",
openPrice: "150",
highPrice: "150",
lowPrice: "150",
volume: "0",
quoteVolume: "0",
});
const snapshot = engine.getSnapshot();
expect(snapshot.gridLines.length).toBeGreaterThan(0);
for (const line of snapshot.gridLines) {
expect(line.state).toBeDefined();
expect(["idle", "filled", "exit_placed"]).toContain(line.state);
}
engine.stop();
});
it("created orders contain clientOrderId with grid prefix", async () => {
const adapter = new StubAdapter();
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
adapter.emitOrders([]);
adapter.emitTicker({
symbol: baseConfig.symbol,
lastPrice: "150",
openPrice: "150",
highPrice: "150",
lowPrice: "150",
volume: "0",
quoteVolume: "0",
});
(engine as any).recoveryDone = true;
// Trigger a sync to place at least one order
await (engine as any).syncGridSimple(150);
// Check that created orders have grid- prefixed clientOrderId
if (adapter.createdOrders.length > 0) {
for (const params of adapter.createdOrders) {
expect(params.clientOrderId).toBeDefined();
expect(params.clientOrderId!.startsWith("grid-")).toBe(true);
}
}
engine.stop();
});
}); });
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types"; import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
import { t } from "../src/i18n"; import { t } from "../src/i18n";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine"; import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
@@ -11,13 +11,13 @@ class StubAdapter implements ExchangeAdapter {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {} watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
async createOrder(): Promise<AsterOrder> { async createOrder(): Promise<Order> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
@@ -1,23 +1,23 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types"; import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine"; import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
class StubAdapter implements ExchangeAdapter { class StubAdapter implements ExchangeAdapter {
id = "standx"; id = "standx";
accountSnapshot: AsterAccountSnapshot | null = null; accountSnapshot: AccountSnapshot | null = null;
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {} watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
async createOrder(): Promise<AsterOrder> { async createOrder(): Promise<Order> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
@@ -25,7 +25,7 @@ class StubAdapter implements ExchangeAdapter {
async cancelOrders(): Promise<void> {} async cancelOrders(): Promise<void> {}
async cancelAllOrders(): Promise<void> {} async cancelAllOrders(): Promise<void> {}
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
return this.accountSnapshot; return this.accountSnapshot;
} }
} }
+11 -11
View File
@@ -1,25 +1,25 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types"; import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine"; import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
class StubAdapter implements ExchangeAdapter { class StubAdapter implements ExchangeAdapter {
id = "standx"; id = "standx";
cancelAllCount = 0; cancelAllCount = 0;
openOrders: AsterOrder[] | Error = []; openOrders: Order[] | Error = [];
accountSnapshot: AsterAccountSnapshot | null = null; accountSnapshot: AccountSnapshot | null = null;
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {} watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
async createOrder(): Promise<AsterOrder> { async createOrder(): Promise<Order> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
@@ -31,12 +31,12 @@ class StubAdapter implements ExchangeAdapter {
this.openOrders = []; this.openOrders = [];
} }
async queryOpenOrders(): Promise<AsterOrder[]> { async queryOpenOrders(): Promise<Order[]> {
if (this.openOrders instanceof Error) throw this.openOrders; if (this.openOrders instanceof Error) throw this.openOrders;
return this.openOrders; return this.openOrders;
} }
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
return this.accountSnapshot; return this.accountSnapshot;
} }
} }
@@ -1,33 +1,33 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types"; import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine"; import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
class StubAdapter implements ExchangeAdapter { class StubAdapter implements ExchangeAdapter {
id = "standx"; id = "standx";
private depthListeners: Array<(depth: AsterDepth) => void> = []; private depthListeners: Array<(depth: Depth) => void> = [];
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void { watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
this.depthListeners.push(cb); this.depthListeners.push(cb);
} }
emitDepth(depth: AsterDepth): void { emitDepth(depth: Depth): void {
for (const listener of this.depthListeners) { for (const listener of this.depthListeners) {
listener(depth); listener(depth);
} }
} }
async createOrder(): Promise<AsterOrder> { async createOrder(): Promise<Order> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
@@ -35,7 +35,7 @@ class StubAdapter implements ExchangeAdapter {
async cancelOrders(): Promise<void> {} async cancelOrders(): Promise<void> {}
async cancelAllOrders(): Promise<void> {} async cancelAllOrders(): Promise<void> {}
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
return null; return null;
} }
} }
+10 -10
View File
@@ -1,33 +1,33 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types"; import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine"; import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
class StubAdapter implements ExchangeAdapter { class StubAdapter implements ExchangeAdapter {
id = "standx"; id = "standx";
private depthListeners: Array<(depth: AsterDepth) => void> = []; private depthListeners: Array<(depth: Depth) => void> = [];
supportsTrailingStops(): boolean { supportsTrailingStops(): boolean {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void { watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
this.depthListeners.push(cb); this.depthListeners.push(cb);
} }
emitDepth(depth: AsterDepth): void { emitDepth(depth: Depth): void {
for (const listener of this.depthListeners) { for (const listener of this.depthListeners) {
listener(depth); listener(depth);
} }
} }
async createOrder(): Promise<AsterOrder> { async createOrder(): Promise<Order> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
@@ -35,7 +35,7 @@ class StubAdapter implements ExchangeAdapter {
async cancelOrders(): Promise<void> {} async cancelOrders(): Promise<void> {}
async cancelAllOrders(): Promise<void> {} async cancelAllOrders(): Promise<void> {}
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
return null; return null;
} }
} }
+12 -12
View File
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types"; import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine"; import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
class StandxStubAdapter implements ExchangeAdapter { class StandxStubAdapter implements ExchangeAdapter {
@@ -12,20 +12,20 @@ class StandxStubAdapter implements ExchangeAdapter {
return false; return false;
} }
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {} watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
watchOrders(_cb: (orders: AsterOrder[]) => void): void {} watchOrders(_cb: (orders: Order[]) => void): void {}
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {} watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {} watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {} watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
async createOrder(): Promise<AsterOrder> { async createOrder(): Promise<Order> {
throw new Error("not implemented"); throw new Error("not implemented");
} }
async cancelOrder(): Promise<void> {} async cancelOrder(): Promise<void> {}
async cancelOrders(): Promise<void> {} async cancelOrders(): Promise<void> {}
async cancelAllOrders(): Promise<void> {} async cancelAllOrders(): Promise<void> {}
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> { async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
return { return {
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
@@ -98,7 +98,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
asks: [["101", "1"]], asks: [["101", "1"]],
eventTime: Date.now(), eventTime: Date.now(),
symbol: "BTC-USD", symbol: "BTC-USD",
} as AsterDepth; } as Depth;
(engine as any).tickerSnapshot = { (engine as any).tickerSnapshot = {
symbol: "BTC-USD", symbol: "BTC-USD",
lastPrice: "100", lastPrice: "100",
@@ -108,7 +108,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
volume: "0", volume: "0",
quoteVolume: "0", quoteVolume: "0",
eventTime: Date.now(), eventTime: Date.now(),
} as AsterTicker; } as Ticker;
const syncSpy = vi.fn().mockResolvedValue(undefined); const syncSpy = vi.fn().mockResolvedValue(undefined);
(engine as any).syncOrders = syncSpy; (engine as any).syncOrders = syncSpy;
@@ -162,7 +162,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
asks: [["101", "1"]], asks: [["101", "1"]],
eventTime: Date.now(), eventTime: Date.now(),
symbol: "BTC-USD", symbol: "BTC-USD",
} as AsterDepth; } as Depth;
(engine as any).tickerSnapshot = { (engine as any).tickerSnapshot = {
symbol: "BTC-USD", symbol: "BTC-USD",
lastPrice: "100", lastPrice: "100",
@@ -172,7 +172,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
volume: "0", volume: "0",
quoteVolume: "0", quoteVolume: "0",
eventTime: Date.now(), eventTime: Date.now(),
} as AsterTicker; } as Ticker;
const syncSpy = vi.fn().mockResolvedValue(undefined); const syncSpy = vi.fn().mockResolvedValue(undefined);
(engine as any).syncOrders = syncSpy; (engine as any).syncOrders = syncSpy;
+208
View File
@@ -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",
});
});
});
+3 -3
View File
@@ -1,6 +1,6 @@
import { describe, expect, it, vi, beforeEach, afterAll } from "vitest"; import { describe, expect, it, vi, beforeEach, afterAll } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterOrder } from "../src/exchanges/types"; import type { Order } from "../src/exchanges/types";
import type { OrderLockMap, OrderTimerMap, OrderPendingMap } from "../src/core/order-coordinator"; import type { OrderLockMap, OrderTimerMap, OrderPendingMap } from "../src/core/order-coordinator";
import { import {
deduplicateOrders, deduplicateOrders,
@@ -15,7 +15,7 @@ import {
const originalTradeExchange = process.env.TRADE_EXCHANGE; const originalTradeExchange = process.env.TRADE_EXCHANGE;
const originalExchange = process.env.EXCHANGE; const originalExchange = process.env.EXCHANGE;
const baseOrder: AsterOrder = { const baseOrder: Order = {
orderId: 1, orderId: 1,
clientOrderId: "client", clientOrderId: "client",
symbol: "BTCUSDT", symbol: "BTCUSDT",
@@ -66,7 +66,7 @@ describe("order-coordinator", () => {
const timers: OrderTimerMap = {}; const timers: OrderTimerMap = {};
const pending: OrderPendingMap = {}; const pending: OrderPendingMap = {};
const log = vi.fn(); const log = vi.fn();
const openOrders: AsterOrder[] = [ const openOrders: Order[] = [
{ ...baseOrder, orderId: 1 }, { ...baseOrder, orderId: 1 },
{ ...baseOrder, orderId: 2 }, { ...baseOrder, orderId: 2 },
]; ];
+3 -3
View File
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { getDepthBetweenPrices } from "../src/utils/price"; import { getDepthBetweenPrices } from "../src/utils/price";
import type { AsterDepth } from "../src/exchanges/types"; import type { Depth } from "../src/exchanges/types";
describe("getDepthBetweenPrices boundary", () => { describe("getDepthBetweenPrices boundary", () => {
it("SELL side excludes quantity exactly at target price", () => { it("SELL side excludes quantity exactly at target price", () => {
const depth: AsterDepth = { const depth: Depth = {
lastUpdateId: 1, lastUpdateId: 1,
bids: [], bids: [],
asks: [ asks: [
@@ -20,7 +20,7 @@ describe("getDepthBetweenPrices boundary", () => {
}); });
it("BUY side excludes quantity exactly at target price", () => { it("BUY side excludes quantity exactly at target price", () => {
const depth: AsterDepth = { const depth: Depth = {
lastUpdateId: 1, lastUpdateId: 1,
bids: [ bids: [
["69355", "1"], ["69355", "1"],
+3 -3
View File
@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { computeBollingerBandwidth, getPosition, getSMA } from "../src/utils/strategy"; import { computeBollingerBandwidth, getPosition, getSMA } from "../src/utils/strategy";
import type { AsterAccountSnapshot, AsterKline } from "../src/exchanges/types"; import type { AccountSnapshot, Kline } from "../src/exchanges/types";
const mockSnapshot = (positions: Array<{ symbol: string; amt: number; entry: number; pnl: number }> = []): AsterAccountSnapshot => ({ const mockSnapshot = (positions: Array<{ symbol: string; amt: number; entry: number; pnl: number }> = []): AccountSnapshot => ({
canTrade: true, canTrade: true,
canDeposit: true, canDeposit: true,
canWithdraw: true, canWithdraw: true,
@@ -20,7 +20,7 @@ const mockSnapshot = (positions: Array<{ symbol: string; amt: number; entry: num
assets: [], assets: [],
}); });
const mockKlines = (values: number[]): AsterKline[] => const mockKlines = (values: number[]): Kline[] =>
values.map((value, index) => ({ values.map((value, index) => ({
openTime: index, openTime: index,
open: String(value), open: String(value),