mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-12 09:48:07 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abe464c505 | ||
|
|
27abeb9fa4 | ||
|
|
98c58727c7 | ||
|
|
4205d5dc12 | ||
|
|
227484152d |
@@ -36,5 +36,3 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
.tmp
|
||||
.tmp/*
|
||||
|
||||
.factory
|
||||
@@ -1,3 +0,0 @@
|
||||
docs/
|
||||
.claude/
|
||||
.cursor/
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
"coverage/**",
|
||||
"data/**",
|
||||
"dist/**",
|
||||
"docs/**",
|
||||
"node_modules/**",
|
||||
"out/**",
|
||||
".tmp/**"
|
||||
]
|
||||
}
|
||||
@@ -1,142 +1,35 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Package Manager
|
||||
## Project Structure & Module Organization
|
||||
The Bun entry point (`index.ts`) lives at the repo root for quick CLI smoke checks. All production code is under `src/`:
|
||||
|
||||
**必须使用 Bun** — 本项目使用 Bun 作为包管理器和运行时。所有命令都必须用 bun 执行,**不要使用 npm、yarn 或 npx**。
|
||||
- `src/strategy/` gathers every live trading engine (`maker`, `offset-maker`, `trend`). Shared helpers for strategy wiring sit in `src/strategy/common/`.
|
||||
- `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.
|
||||
|
||||
## Project Structure
|
||||
## Build, Test, and Development Commands
|
||||
- `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.
|
||||
|
||||
入口 `index.ts` 在仓库根目录,用于 CLI 启动。所有生产代码在 `src/` 下:
|
||||
Strategy-specific scripts still execute through the CLI; there is no separate `legacy/` workspace.
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
## 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.
|
||||
|
||||
辅助目录:
|
||||
- `tests/` — Vitest 测试套件(大部分测试在此,少量与逻辑共处于 `src/`)
|
||||
- `docs/` — 交易所 API 参考文档
|
||||
- `scripts/` — 一次性脚本(密钥派生等)
|
||||
- `skills/` — AI 助手技能配置
|
||||
## 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.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
bun install # 安装依赖
|
||||
bun run dev # 启动 CLI 菜单(等同于 bun run index.ts)
|
||||
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:`),每次提交作用域限单个模块或策略。
|
||||
## Commit & Pull Request Guidelines
|
||||
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.
|
||||
- Validation notes (commands run, environments touched).
|
||||
- Relevant logs or screenshots for behavior changes.
|
||||
Link issues/tasks when available.
|
||||
|
||||
## Environment & Secrets
|
||||
|
||||
复制 `.env.example` 为 `.env` 填入 API 密钥。**不要读取、打印或提交 `.env` 和任何密钥。** 泄露立即轮换。
|
||||
|
||||
## Safety
|
||||
|
||||
- 永远不要读取、运行、打印或访问 `.env` 或任何 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.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# RitMEX Bot - Claude Instructions
|
||||
|
||||
## Package Manager
|
||||
|
||||
**必须使用 Bun** - 这个项目使用 Bun 作为包管理器和运行时。所有能用 bun 执行的命令都必须使用 bun:
|
||||
|
||||
- 安装依赖: `bun install`
|
||||
- 运行脚本: `bun run <script>`
|
||||
- 执行测试: `bun test`
|
||||
- 类型检查: `bun run typecheck`
|
||||
|
||||
**不要使用 npm、yarn 或 npx**
|
||||
@@ -11,7 +11,6 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
|
||||
如果您希望获取优惠并支持本项目,请考虑使用以下注册链接:
|
||||
|
||||
* [Lighter 手续费优惠注册链接](https://app.lighter.xyz/?referral=111909FA)
|
||||
* [Ondo Perps 邀请注册链接](https://app.ondoperps.xyz/?ref=4A3ACQ)
|
||||
* [Aster 手续费优惠注册链接](https://www.asterdex.com/zh-CN/referral/4665f3)
|
||||
* [StandX 手续费优惠注册链接](https://standx.com/referral?code=xingxingjun)
|
||||
* [Binance 手续费优惠注册链接](https://www.binance.com/join?ref=KNKCA9XC)
|
||||
@@ -21,29 +20,10 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
|
||||
* [edgex 手续费优惠注册链接](https://pro.edgex.exchange/referral/BULL)
|
||||
* [Paradex 手续费优惠注册链接](https://paradex.io/ref/xingxingjun)
|
||||
* [Apex 手续费优惠注册链接](https://join.omni.apex.exchange/SEA)
|
||||
* [Hyperliquid 邀请注册链接](https://app.hyperliquid.xyz/join/RITMEX)
|
||||
|
||||
## CLI 命令模式(ritmex-bot)
|
||||
`ritmex-bot` 支持 Agent 友好的结构化命令调用,覆盖交易所能力查询、行情、账户、仓位、下单、撤单与策略启动。
|
||||
|
||||
- 保持现有环境变量体系,不新增也不改名,只读取当前执行环境中的变量。
|
||||
- `--symbol` 原样透传,不对交易对做统一改写。
|
||||
- 支持 `--dry-run` 模拟执行与 `--json` 结构化输出,便于自动化系统集成。
|
||||
|
||||
### 安装当前项目 Skill(skills 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 使用手册(中文)](cli-guide.md)
|
||||
- [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
|
||||
- [简明上手指南(零基础)](simple-readme.md)
|
||||
- [基础网格策略使用教程](grid-trading.md)
|
||||
- [Ondo Perps 接入说明](docs/ondoperps/README.md)
|
||||
|
||||
## 核心特性
|
||||
- **实时行情与风控**:Websocket + REST 自动同步账户、挂单与仓位,断线后自动恢复。
|
||||
@@ -62,7 +42,6 @@ npx skills add https://github.com/discountry/ritmex-bot --skill use-ritmex-bot
|
||||
| Backpack | USDC 永续 | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, `BACKPACK_PASSWORD` | `BACKPACK_SANDBOX=true` 启用沙盒
|
||||
| Paradex | StarkEx 永续 | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | `PARADEX_SANDBOX=true` 使用测试网
|
||||
| Nado | USDC 永续 | `NADO_SIGNER_PRIVATE_KEY`, `NADO_SUBACCOUNT_OWNER` | `NADO_ENV` 可切换 `inkMainnet`/`inkTestnet`
|
||||
| Ondo Perps | USD 加密资产/股票/商品永续 | `ONDOPERPS_API_KEY_ID`, `ONDOPERPS_API_SECRET` | API Key HMAC 鉴权;支持生产与沙盒环境
|
||||
|
||||
## 系统要求
|
||||
- Bun ≥ 1.2(需同时包含 `bun`、`bunx` 命令)
|
||||
@@ -107,7 +86,7 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
|
||||
|
||||
| 变量 | 说明 |
|
||||
| --- | --- |
|
||||
| `EXCHANGE` | 选择交易所(`aster`/`binance`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado`/`ondoperps`) |
|
||||
| `EXCHANGE` | 选择交易所(`aster`/`binance`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado`) |
|
||||
| `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT`) |
|
||||
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
|
||||
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
|
||||
@@ -164,17 +143,6 @@ EXCHANGE=binance BINANCE_MARKET_TYPE=perp BINANCE_SYMBOL=BTCUSDT_PERP bun run in
|
||||
EXCHANGE=binance BINANCE_MARKET_TYPE=spot BINANCE_SYMBOL=BTCUSDT bun run index.ts --strategy grid
|
||||
```
|
||||
|
||||
### Ondo Perps
|
||||
1. 使用[邀请链接](https://app.ondoperps.xyz/?ref=4A3ACQ)注册并在账户页面创建具备交易权限的 API Key。
|
||||
2. 设置 `EXCHANGE=ondoperps`,填写 `ONDOPERPS_API_KEY_ID` 与 `ONDOPERPS_API_SECRET`。
|
||||
3. 设置官方市场格式的 `ONDOPERPS_SYMBOL`,默认 `BTC-USD.P`;其他示例包括 `XAU-USD.P` 与 `NVDA-USD.P`。
|
||||
4. 沙盒环境设置 `ONDOPERPS_SANDBOX=true`;自定义端点可使用 `ONDOPERPS_BASE_URL` 与 `ONDOPERPS_WS_URL`。
|
||||
5. Builder 接入可选配置 `ONDOPERPS_BUILDER_CODE` 与 `ONDOPERPS_BUILDER_FEE_RATE_BPS`,费率上限为 10 bps。
|
||||
|
||||
兼容配置:`ondoperp` 可作为 `ondoperps` 的交易所别名,旧 `ONDOPERP_*` 环境变量前缀继续生效。
|
||||
|
||||
适配器使用 REST HMAC 签名执行下单、撤单和账户查询,通过 WebSocket 接收深度、标记价、K 线、订单、仓位与余额更新。仓位级止损映射为统一的 `STOP_MARKET` 订单。官方接入索引:[Ondo Perps llms.txt](https://ondoperps.mintlify.app/llms.txt)。
|
||||
|
||||
### StandX
|
||||
|
||||
* [StandX 做市策略教程](docs/standx/maker-points-guide.md)
|
||||
@@ -241,40 +209,9 @@ EXCHANGE=binance BINANCE_MARKET_TYPE=spot BINANCE_SYMBOL=BTCUSDT bun run index.t
|
||||
bun run index.ts # 启动 CLI(默认入口)
|
||||
bun run start # 等价于运行 index.ts
|
||||
bun run dev # 调试模式
|
||||
bun run lint # 执行 Oxlint 检查
|
||||
bun run lint:fix # 自动修复可安全修复的问题
|
||||
bun x vitest run # 执行全部测试
|
||||
```
|
||||
|
||||
## ritmex-bot 命令模式(Agent 友好)
|
||||
项目现已支持独立命令模式,命令名为 `ritmex-bot`:
|
||||
|
||||
```bash
|
||||
ritmex-bot doctor
|
||||
ritmex-bot exchange list
|
||||
ritmex-bot market ticker --exchange binance --symbol BTCUSDT
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000 --dry-run
|
||||
ritmex-bot strategy run --strategy maker --exchange standx --silent --dry-run
|
||||
```
|
||||
|
||||
### 运行方式
|
||||
```bash
|
||||
# 全局安装
|
||||
npm install -g ritmex-bot
|
||||
ritmex-bot doctor
|
||||
|
||||
# 不安装直接运行
|
||||
npx ritmex-bot doctor
|
||||
bunx ritmex-bot doctor
|
||||
```
|
||||
|
||||
### 全局参数
|
||||
- `--exchange`:按现有逻辑选择交易所(不修改原有环境变量体系)
|
||||
- `--symbol`:原样透传,不做统一或改写
|
||||
- `--dry-run`:模拟执行,不发真实下单/撤单请求
|
||||
- `--json`:输出结构化 JSON,便于 AI Agent 解析
|
||||
- `--timeout`:命令超时毫秒数
|
||||
|
||||
## 静默启动与后台运行
|
||||
### 直接静默启动
|
||||
无需进入 Ink 菜单,可用命令行直接拉起指定策略:
|
||||
@@ -309,8 +246,6 @@ bun run pm2:start:offset
|
||||
## 测试
|
||||
项目使用 Vitest:
|
||||
```bash
|
||||
bun run lint
|
||||
bun run lint:fix
|
||||
bun run test
|
||||
bun x vitest --watch
|
||||
```
|
||||
|
||||
+1
-66
@@ -7,7 +7,6 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
|
||||
If you'd like to support this project and get fee discounts, please consider using these referral links:
|
||||
|
||||
* [Lighter referral link](https://app.lighter.xyz/?referral=111909FA)
|
||||
* [Ondo Perps invite link](https://app.ondoperps.xyz/?ref=4A3ACQ)
|
||||
* [Aster referral link](https://www.asterdex.com/en/referral/4665f3)
|
||||
* [StandX referral link](https://standx.com/referral?code=xingxingjun)
|
||||
* [Binance referral link](https://www.binance.com/join?ref=KNKCA9XC)
|
||||
@@ -17,29 +16,10 @@ If you'd like to support this project and get fee discounts, please consider usi
|
||||
* [edgex referral link](https://pro.edgex.exchange/referral/BULL)
|
||||
* [Paradex referral link](https://paradex.io/ref/xingxingjun)
|
||||
* [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
|
||||
- [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
|
||||
- [ritmex-bot CLI 使用手册(中文)](cli-guide.md)
|
||||
- [Beginner-friendly Quick Start](simple-readme.md)
|
||||
- [Grid Trading Strategy Guide](grid-trading.md)
|
||||
- [Ondo Perps Integration Guide](docs/ondoperps/README.md)
|
||||
|
||||
## Highlights
|
||||
- **Live data & risk sync** via websockets with REST fallbacks and full reconciliation on restart.
|
||||
@@ -58,7 +38,6 @@ Full guide: [ritmex-bot CLI User Guide (English)](cli-guide.en.md)
|
||||
| Backpack | USDC perpetuals | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, `BACKPACK_PASSWORD` | Set `BACKPACK_SANDBOX=true` for the sandbox |
|
||||
| Paradex | StarkEx perpetuals | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | Toggle `PARADEX_SANDBOX=true` for the testnet |
|
||||
| Nado | USDC perpetuals | `NADO_SIGNER_PRIVATE_KEY`, `NADO_SUBACCOUNT_OWNER` | Switch `NADO_ENV` between `inkMainnet` and `inkTestnet` |
|
||||
| Ondo Perps | USD crypto/equity/commodity perpetuals | `ONDOPERPS_API_KEY_ID`, `ONDOPERPS_API_SECRET` | API-key HMAC authentication with production and sandbox endpoints |
|
||||
|
||||
## Requirements
|
||||
- Bun >= 1.2 (both `bun` and `bunx` on PATH)
|
||||
@@ -103,7 +82,7 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
|
||||
|
||||
| Variable | Purpose |
|
||||
| --- | --- |
|
||||
| `EXCHANGE` | Choose the venue (`aster` / `binance` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado` / `ondoperps`) |
|
||||
| `EXCHANGE` | Choose the venue (`aster` / `binance` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) |
|
||||
| `TRADE_SYMBOL` | Contract symbol (defaults to `BTCUSDT`) |
|
||||
| `TRADE_AMOUNT` | Order size in base asset units |
|
||||
| `LOSS_LIMIT` | Max per-trade loss in USDT before forced close |
|
||||
@@ -161,17 +140,6 @@ EXCHANGE=binance BINANCE_MARKET_TYPE=perp BINANCE_SYMBOL=BTCUSDT_PERP bun run in
|
||||
EXCHANGE=binance BINANCE_MARKET_TYPE=spot BINANCE_SYMBOL=BTCUSDT bun run index.ts --strategy grid
|
||||
```
|
||||
|
||||
### Ondo Perps
|
||||
1. Register through the [invite link](https://app.ondoperps.xyz/?ref=4A3ACQ) and create an API key with trading permissions from the account page.
|
||||
2. Set `EXCHANGE=ondoperps`, `ONDOPERPS_API_KEY_ID`, and `ONDOPERPS_API_SECRET`.
|
||||
3. Set `ONDOPERPS_SYMBOL` with the official market format. The default is `BTC-USD.P`; other examples include `XAU-USD.P` and `NVDA-USD.P`.
|
||||
4. Use `ONDOPERPS_SANDBOX=true` for sandbox endpoints. `ONDOPERPS_BASE_URL` and `ONDOPERPS_WS_URL` provide explicit endpoint overrides.
|
||||
5. Builder integrations can set `ONDOPERPS_BUILDER_CODE` and `ONDOPERPS_BUILDER_FEE_RATE_BPS`; the fee is capped at 10 bps.
|
||||
|
||||
Compatibility: `ondoperp` remains an exchange alias for `ondoperps`, and legacy `ONDOPERP_*` environment variables remain supported.
|
||||
|
||||
The adapter signs REST requests with the Ondo API-key HMAC scheme and consumes WebSocket depth, mark price, kline, order, position, and balance updates. Position-level stops map into the shared `STOP_MARKET` order contract. Official integration index: [Ondo Perps llms.txt](https://ondoperps.mintlify.app/llms.txt).
|
||||
|
||||
### StandX
|
||||
|
||||
* [StandX Maker Points Strategy Guide](docs/standx/maker-points-guide.md)
|
||||
@@ -237,40 +205,9 @@ Please keep these credentials safe and do not share them with anyone.
|
||||
bun run index.ts # Launch the CLI (default entrypoint)
|
||||
bun run start # Alias for bun run index.ts
|
||||
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
|
||||
```
|
||||
|
||||
## ritmex-bot Command Mode (Agent-friendly)
|
||||
The project now supports a standalone command mode with the command name `ritmex-bot`:
|
||||
|
||||
```bash
|
||||
ritmex-bot doctor
|
||||
ritmex-bot exchange list
|
||||
ritmex-bot market ticker --exchange binance --symbol BTCUSDT
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000 --dry-run
|
||||
ritmex-bot strategy run --strategy maker --exchange standx --silent --dry-run
|
||||
```
|
||||
|
||||
### Run Modes
|
||||
```bash
|
||||
# Global install
|
||||
npm install -g ritmex-bot
|
||||
ritmex-bot doctor
|
||||
|
||||
# No install
|
||||
npx ritmex-bot doctor
|
||||
bunx ritmex-bot doctor
|
||||
```
|
||||
|
||||
### Global Flags
|
||||
- `--exchange`: picks exchange using the existing env/config logic
|
||||
- `--symbol`: passed through as-is (no symbol normalization)
|
||||
- `--dry-run`: simulation mode (no real create/cancel side effects)
|
||||
- `--json`: structured JSON output for AI agents
|
||||
- `--timeout`: command timeout in milliseconds
|
||||
|
||||
## Silent & Background Execution
|
||||
### Direct silent launch
|
||||
Skip the Ink menu and start a strategy directly:
|
||||
@@ -305,8 +242,6 @@ Run `pm2 save` afterwards if you want the process list to survive reboots.
|
||||
## Testing
|
||||
Powered by Vitest:
|
||||
```bash
|
||||
bun run lint
|
||||
bun run lint:fix
|
||||
bun run test
|
||||
bun x vitest --watch
|
||||
```
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const entry = resolve(__dirname, "..", "index.ts");
|
||||
const bunBinary = process.env.BUN_BIN || "bun";
|
||||
|
||||
const result = spawnSync(bunBinary, ["run", entry, ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error("[ritmex-bot] Bun is required to run this CLI. Install Bun from https://bun.sh");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
@@ -5,27 +5,26 @@
|
||||
"": {
|
||||
"name": "ritmex-bot",
|
||||
"dependencies": {
|
||||
"@grvt/client": "^1.6.27",
|
||||
"@nadohq/client": "^0.1.0-alpha.51",
|
||||
"@grvt/client": "^1.6.4",
|
||||
"@nadohq/client": "^0.1.0-alpha.41",
|
||||
"@noble/ed25519": "^3.0.0",
|
||||
"axios": "^1.13.6",
|
||||
"bignumber.js": "^10.0.2",
|
||||
"ccxt": "^4.5.40",
|
||||
"dotenv": "^17.3.1",
|
||||
"ethereum-cryptography": "^3.2.0",
|
||||
"ink": "^6.8.0",
|
||||
"react": "^19.2.4",
|
||||
"axios": "^1.12.2",
|
||||
"bignumber.js": "^9.3.1",
|
||||
"ccxt": "^4.5.12",
|
||||
"dotenv": "^17.2.2",
|
||||
"ethereum-cryptography": "^2.1.3",
|
||||
"ink": "^6.3.1",
|
||||
"react": "^19.1.1",
|
||||
"trading-signals": "^7.4.3",
|
||||
"viem": "^2.46.3",
|
||||
"ws": "^8.19.0",
|
||||
"viem": "^2.43.1",
|
||||
"ws": "^8.18.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.9",
|
||||
"oxlint": "^1.54.0",
|
||||
"vitest": "^4.0.18",
|
||||
"@types/bun": "latest",
|
||||
"vitest": "^3.2.4",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -86,65 +85,27 @@
|
||||
|
||||
"@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.27", "", { "dependencies": { "axios": "^1.13.5" } }, "sha512-YUb7bsPPRNhZs/T+RGDBp/AKbegRHvjB8KXpTEAy5Dbf44RI00KoX9S/R12HzxLtS5rSblmcpD7hmCtDnlcN5Q=="],
|
||||
"@grvt/client": ["@grvt/client@1.6.25", "", { "dependencies": { "axios": "^1.13.2" } }, "sha512-LG3oZSJDq1Qz6mLiHDmg+v/Hp+Nd+yOp8GIvkBVyIdOcPVzuzC8UQD7IxvS+g5EfQQKLiKzXSjKLc39a11Uj1A=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@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/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/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/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/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/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/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/shared": ["@nadohq/shared@0.1.0-alpha.45", "", { "peerDependencies": { "bignumber.js": "^9.3.0", "viem": "*" } }, "sha512-4Ml8dO9mkwnIidflpunwVMLX2KJA79b53T9xTBkH1Yzy0iOnbEE27r25wvgJgyszU0aznbPBv2++FZ0d0B5Zhg=="],
|
||||
|
||||
"@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=="],
|
||||
"@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=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@1.9.0", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg=="],
|
||||
"@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="],
|
||||
|
||||
"@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="],
|
||||
|
||||
"@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=="],
|
||||
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="],
|
||||
|
||||
@@ -190,64 +151,72 @@
|
||||
|
||||
"@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.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
|
||||
"@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
|
||||
|
||||
"@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/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/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
|
||||
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="],
|
||||
|
||||
"@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/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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/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/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/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/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
|
||||
"@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/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/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/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
||||
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
||||
"@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=="],
|
||||
|
||||
"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.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
|
||||
"ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||
|
||||
"axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
|
||||
"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=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@10.0.2", "", {}, "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ=="],
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ccxt": ["ccxt@4.5.40", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-8nJhhNKft7+fELDNQuCV2AQZBxLJfYQaD2LpqxHrucSszosyVl0u6lCIFfcnDUbeUW925uhrsKevHf2fNbLjKw=="],
|
||||
"ccxt": ["ccxt@4.5.35", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-aFn1jq/vR9wD3Vxu/2UFoS8snqlvfYn/iYrNhyZZfzB3N+kAHhP5+sAgO6ZwNHkHiUT9IPPahC1ZSFdPtjpIYA=="],
|
||||
|
||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
"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=="],
|
||||
|
||||
"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-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
||||
@@ -260,9 +229,13 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -288,7 +261,7 @@
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
||||
|
||||
@@ -298,7 +271,7 @@
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
@@ -320,7 +293,7 @@
|
||||
|
||||
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
@@ -328,7 +301,11 @@
|
||||
|
||||
"isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -338,20 +315,20 @@
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
@@ -374,7 +351,7 @@
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
|
||||
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
@@ -382,43 +359,47 @@
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
|
||||
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
|
||||
|
||||
"type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
"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@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=="],
|
||||
|
||||
"widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -426,16 +407,34 @@
|
||||
|
||||
"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/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/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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
|
||||
-389
@@ -1,389 +0,0 @@
|
||||
# ritmex-bot CLI User Guide (English)
|
||||
|
||||
This guide documents the `ritmex-bot` command mode (agent-friendly mode), which lets you run structured trading operations without entering the Ink interactive menu.
|
||||
|
||||
## 1. Scope
|
||||
|
||||
`ritmex-bot` CLI command mode covers:
|
||||
|
||||
- Exchange listing and capability checks
|
||||
- Market data (`ticker` / `depth` / `kline`)
|
||||
- Account and position queries
|
||||
- Open/create/cancel/cancel-all order operations
|
||||
- Strategy execution (including dry-run)
|
||||
|
||||
This mode is suitable for:
|
||||
|
||||
- Manual terminal usage
|
||||
- `npx` / `bunx` invocation
|
||||
- Programmatic AI-agent workflows (recommended with `--json`)
|
||||
|
||||
## 2. Installation and Run Modes
|
||||
|
||||
### 2.1 Global install (recommended)
|
||||
|
||||
```bash
|
||||
npm install -g ritmex-bot
|
||||
ritmex-bot doctor
|
||||
```
|
||||
|
||||
### 2.2 Quick run without install
|
||||
|
||||
```bash
|
||||
npx ritmex-bot doctor
|
||||
bunx ritmex-bot doctor
|
||||
```
|
||||
|
||||
### 2.3 Local dependency
|
||||
|
||||
```bash
|
||||
npm install ritmex-bot
|
||||
npx ritmex-bot doctor
|
||||
```
|
||||
|
||||
### 2.4 Run from repository source
|
||||
|
||||
```bash
|
||||
bun run index.ts doctor
|
||||
bun run index.ts market ticker --exchange binance --symbol BTCUSDT
|
||||
```
|
||||
|
||||
> Note: `bin/ritmex-bot` launches the entrypoint via `bun run`, so Bun must be available in the runtime environment.
|
||||
|
||||
## 3. Command Shape
|
||||
|
||||
```bash
|
||||
ritmex-bot <root-command> <action> [options]
|
||||
```
|
||||
|
||||
Supported root commands:
|
||||
|
||||
- `help`
|
||||
- `doctor`
|
||||
- `exchange`
|
||||
- `market`
|
||||
- `account`
|
||||
- `position`
|
||||
- `order`
|
||||
- `strategy`
|
||||
|
||||
Quick help:
|
||||
|
||||
```bash
|
||||
ritmex-bot help
|
||||
ritmex-bot market ticker --help
|
||||
```
|
||||
|
||||
## 4. Global Options
|
||||
|
||||
| Option | Short | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| `--exchange` | `-e` | Exchange ID override | Existing env resolution |
|
||||
| `--symbol` | - | Trading symbol (pass-through) | Existing env resolution |
|
||||
| `--json` | `-j` | JSON output | `false` |
|
||||
| `--dry-run` | `-d` | Simulate write operations | `false` |
|
||||
| `--timeout` | `-t` | Timeout in milliseconds | `25000` |
|
||||
| `--help` | `-h` | Show command help | `false` |
|
||||
|
||||
## 5. Environment Variables and Symbol Rules
|
||||
|
||||
Command mode follows these constraints:
|
||||
|
||||
- No new environment variables are introduced.
|
||||
- No existing environment-variable names are modified.
|
||||
- Runtime values are read directly from the current CLI environment.
|
||||
- If `--exchange` is not provided, exchange resolution follows the existing logic (for example `EXCHANGE` / `TRADE_EXCHANGE`).
|
||||
- If `--symbol` is not provided, symbol resolution follows the existing exchange-specific env logic.
|
||||
- Symbols are never normalized or rewritten by command mode.
|
||||
|
||||
That means symbol variants such as `BTCUSDT`, `BTCUSDC`, spot/perp pairs, and venue-specific naming are fully controlled by your existing environment configuration.
|
||||
|
||||
## 6. Command Reference
|
||||
|
||||
### 6.1 `doctor`
|
||||
|
||||
Checks effective exchange/symbol setup and runtime adapter capabilities.
|
||||
|
||||
```bash
|
||||
ritmex-bot doctor
|
||||
ritmex-bot doctor --exchange binance --symbol BTCUSDT --json
|
||||
```
|
||||
|
||||
### 6.2 `exchange`
|
||||
|
||||
### List supported exchanges
|
||||
|
||||
```bash
|
||||
ritmex-bot exchange list
|
||||
```
|
||||
|
||||
### Inspect exchange capabilities
|
||||
|
||||
```bash
|
||||
ritmex-bot exchange capabilities --exchange standx
|
||||
```
|
||||
|
||||
If runtime adapter initialization fails, CLI falls back to static capability metadata (`source: "static"`) and includes a warning.
|
||||
|
||||
### 6.3 `market`
|
||||
|
||||
### `market ticker`
|
||||
|
||||
```bash
|
||||
ritmex-bot market ticker --exchange binance --symbol BTCUSDT
|
||||
```
|
||||
|
||||
### `market depth`
|
||||
|
||||
```bash
|
||||
ritmex-bot market depth --exchange binance --symbol BTCUSDT --levels 10
|
||||
```
|
||||
|
||||
`--levels` is optional; when provided, depth levels are truncated to that value.
|
||||
|
||||
### `market kline`
|
||||
|
||||
```bash
|
||||
ritmex-bot market kline --exchange binance --symbol BTCUSDT --interval 1m --limit 50
|
||||
```
|
||||
|
||||
Arguments:
|
||||
|
||||
- `--interval` is required
|
||||
- `--limit` is optional (keeps the latest N candles)
|
||||
|
||||
### 6.4 `account` and `position`
|
||||
|
||||
### Account snapshot
|
||||
|
||||
```bash
|
||||
ritmex-bot account snapshot --exchange standx
|
||||
ritmex-bot account summary --exchange standx
|
||||
```
|
||||
|
||||
`summary` is an alias of `snapshot`.
|
||||
|
||||
### Position list
|
||||
|
||||
```bash
|
||||
ritmex-bot position list --exchange standx
|
||||
ritmex-bot position list --exchange standx --symbol BTC-USD
|
||||
```
|
||||
|
||||
If `--symbol` is provided, result positions are filtered by that symbol.
|
||||
|
||||
### 6.5 `order`
|
||||
|
||||
### Open orders
|
||||
|
||||
```bash
|
||||
ritmex-bot order open --exchange binance --symbol BTCUSDT
|
||||
```
|
||||
|
||||
### Create order
|
||||
|
||||
```bash
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000
|
||||
```
|
||||
|
||||
Create-order arguments:
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--side` | Yes | `buy` / `sell` |
|
||||
| `--type` | Yes | `limit` / `market` / `stop` / `trailing-stop` / `close` |
|
||||
| `--quantity` or `--qty` | Yes | Order quantity |
|
||||
| `--price` | Required for `limit` | Limit price |
|
||||
| `--stop-price` | Required for `stop` | Stop trigger price |
|
||||
| `--activation-price` | Required for `trailing-stop` | Trailing-stop activation price |
|
||||
| `--callback-rate` | Required for `trailing-stop` | Trailing-stop callback rate |
|
||||
| `--time-in-force` | No | `GTC` / `IOC` / `FOK` / `GTX` |
|
||||
| `--reduce-only` | No | `true/false` |
|
||||
| `--close-position` | No | `true/false` |
|
||||
| `--trigger-type` | No | `UNSPECIFIED` / `TAKE_PROFIT` / `STOP_LOSS` |
|
||||
| `--sl-price` | No | Stop-loss price (router applies venue capability rules) |
|
||||
| `--tp-price` | No | Take-profit price (router applies venue capability rules) |
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Market order
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type market --qty 0.01
|
||||
|
||||
# Stop order
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side sell --type stop --qty 0.01 --stop-price 86000
|
||||
|
||||
# Trailing-stop order
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side sell --type trailing-stop --qty 0.01 --activation-price 91000 --callback-rate 0.2
|
||||
|
||||
# Close intent order (defaults reduceOnly/closePosition in route layer)
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side sell --type close --qty 0.01
|
||||
```
|
||||
|
||||
### Cancel order
|
||||
|
||||
```bash
|
||||
ritmex-bot order cancel --exchange binance --symbol BTCUSDT --order-id 123456
|
||||
```
|
||||
|
||||
### Cancel all orders
|
||||
|
||||
```bash
|
||||
ritmex-bot order cancel-all --exchange binance --symbol BTCUSDT
|
||||
```
|
||||
|
||||
If `forceCancelAllOrders` is available on the adapter, CLI uses it first; otherwise it falls back to standard cancel-all behavior.
|
||||
|
||||
### 6.6 `strategy`
|
||||
|
||||
### Run strategy
|
||||
|
||||
```bash
|
||||
ritmex-bot strategy run --strategy maker --exchange standx --silent
|
||||
```
|
||||
|
||||
Supported strategy IDs:
|
||||
|
||||
- `trend`
|
||||
- `swing`
|
||||
- `guardian`
|
||||
- `maker`
|
||||
- `maker-points`
|
||||
- `offset-maker`
|
||||
- `liquidity-maker`
|
||||
- `basis`
|
||||
- `grid`
|
||||
|
||||
Supported aliases:
|
||||
|
||||
- `offset` -> `offset-maker`
|
||||
- `makerpoints` / `maker_points` -> `maker-points`
|
||||
- `liquidity` / `liquiditymaker` / `liquidity_maker` -> `liquidity-maker`
|
||||
|
||||
Extra options:
|
||||
|
||||
- `--silent` (short form `-q`) for quieter strategy startup logs
|
||||
- `--dry-run` to propagate simulation mode into strategy execution
|
||||
|
||||
## 7. `--dry-run` Behavior
|
||||
|
||||
With `--dry-run` enabled:
|
||||
|
||||
- `order create` / `order cancel` / `order cancel-all` do not send real write operations.
|
||||
- Response includes `dryRunActions` so you can inspect the simulated intent.
|
||||
- `strategy run` receives dry-run mode in the strategy runner.
|
||||
- Read-only commands (market/account/position queries) keep normal behavior.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000 --dry-run --json
|
||||
```
|
||||
|
||||
## 8. Unsupported Features
|
||||
|
||||
Exchange capabilities are intentionally not forced into a fake unified surface. If a venue does not support a feature, CLI returns `UNSUPPORTED` directly.
|
||||
|
||||
Typical cases:
|
||||
|
||||
- Exchange does not implement `queryOpenOrders`
|
||||
- Exchange does not implement `queryAccountSnapshot`
|
||||
- Exchange-specific order type limitations
|
||||
|
||||
Callers should handle `UNSUPPORTED` as a first-class outcome.
|
||||
|
||||
## 9. Output and Exit Codes
|
||||
|
||||
### 9.1 Human-readable output (default)
|
||||
|
||||
Success:
|
||||
|
||||
```text
|
||||
[OK] market-ticker
|
||||
time: 2026-02-27T12:00:00.000Z
|
||||
exchange: binance
|
||||
symbol: BTCUSDT
|
||||
dryRun: false
|
||||
...
|
||||
```
|
||||
|
||||
Failure:
|
||||
|
||||
```text
|
||||
[ERROR] order-open
|
||||
time: 2026-02-27T12:00:00.000Z
|
||||
code: UNSUPPORTED
|
||||
message: queryOpenOrders is not supported on exchange 'aster'
|
||||
retryable: false
|
||||
```
|
||||
|
||||
### 9.2 JSON output (`--json`)
|
||||
|
||||
Success payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"command": "market-ticker",
|
||||
"exchange": "binance",
|
||||
"symbol": "BTCUSDT",
|
||||
"dryRun": false,
|
||||
"ts": "2026-02-27T12:00:00.000Z",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
Failure payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"command": "order-open",
|
||||
"exchange": "aster",
|
||||
"symbol": "BTCUSDT",
|
||||
"dryRun": false,
|
||||
"ts": "2026-02-27T12:00:00.000Z",
|
||||
"error": {
|
||||
"code": "UNSUPPORTED",
|
||||
"message": "queryOpenOrders is not supported on exchange 'aster'",
|
||||
"retryable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 Exit codes
|
||||
|
||||
| Exit Code | Meaning |
|
||||
| --- | --- |
|
||||
| `0` | Success |
|
||||
| `2` | Invalid arguments (`INVALID_ARGS`) |
|
||||
| `3` | Missing environment setup (`MISSING_ENV`) |
|
||||
| `5` | Unsupported feature (`UNSUPPORTED`) |
|
||||
| `6` | Exchange execution error (`EXCHANGE_ERROR`) |
|
||||
| `7` | Timeout (`TIMEOUT`) |
|
||||
|
||||
## 10. Recommended AI-Agent Flow
|
||||
|
||||
```bash
|
||||
# 1) Check venue capabilities
|
||||
ritmex-bot exchange capabilities --exchange binance --json
|
||||
|
||||
# 2) Fetch market state
|
||||
ritmex-bot market ticker --exchange binance --symbol BTCUSDT --json
|
||||
|
||||
# 3) Validate order parameters with dry-run
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000 --dry-run --json
|
||||
|
||||
# 4) Submit live order after validation (remove --dry-run)
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000 --json
|
||||
```
|
||||
|
||||
## 11. Compatibility With Existing Features
|
||||
|
||||
Command mode does not break existing interactive or legacy startup flows:
|
||||
|
||||
- `bun run index.ts` still opens the Ink menu
|
||||
- `bun run index.ts --strategy trend --silent` still starts strategy directly
|
||||
- Command mode is used only when the first argument matches command roots (for example `market`, `order`, `strategy`)
|
||||
|
||||
If you already run the project with current env variables and strategy configs, command mode can be adopted incrementally with no env-key migration.
|
||||
-388
@@ -1,388 +0,0 @@
|
||||
# ritmex-bot CLI 使用手册(中文)
|
||||
|
||||
本文档介绍 `ritmex-bot` 的命令模式(Agent 友好模式),用于在不进入 Ink 菜单的情况下执行结构化交易操作。
|
||||
|
||||
## 1. 适用范围
|
||||
|
||||
`ritmex-bot` CLI 命令模式覆盖以下能力:
|
||||
|
||||
- 交易所列表与能力查询
|
||||
- 行情(ticker/depth/kline)
|
||||
- 账户与仓位查询
|
||||
- 订单查询、下单、撤单、全撤
|
||||
- 策略启动(支持 dry-run)
|
||||
|
||||
这套命令适合:
|
||||
|
||||
- 直接在终端手动执行
|
||||
- 通过 `npx` / `bunx` 调用
|
||||
- 被 AI Agent / 自动化系统程序化调用(推荐配合 `--json`)
|
||||
|
||||
## 2. 安装与运行方式
|
||||
|
||||
### 2.1 全局安装(推荐)
|
||||
|
||||
```bash
|
||||
npm install -g ritmex-bot
|
||||
ritmex-bot doctor
|
||||
```
|
||||
|
||||
### 2.2 无需安装直接运行
|
||||
|
||||
```bash
|
||||
npx ritmex-bot doctor
|
||||
bunx ritmex-bot doctor
|
||||
```
|
||||
|
||||
### 2.3 作为项目依赖
|
||||
|
||||
```bash
|
||||
npm install ritmex-bot
|
||||
npx ritmex-bot doctor
|
||||
```
|
||||
|
||||
### 2.4 在仓库源码中运行
|
||||
|
||||
```bash
|
||||
bun run index.ts doctor
|
||||
bun run index.ts market ticker --exchange binance --symbol BTCUSDT
|
||||
```
|
||||
|
||||
> 注意:`bin/ritmex-bot` 会调用 `bun run` 启动入口文件,因此运行环境需要可用的 Bun。
|
||||
|
||||
## 3. 命令结构
|
||||
|
||||
```bash
|
||||
ritmex-bot <root-command> <action> [options]
|
||||
```
|
||||
|
||||
支持的根命令:
|
||||
|
||||
- `help`
|
||||
- `doctor`
|
||||
- `exchange`
|
||||
- `market`
|
||||
- `account`
|
||||
- `position`
|
||||
- `order`
|
||||
- `strategy`
|
||||
|
||||
快速帮助:
|
||||
|
||||
```bash
|
||||
ritmex-bot help
|
||||
ritmex-bot market ticker --help
|
||||
```
|
||||
|
||||
## 4. 全局参数
|
||||
|
||||
| 参数 | 短参数 | 说明 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--exchange` | `-e` | 指定交易所 ID | 走现有环境变量解析 |
|
||||
| `--symbol` | - | 指定交易对,原样透传 | 走现有环境变量解析 |
|
||||
| `--json` | `-j` | 以 JSON 输出结果 | `false` |
|
||||
| `--dry-run` | `-d` | 模拟执行(不发真实写操作) | `false` |
|
||||
| `--timeout` | `-t` | 超时毫秒数 | `25000` |
|
||||
| `--help` | `-h` | 显示命令帮助 | `false` |
|
||||
|
||||
## 5. 环境变量与 Symbol 规则
|
||||
|
||||
`ritmex-bot` 命令模式遵循以下约束:
|
||||
|
||||
- 不新增环境变量,不修改任何已有变量名称。
|
||||
- 从当前 CLI 执行环境读取已有配置(例如 `.env` 已加载到进程环境后)。
|
||||
- `--exchange` 未指定时,按现有逻辑解析交易所(例如 `EXCHANGE` / `TRADE_EXCHANGE`)。
|
||||
- `--symbol` 未指定时,按现有逻辑从对应交易所配置中解析符号。
|
||||
- 对 `symbol` 不做统一、不做改写、不做跨交易所映射。你传什么就用什么。
|
||||
|
||||
这意味着像 Binance 的 `BTCUSDT`、`BTCUSDC`、现货/永续等差异,全部由你通过现有配置自行决定。
|
||||
|
||||
## 6. 命令详解
|
||||
|
||||
### 6.1 `doctor`
|
||||
|
||||
用于检查当前交易所/交易对配置与适配器能力。
|
||||
|
||||
```bash
|
||||
ritmex-bot doctor
|
||||
ritmex-bot doctor --exchange binance --symbol BTCUSDT --json
|
||||
```
|
||||
|
||||
### 6.2 `exchange`
|
||||
|
||||
### 列出支持的交易所
|
||||
|
||||
```bash
|
||||
ritmex-bot exchange list
|
||||
```
|
||||
|
||||
### 查询交易所能力
|
||||
|
||||
```bash
|
||||
ritmex-bot exchange capabilities --exchange standx
|
||||
```
|
||||
|
||||
如果运行时无法构建适配器,会返回静态能力信息(`source: "static"`)及警告。
|
||||
|
||||
### 6.3 `market`
|
||||
|
||||
### `market ticker`
|
||||
|
||||
```bash
|
||||
ritmex-bot market ticker --exchange binance --symbol BTCUSDT
|
||||
```
|
||||
|
||||
### `market depth`
|
||||
|
||||
```bash
|
||||
ritmex-bot market depth --exchange binance --symbol BTCUSDT --levels 10
|
||||
```
|
||||
|
||||
`--levels` 为可选,指定后会截断返回档位数量。
|
||||
|
||||
### `market kline`
|
||||
|
||||
```bash
|
||||
ritmex-bot market kline --exchange binance --symbol BTCUSDT --interval 1m --limit 50
|
||||
```
|
||||
|
||||
参数说明:
|
||||
|
||||
- `--interval` 必填
|
||||
- `--limit` 可选(仅保留最后 N 条)
|
||||
|
||||
### 6.4 `account` 与 `position`
|
||||
|
||||
### 账户快照
|
||||
|
||||
```bash
|
||||
ritmex-bot account snapshot --exchange standx
|
||||
ritmex-bot account summary --exchange standx
|
||||
```
|
||||
|
||||
`summary` 是 `snapshot` 的别名。
|
||||
|
||||
### 仓位列表
|
||||
|
||||
```bash
|
||||
ritmex-bot position list --exchange standx
|
||||
ritmex-bot position list --exchange standx --symbol BTC-USD
|
||||
```
|
||||
|
||||
传 `--symbol` 时会在返回结果中做符号过滤。
|
||||
|
||||
### 6.5 `order`
|
||||
|
||||
### 查询当前挂单
|
||||
|
||||
```bash
|
||||
ritmex-bot order open --exchange binance --symbol BTCUSDT
|
||||
```
|
||||
|
||||
### 创建订单
|
||||
|
||||
```bash
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000
|
||||
```
|
||||
|
||||
创建订单参数:
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `--side` | 是 | `buy` / `sell` |
|
||||
| `--type` | 是 | `limit` / `market` / `stop` / `trailing-stop` / `close` |
|
||||
| `--quantity` 或 `--qty` | 是 | 下单数量 |
|
||||
| `--price` | `limit` 必填 | 限价单价格 |
|
||||
| `--stop-price` | `stop` 必填 | 触发价 |
|
||||
| `--activation-price` | `trailing-stop` 必填 | 激活价 |
|
||||
| `--callback-rate` | `trailing-stop` 必填 | 回调比例 |
|
||||
| `--time-in-force` | 否 | `GTC` / `IOC` / `FOK` / `GTX` |
|
||||
| `--reduce-only` | 否 | `true/false` |
|
||||
| `--close-position` | 否 | `true/false` |
|
||||
| `--trigger-type` | 否 | `UNSPECIFIED` / `TAKE_PROFIT` / `STOP_LOSS` |
|
||||
| `--sl-price` | 否 | 止损价(路由层按交易所能力处理) |
|
||||
| `--tp-price` | 否 | 止盈价(路由层按交易所能力处理) |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
# 市价单
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type market --qty 0.01
|
||||
|
||||
# 止损单
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side sell --type stop --qty 0.01 --stop-price 86000
|
||||
|
||||
# 移动止损单
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side sell --type trailing-stop --qty 0.01 --activation-price 91000 --callback-rate 0.2
|
||||
|
||||
# close 语义单(默认会补全 reduceOnly/closePosition)
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side sell --type close --qty 0.01
|
||||
```
|
||||
|
||||
### 撤单
|
||||
|
||||
```bash
|
||||
ritmex-bot order cancel --exchange binance --symbol BTCUSDT --order-id 123456
|
||||
```
|
||||
|
||||
### 全撤
|
||||
|
||||
```bash
|
||||
ritmex-bot order cancel-all --exchange binance --symbol BTCUSDT
|
||||
```
|
||||
|
||||
若交易所支持 `forceCancelAllOrders`,会优先走该能力;否则回退普通批量撤单逻辑。
|
||||
|
||||
### 6.6 `strategy`
|
||||
|
||||
### 启动策略
|
||||
|
||||
```bash
|
||||
ritmex-bot strategy run --strategy maker --exchange standx --silent
|
||||
```
|
||||
|
||||
支持策略 ID:
|
||||
|
||||
- `trend`
|
||||
- `swing`
|
||||
- `guardian`
|
||||
- `maker`
|
||||
- `maker-points`
|
||||
- `offset-maker`
|
||||
- `liquidity-maker`
|
||||
- `basis`
|
||||
- `grid`
|
||||
|
||||
常用别名:
|
||||
|
||||
- `offset` -> `offset-maker`
|
||||
- `makerpoints` / `maker_points` -> `maker-points`
|
||||
- `liquidity` / `liquiditymaker` / `liquidity_maker` -> `liquidity-maker`
|
||||
|
||||
额外参数:
|
||||
|
||||
- `--silent`(短参数 `-q`)静默运行
|
||||
- `--dry-run` 传递到策略运行器,启用策略级模拟
|
||||
|
||||
## 7. `--dry-run` 模拟执行
|
||||
|
||||
当开启 `--dry-run`:
|
||||
|
||||
- `order create` / `order cancel` / `order cancel-all` 不会发送真实写操作。
|
||||
- 返回结果包含 `dryRunActions`,用于观察本次模拟会执行什么操作。
|
||||
- `strategy run` 会将 dry-run 模式传入策略执行链路。
|
||||
- 只读类命令(行情、查询)不会改变行为。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000 --dry-run --json
|
||||
```
|
||||
|
||||
## 8. 不支持能力的处理
|
||||
|
||||
不同交易所能力不完全一致。对于未实现或不支持的功能,CLI 会直接返回 `UNSUPPORTED` 错误,而不是静默忽略。
|
||||
|
||||
常见场景:
|
||||
|
||||
- 某些交易所不支持 `queryOpenOrders`
|
||||
- 某些交易所不支持 `queryAccountSnapshot`
|
||||
- 某些交易所不支持特定委托类型
|
||||
|
||||
建议调用方按错误码降级处理。
|
||||
|
||||
## 9. 输出格式与退出码
|
||||
|
||||
### 9.1 文本输出(默认)
|
||||
|
||||
成功:
|
||||
|
||||
```text
|
||||
[OK] market-ticker
|
||||
time: 2026-02-27T12:00:00.000Z
|
||||
exchange: binance
|
||||
symbol: BTCUSDT
|
||||
dryRun: false
|
||||
...
|
||||
```
|
||||
|
||||
失败:
|
||||
|
||||
```text
|
||||
[ERROR] order-open
|
||||
time: 2026-02-27T12:00:00.000Z
|
||||
code: UNSUPPORTED
|
||||
message: queryOpenOrders is not supported on exchange 'aster'
|
||||
retryable: false
|
||||
```
|
||||
|
||||
### 9.2 JSON 输出(`--json`)
|
||||
|
||||
成功结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"command": "market-ticker",
|
||||
"exchange": "binance",
|
||||
"symbol": "BTCUSDT",
|
||||
"dryRun": false,
|
||||
"ts": "2026-02-27T12:00:00.000Z",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
失败结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"command": "order-open",
|
||||
"exchange": "aster",
|
||||
"symbol": "BTCUSDT",
|
||||
"dryRun": false,
|
||||
"ts": "2026-02-27T12:00:00.000Z",
|
||||
"error": {
|
||||
"code": "UNSUPPORTED",
|
||||
"message": "queryOpenOrders is not supported on exchange 'aster'",
|
||||
"retryable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 退出码
|
||||
|
||||
| 退出码 | 含义 |
|
||||
| --- | --- |
|
||||
| `0` | 成功 |
|
||||
| `2` | 参数错误(`INVALID_ARGS`) |
|
||||
| `3` | 缺少环境配置(`MISSING_ENV`) |
|
||||
| `5` | 功能不支持(`UNSUPPORTED`) |
|
||||
| `6` | 交易所执行错误(`EXCHANGE_ERROR`) |
|
||||
| `7` | 超时(`TIMEOUT`) |
|
||||
|
||||
## 10. AI Agent 推荐调用流程
|
||||
|
||||
```bash
|
||||
# 1) 确认交易所能力
|
||||
ritmex-bot exchange capabilities --exchange binance --json
|
||||
|
||||
# 2) 拉取行情
|
||||
ritmex-bot market ticker --exchange binance --symbol BTCUSDT --json
|
||||
|
||||
# 3) dry-run 验证下单参数
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000 --dry-run --json
|
||||
|
||||
# 4) 确认后执行真实下单(去掉 --dry-run)
|
||||
ritmex-bot order create --exchange binance --symbol BTCUSDT --side buy --type limit --quantity 0.01 --price 90000 --json
|
||||
```
|
||||
|
||||
## 11. 与现有功能兼容性
|
||||
|
||||
命令模式不会破坏现有 Ink 交互模式与原有参数入口:
|
||||
|
||||
- `bun run index.ts` 仍进入交互菜单
|
||||
- `bun run index.ts --strategy trend --silent` 仍可直接跑策略
|
||||
- 仅当首参数匹配命令模式根命令时,才进入 `ritmex-bot` 命令执行路径
|
||||
|
||||
如果你已在使用现有环境变量与策略配置,可直接增量接入命令模式,无需迁移变量名。
|
||||
@@ -1,58 +0,0 @@
|
||||
# Ondo Perps 接入说明
|
||||
|
||||
## 链接
|
||||
|
||||
- 邀请注册:https://app.ondoperps.xyz/?ref=4A3ACQ
|
||||
- 官方文档索引:https://ondoperps.mintlify.app/llms.txt
|
||||
- REST OpenAPI:https://docs.ondoperps.xyz/api-reference/rest-spec.json
|
||||
- WebSocket OpenAPI:https://docs.ondoperps.xyz/api-reference/ws-spec.json
|
||||
- API Key 鉴权:https://docs.ondoperps.xyz/api-reference/api_key_authentication.md
|
||||
|
||||
## 配置
|
||||
|
||||
```bash
|
||||
EXCHANGE=ondoperps
|
||||
ONDOPERPS_API_KEY_ID=ondoKeyId_xxx
|
||||
ONDOPERPS_API_SECRET=ondoApiSecret_xxx
|
||||
ONDOPERPS_SYMBOL=BTC-USD.P
|
||||
```
|
||||
|
||||
Ondo Perps 市场使用 `{TICKER}-USD.P` 格式。默认值为 `BTC-USD.P`,其他示例包括 `ETH-USD.P`、`XAU-USD.P`、`NVDA-USD.P` 与 `AMD-USD.P`。
|
||||
|
||||
兼容入口:`EXCHANGE=ondoperp` 会解析为 `ondoperps`,旧 `ONDOPERP_*` 环境变量会在对应 `ONDOPERPS_*` 变量缺失时使用。
|
||||
|
||||
可选配置:
|
||||
|
||||
| 变量 | 说明 |
|
||||
| --- | --- |
|
||||
| `ONDOPERPS_SANDBOX` | `true` 时连接官方沙盒 REST 与 WebSocket 地址 |
|
||||
| `ONDOPERPS_BASE_URL` | 覆盖 REST API 地址 |
|
||||
| `ONDOPERPS_WS_URL` | 覆盖 WebSocket 地址 |
|
||||
| `ONDOPERPS_BUILDER_CODE` | Ondo 分配的 Builder Code |
|
||||
| `ONDOPERPS_BUILDER_FEE_RATE_BPS` | Builder 订单费率,适配器限制在 1–10 bps |
|
||||
|
||||
## 已接入能力
|
||||
|
||||
- API Key HMAC-SHA256 REST 鉴权
|
||||
- 限价单、Post Only 限价单、市价单、批量撤单与全部撤单
|
||||
- 仓位级止损与止盈,映射到统一订单类型
|
||||
- 账户余额、仓位、活动订单与市场精度查询
|
||||
- 深度、标记价、K 线、资金费率、订单、仓位与余额 WebSocket 订阅
|
||||
- REST 定时校准,覆盖 WebSocket 断线和私有频道鉴权异常场景
|
||||
- 生产与沙盒端点切换
|
||||
|
||||
## 鉴权规则
|
||||
|
||||
REST 请求发送以下请求头:
|
||||
|
||||
- `ONDO-KEY-ID`
|
||||
- `ONDO-TIMESTAMP`
|
||||
- `ONDO-SIGN`
|
||||
|
||||
签名内容为 `timestamp + uppercaseMethod + requestPathWithQuery + body`,使用 API Secret 进行 HMAC-SHA256 并输出十六进制字符串。
|
||||
|
||||
WebSocket 登录消息使用 API Key ID、毫秒时间戳与 `time + "ondo_perps_ws_login"` 的 HMAC-SHA256 签名。连接空闲限制为 180 秒,适配器每 30 秒发送一次 ping。
|
||||
|
||||
## 安全
|
||||
|
||||
API Secret 只通过运行环境传入。仓库文件、日志与错误信息均不保存请求签名头或凭证内容。建议在 Ondo Perps 后台配置固定 IPv4 白名单,并为 API Key 只授予策略需要的权限。
|
||||
+13
-24
@@ -1,21 +1,11 @@
|
||||
{
|
||||
"name": "ritmex-bot",
|
||||
"version": "0.1.0",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/discountry/ritmex-bot.git"
|
||||
},
|
||||
"bin": {
|
||||
"ritmex-bot": "./bin/ritmex-bot"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun run index.ts",
|
||||
"start": "bun run index.ts",
|
||||
"lint": "oxlint",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"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:watch": "bun x vitest",
|
||||
@@ -28,26 +18,25 @@
|
||||
"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": {
|
||||
"@types/bun": "^1.3.9",
|
||||
"oxlint": "^1.54.0",
|
||||
"vitest": "^4.0.18"
|
||||
"@types/bun": "latest",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grvt/client": "^1.6.27",
|
||||
"@nadohq/client": "^0.1.0-alpha.51",
|
||||
"@grvt/client": "^1.6.25",
|
||||
"@nadohq/client": "^0.1.0-alpha.45",
|
||||
"@noble/ed25519": "^3.0.0",
|
||||
"axios": "^1.13.6",
|
||||
"bignumber.js": "^10.0.2",
|
||||
"ccxt": "^4.5.40",
|
||||
"dotenv": "^17.3.1",
|
||||
"ethereum-cryptography": "^3.2.0",
|
||||
"ink": "^6.8.0",
|
||||
"axios": "^1.13.4",
|
||||
"bignumber.js": "^9.3.1",
|
||||
"ccxt": "^4.5.35",
|
||||
"dotenv": "^17.2.3",
|
||||
"ethereum-cryptography": "^2.2.1",
|
||||
"ink": "^6.6.0",
|
||||
"react": "^19.2.4",
|
||||
"trading-signals": "^7.4.3",
|
||||
"viem": "^2.46.3",
|
||||
"viem": "^2.45.1",
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
---
|
||||
name: use-ritmex-bot
|
||||
description: Use when the task requires operating exchanges with the ritmex-bot CLI, including capability checks, market/account/position queries, order operations, strategy run, dry-run simulation, and JSON output parsing.
|
||||
---
|
||||
|
||||
# Use ritmex-bot CLI
|
||||
|
||||
This skill is for running `ritmex-bot` in an agent-safe, exchange-compatible way.
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- The user asks to use `ritmex-bot` commands directly.
|
||||
- The user wants market data, account/position, or order operations from supported exchanges.
|
||||
- The user wants AI-agent-friendly CLI execution with `--json` output.
|
||||
- The user wants simulation with `--dry-run` before real writes.
|
||||
|
||||
## Hard Rules
|
||||
|
||||
1. Do not change environment-variable names and do not invent new env keys.
|
||||
2. Read config from the current shell environment as-is.
|
||||
3. Do not normalize or rewrite `--symbol`; pass it through exactly.
|
||||
4. If a feature is not supported by an exchange, return it as unsupported (`UNSUPPORTED`), do not fake behavior.
|
||||
5. For write actions (`order create`, `order cancel`, `order cancel-all`), prefer `--dry-run` first unless the user explicitly asks to skip simulation.
|
||||
6. Use `--json` whenever output must be consumed by another agent/tool.
|
||||
|
||||
## Command Entry Options
|
||||
|
||||
Use one of these:
|
||||
|
||||
```bash
|
||||
ritmex-bot <command>
|
||||
npx ritmex-bot <command>
|
||||
bunx ritmex-bot <command>
|
||||
bun run index.ts <command>
|
||||
```
|
||||
|
||||
If `ritmex-bot` is unavailable, use `bun run index.ts <command>` from repo root.
|
||||
|
||||
## Default Agent Workflow
|
||||
|
||||
1. Determine exchange and symbol from user request.
|
||||
2. If missing, rely on existing env resolution; do not create fallback env variables.
|
||||
3. Run capability precheck:
|
||||
- `exchange list`
|
||||
- `exchange capabilities --exchange <id>`
|
||||
4. For read operations, run command directly (prefer `--json`).
|
||||
5. For write operations:
|
||||
- Run the exact command with `--dry-run --json`.
|
||||
- Validate payload and dry-run actions.
|
||||
- Run live command only when user confirmed or explicitly requested live execution.
|
||||
6. Post-check with `order open`, `position list`, or `account snapshot` as needed.
|
||||
|
||||
## Global Flags
|
||||
|
||||
| Flag | Short | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `--exchange` | `-e` | Exchange override |
|
||||
| `--symbol` | - | Trading symbol (pass-through) |
|
||||
| `--json` | `-j` | JSON output |
|
||||
| `--dry-run` | `-d` | Simulate write ops |
|
||||
| `--timeout` | `-t` | Timeout in ms (default 25000) |
|
||||
| `--help` | `-h` | Show help |
|
||||
|
||||
## Root Commands
|
||||
|
||||
- `help`
|
||||
- `doctor`
|
||||
- `exchange`
|
||||
- `market`
|
||||
- `account`
|
||||
- `position`
|
||||
- `order`
|
||||
- `strategy`
|
||||
|
||||
## Command Reference
|
||||
|
||||
### `doctor`
|
||||
|
||||
```bash
|
||||
ritmex-bot doctor
|
||||
ritmex-bot doctor --exchange binance --symbol BTCUSDT --json
|
||||
```
|
||||
|
||||
Returns effective setup and runtime capabilities.
|
||||
|
||||
### `exchange`
|
||||
|
||||
```bash
|
||||
ritmex-bot exchange list
|
||||
ritmex-bot exchange capabilities --exchange standx
|
||||
```
|
||||
|
||||
If runtime adapter cannot initialize, capabilities may fallback to static metadata.
|
||||
|
||||
### `market`
|
||||
|
||||
```bash
|
||||
ritmex-bot market ticker --exchange <id> --symbol <symbol>
|
||||
ritmex-bot market depth --exchange <id> --symbol <symbol> --levels 10
|
||||
ritmex-bot market kline --exchange <id> --symbol <symbol> --interval 1m --limit 100
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `kline` requires `--interval`.
|
||||
- `depth --levels` is optional.
|
||||
- `kline --limit` is optional.
|
||||
|
||||
### `account`
|
||||
|
||||
```bash
|
||||
ritmex-bot account snapshot --exchange <id>
|
||||
ritmex-bot account summary --exchange <id>
|
||||
```
|
||||
|
||||
`summary` is an alias of `snapshot`.
|
||||
|
||||
### `position`
|
||||
|
||||
```bash
|
||||
ritmex-bot position list --exchange <id>
|
||||
ritmex-bot position list --exchange <id> --symbol <symbol>
|
||||
```
|
||||
|
||||
### `order`
|
||||
|
||||
### Query open orders
|
||||
|
||||
```bash
|
||||
ritmex-bot order open --exchange <id> --symbol <symbol>
|
||||
```
|
||||
|
||||
### Create order
|
||||
|
||||
```bash
|
||||
ritmex-bot order create --exchange <id> --symbol <symbol> --side buy --type limit --quantity 0.01 --price 90000
|
||||
```
|
||||
|
||||
Required:
|
||||
|
||||
- `--side` = `buy|sell`
|
||||
- `--type` = `limit|market|stop|trailing-stop|close`
|
||||
- `--quantity` or `--qty`
|
||||
|
||||
Conditional required:
|
||||
|
||||
- `limit`: `--price`
|
||||
- `stop`: `--stop-price`
|
||||
- `trailing-stop`: `--activation-price` and `--callback-rate`
|
||||
|
||||
Optional:
|
||||
|
||||
- `--time-in-force` (`GTC|IOC|FOK|GTX`)
|
||||
- `--reduce-only` (`true|false`)
|
||||
- `--close-position` (`true|false`)
|
||||
- `--trigger-type` (`UNSPECIFIED|TAKE_PROFIT|STOP_LOSS`)
|
||||
- `--sl-price`
|
||||
- `--tp-price`
|
||||
|
||||
### Cancel one order
|
||||
|
||||
```bash
|
||||
ritmex-bot order cancel --exchange <id> --symbol <symbol> --order-id <id>
|
||||
```
|
||||
|
||||
### Cancel all
|
||||
|
||||
```bash
|
||||
ritmex-bot order cancel-all --exchange <id> --symbol <symbol>
|
||||
```
|
||||
|
||||
### `strategy`
|
||||
|
||||
```bash
|
||||
ritmex-bot strategy run --strategy maker --exchange standx --silent
|
||||
ritmex-bot strategy run --strategy offset --exchange binance --dry-run
|
||||
```
|
||||
|
||||
Supported strategy IDs:
|
||||
|
||||
- `trend`
|
||||
- `swing`
|
||||
- `guardian`
|
||||
- `maker`
|
||||
- `maker-points`
|
||||
- `offset-maker`
|
||||
- `liquidity-maker`
|
||||
- `basis`
|
||||
- `grid`
|
||||
|
||||
Aliases:
|
||||
|
||||
- `offset` -> `offset-maker`
|
||||
- `makerpoints` / `maker_points` -> `maker-points`
|
||||
- `liquidity` / `liquiditymaker` / `liquidity_maker` -> `liquidity-maker`
|
||||
|
||||
Extra flags:
|
||||
|
||||
- `--silent` (short alias `-q`)
|
||||
- `--dry-run`
|
||||
|
||||
## Dry-Run First Patterns
|
||||
|
||||
### Create order safely
|
||||
|
||||
```bash
|
||||
# 1) Simulate
|
||||
ritmex-bot order create --exchange <id> --symbol <symbol> --side buy --type limit --quantity 0.01 --price 90000 --dry-run --json
|
||||
|
||||
# 2) Execute live only after confirmation
|
||||
ritmex-bot order create --exchange <id> --symbol <symbol> --side buy --type limit --quantity 0.01 --price 90000 --json
|
||||
```
|
||||
|
||||
### Cancel safely
|
||||
|
||||
```bash
|
||||
# 1) Simulate
|
||||
ritmex-bot order cancel --exchange <id> --symbol <symbol> --order-id <id> --dry-run --json
|
||||
|
||||
# 2) Execute live
|
||||
ritmex-bot order cancel --exchange <id> --symbol <symbol> --order-id <id> --json
|
||||
```
|
||||
|
||||
## Agent Output Handling
|
||||
|
||||
Prefer `--json` and parse:
|
||||
|
||||
- `success` (boolean)
|
||||
- `command` (executed command kind)
|
||||
- `exchange`
|
||||
- `symbol`
|
||||
- `dryRun`
|
||||
- `data` (success payload)
|
||||
- `error.code`, `error.message`, `error.retryable` (failure payload)
|
||||
|
||||
Human-readable mode is fine for manual terminal use; `--json` is preferred for automation.
|
||||
|
||||
## Error Codes and Exit Codes
|
||||
|
||||
Map failures by code/exit code:
|
||||
|
||||
- `INVALID_ARGS` -> exit `2`
|
||||
- `MISSING_ENV` -> exit `3`
|
||||
- `UNSUPPORTED` -> exit `5`
|
||||
- `EXCHANGE_ERROR` -> exit `6`
|
||||
- `TIMEOUT` -> exit `7`
|
||||
|
||||
Handling policy:
|
||||
|
||||
1. `INVALID_ARGS`: fix command arguments and retry once.
|
||||
2. `MISSING_ENV`: report missing configuration; do not invent env keys.
|
||||
3. `UNSUPPORTED`: return clearly as unsupported for that exchange.
|
||||
4. `EXCHANGE_ERROR`: return details and retry only if user requests.
|
||||
5. `TIMEOUT`: optionally retry with larger `--timeout` once.
|
||||
|
||||
## Symbol and Exchange-Specific Behavior
|
||||
|
||||
- Never apply cross-exchange symbol mapping inside the skill.
|
||||
- Respect user-provided symbols exactly (examples: `BTCUSDT`, `BTCUSDC`, `BTC_USD_PERP`, `BTC-PERP`).
|
||||
- If no `--symbol` is provided, let existing exchange config resolve it.
|
||||
- If no `--exchange` is provided, let existing env resolution decide it.
|
||||
|
||||
## Ready-to-Use Recipes
|
||||
|
||||
### Preflight
|
||||
|
||||
```bash
|
||||
ritmex-bot exchange list --json
|
||||
ritmex-bot exchange capabilities --exchange <id> --json
|
||||
ritmex-bot doctor --exchange <id> --symbol <symbol> --json
|
||||
```
|
||||
|
||||
### Read-only market/account state
|
||||
|
||||
```bash
|
||||
ritmex-bot market ticker --exchange <id> --symbol <symbol> --json
|
||||
ritmex-bot market depth --exchange <id> --symbol <symbol> --levels 20 --json
|
||||
ritmex-bot market kline --exchange <id> --symbol <symbol> --interval 1m --limit 120 --json
|
||||
ritmex-bot account snapshot --exchange <id> --json
|
||||
ritmex-bot position list --exchange <id> --symbol <symbol> --json
|
||||
```
|
||||
|
||||
### Write flow (safe)
|
||||
|
||||
```bash
|
||||
ritmex-bot order create --exchange <id> --symbol <symbol> --side buy --type market --qty 0.01 --dry-run --json
|
||||
ritmex-bot order create --exchange <id> --symbol <symbol> --side buy --type market --qty 0.01 --json
|
||||
ritmex-bot order open --exchange <id> --symbol <symbol> --json
|
||||
```
|
||||
|
||||
### Strategy flow
|
||||
|
||||
```bash
|
||||
ritmex-bot strategy run --strategy trend --exchange <id> --dry-run
|
||||
ritmex-bot strategy run --strategy trend --exchange <id> --silent
|
||||
```
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
Before returning results to user:
|
||||
|
||||
1. Confirm command and parameters used.
|
||||
2. Confirm whether run was `dryRun` or live.
|
||||
3. For live writes, provide immediate post-check output (`order open` / `position list`).
|
||||
4. If unsupported, explicitly name exchange + unsupported method.
|
||||
5. If failed, return error code/message and next corrective action.
|
||||
+1
-8
@@ -88,8 +88,6 @@ function assignExchange(options: CliOptions, raw: string): void {
|
||||
options.exchange = normalized as CliOptions["exchange"];
|
||||
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
|
||||
options.exchange = "grvt";
|
||||
} else if (normalized === "ondo" || normalized === "ondoperp") {
|
||||
options.exchange = "ondoperps";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,10 +101,5 @@ export function printCliHelp(): void {
|
||||
` Aliases: liquidity, liquidity-maker for the liquidity maker engine.\n` +
|
||||
` --exchange, -e Choose exchange. Overrides EXCHANGE/TRADE_EXCHANGE environment variables.\n` +
|
||||
` --silent, -q Reduce console output. When used with --strategy, runs in silent daemon mode.\n` +
|
||||
` --help, -h Show this help message.\n\n` +
|
||||
`Command mode:\n` +
|
||||
` ritmex-bot doctor\n` +
|
||||
` ritmex-bot exchange list\n` +
|
||||
` ritmex-bot market ticker --exchange <id> --symbol <symbol>\n` +
|
||||
` ritmex-bot order create --side buy --type limit --quantity 0.01 --price 100000 --dry-run\n`);
|
||||
` --help, -h Show this help message.\n`);
|
||||
}
|
||||
|
||||
@@ -1,679 +0,0 @@
|
||||
import { resolveSymbolFromEnv } from "../config";
|
||||
import { type ExchangeAdapter } from "../exchanges/adapter";
|
||||
import { DryRunExchangeAdapter } from "../exchanges/dry-run-adapter";
|
||||
import {
|
||||
SUPPORTED_EXCHANGE_IDS,
|
||||
getExchangeDisplayName,
|
||||
resolveExchangeId,
|
||||
type SupportedExchangeId,
|
||||
} from "../exchanges/create-adapter";
|
||||
import {
|
||||
routeCloseOrder,
|
||||
routeLimitOrder,
|
||||
routeMarketOrder,
|
||||
routeStopOrder,
|
||||
routeTrailingStopOrder,
|
||||
} from "../exchanges/order-router";
|
||||
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||
import type { Depth, Kline, Order, Ticker } from "../exchanges/types";
|
||||
import { startStrategy } from "./strategy-runner";
|
||||
import type {
|
||||
CommandErrorPayload,
|
||||
CommandExecutionResult,
|
||||
CommandFailurePayload,
|
||||
CommandPayload,
|
||||
CommandSuccessPayload,
|
||||
ParsedCliCommand,
|
||||
} from "./command-types";
|
||||
|
||||
const EXIT_CODE_SUCCESS = 0;
|
||||
const EXIT_CODE_INVALID_ARGS = 2;
|
||||
const EXIT_CODE_MISSING_ENV = 3;
|
||||
const EXIT_CODE_UNSUPPORTED = 5;
|
||||
const EXIT_CODE_EXCHANGE_ERROR = 6;
|
||||
const EXIT_CODE_TIMEOUT = 7;
|
||||
|
||||
const STATIC_CAPABILITIES: Record<
|
||||
SupportedExchangeId,
|
||||
{
|
||||
trailingStops: boolean | "conditional";
|
||||
fundingRate: boolean;
|
||||
precision: boolean;
|
||||
queryOpenOrders: boolean;
|
||||
queryAccountSnapshot: boolean;
|
||||
changeMarginMode: boolean;
|
||||
forceCancelAllOrders: boolean;
|
||||
}
|
||||
> = {
|
||||
aster: {
|
||||
trailingStops: true,
|
||||
fundingRate: false,
|
||||
precision: true,
|
||||
queryOpenOrders: false,
|
||||
queryAccountSnapshot: false,
|
||||
changeMarginMode: false,
|
||||
forceCancelAllOrders: false,
|
||||
},
|
||||
grvt: {
|
||||
trailingStops: false,
|
||||
fundingRate: false,
|
||||
precision: false,
|
||||
queryOpenOrders: false,
|
||||
queryAccountSnapshot: false,
|
||||
changeMarginMode: false,
|
||||
forceCancelAllOrders: false,
|
||||
},
|
||||
lighter: {
|
||||
trailingStops: false,
|
||||
fundingRate: false,
|
||||
precision: true,
|
||||
queryOpenOrders: false,
|
||||
queryAccountSnapshot: false,
|
||||
changeMarginMode: false,
|
||||
forceCancelAllOrders: false,
|
||||
},
|
||||
backpack: {
|
||||
trailingStops: false,
|
||||
fundingRate: false,
|
||||
precision: false,
|
||||
queryOpenOrders: false,
|
||||
queryAccountSnapshot: false,
|
||||
changeMarginMode: false,
|
||||
forceCancelAllOrders: false,
|
||||
},
|
||||
paradex: {
|
||||
trailingStops: false,
|
||||
fundingRate: false,
|
||||
precision: false,
|
||||
queryOpenOrders: false,
|
||||
queryAccountSnapshot: false,
|
||||
changeMarginMode: false,
|
||||
forceCancelAllOrders: false,
|
||||
},
|
||||
nado: {
|
||||
trailingStops: false,
|
||||
fundingRate: true,
|
||||
precision: true,
|
||||
queryOpenOrders: false,
|
||||
queryAccountSnapshot: false,
|
||||
changeMarginMode: false,
|
||||
forceCancelAllOrders: false,
|
||||
},
|
||||
standx: {
|
||||
trailingStops: false,
|
||||
fundingRate: true,
|
||||
precision: true,
|
||||
queryOpenOrders: true,
|
||||
queryAccountSnapshot: true,
|
||||
changeMarginMode: true,
|
||||
forceCancelAllOrders: true,
|
||||
},
|
||||
binance: {
|
||||
trailingStops: "conditional",
|
||||
fundingRate: true,
|
||||
precision: true,
|
||||
queryOpenOrders: true,
|
||||
queryAccountSnapshot: true,
|
||||
changeMarginMode: true,
|
||||
forceCancelAllOrders: true,
|
||||
},
|
||||
ondoperps: {
|
||||
trailingStops: false,
|
||||
fundingRate: true,
|
||||
precision: true,
|
||||
queryOpenOrders: true,
|
||||
queryAccountSnapshot: true,
|
||||
changeMarginMode: false,
|
||||
forceCancelAllOrders: true,
|
||||
},
|
||||
};
|
||||
|
||||
export interface CommandExecutorDependencies {
|
||||
buildAdapterFromEnvFn?: typeof buildAdapterFromEnv;
|
||||
startStrategyFn?: typeof startStrategy;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
class CommandExecutionError extends Error {
|
||||
constructor(
|
||||
readonly code: CommandErrorPayload["code"],
|
||||
readonly exitCode: number,
|
||||
message: string,
|
||||
readonly retryable: boolean = false,
|
||||
readonly details?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = "CommandExecutionError";
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeCliCommand(
|
||||
command: ParsedCliCommand,
|
||||
deps: CommandExecutorDependencies = {}
|
||||
): Promise<CommandExecutionResult> {
|
||||
const buildAdapterFromEnvFn = deps.buildAdapterFromEnvFn ?? buildAdapterFromEnv;
|
||||
const startStrategyFn = deps.startStrategyFn ?? startStrategy;
|
||||
const now = deps.now ?? (() => Date.now());
|
||||
|
||||
try {
|
||||
const data = await withExchangeOverride(command.exchange, async () => {
|
||||
switch (command.kind) {
|
||||
case "help":
|
||||
return { topic: command.topic ?? null };
|
||||
case "doctor":
|
||||
return handleDoctor(command, buildAdapterFromEnvFn);
|
||||
case "exchange-list":
|
||||
return {
|
||||
exchanges: SUPPORTED_EXCHANGE_IDS.map((id) => ({
|
||||
id,
|
||||
name: getExchangeDisplayName(id),
|
||||
})),
|
||||
};
|
||||
case "exchange-capabilities":
|
||||
return handleExchangeCapabilities(command, buildAdapterFromEnvFn);
|
||||
case "market-ticker":
|
||||
return handleMarketTicker(command, buildAdapterFromEnvFn);
|
||||
case "market-depth":
|
||||
return handleMarketDepth(command, buildAdapterFromEnvFn);
|
||||
case "market-kline":
|
||||
return handleMarketKline(command, buildAdapterFromEnvFn);
|
||||
case "account-snapshot":
|
||||
return handleAccountSnapshot(command, buildAdapterFromEnvFn);
|
||||
case "position-list":
|
||||
return handlePositionList(command, buildAdapterFromEnvFn);
|
||||
case "order-open":
|
||||
return handleOrderOpen(command, buildAdapterFromEnvFn);
|
||||
case "order-create":
|
||||
return handleOrderCreate(command, buildAdapterFromEnvFn);
|
||||
case "order-cancel":
|
||||
return handleOrderCancel(command, buildAdapterFromEnvFn);
|
||||
case "order-cancel-all":
|
||||
return handleOrderCancelAll(command, buildAdapterFromEnvFn);
|
||||
case "strategy-run":
|
||||
await startStrategyFn(command.strategy, { silent: command.silent, dryRun: command.dryRun });
|
||||
return {
|
||||
strategy: command.strategy,
|
||||
status: "stopped",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const payload = successPayload(command, now(), data);
|
||||
return {
|
||||
exitCode: EXIT_CODE_SUCCESS,
|
||||
payload,
|
||||
forceExit: command.kind !== "strategy-run",
|
||||
};
|
||||
} catch (error) {
|
||||
const mapped = mapToCommandExecutionError(error);
|
||||
const payload = failurePayload(command, now(), mapped);
|
||||
return {
|
||||
exitCode: mapped.exitCode,
|
||||
payload,
|
||||
forceExit: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function renderCommandPayload(payload: CommandPayload, json: boolean): string {
|
||||
if (json) {
|
||||
return JSON.stringify(payload, null, 2);
|
||||
}
|
||||
if (payload.success) {
|
||||
return formatHumanSuccess(payload);
|
||||
}
|
||||
return formatHumanError(payload);
|
||||
}
|
||||
|
||||
async function handleDoctor(
|
||||
command: Extract<ParsedCliCommand, { kind: "doctor" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const exchange = resolveEffectiveExchange(command.exchange);
|
||||
const symbol = resolveEffectiveSymbol(command.symbol, exchange);
|
||||
const adapter = createAdapter(buildAdapterFromEnvFn, exchange, symbol);
|
||||
return {
|
||||
exchange,
|
||||
exchangeName: getExchangeDisplayName(exchange),
|
||||
symbol,
|
||||
adapterId: adapter.id,
|
||||
capabilities: runtimeCapabilities(adapter),
|
||||
};
|
||||
}
|
||||
|
||||
async function handleExchangeCapabilities(
|
||||
command: Extract<ParsedCliCommand, { kind: "exchange-capabilities" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const exchange = resolveEffectiveExchange(command.exchange);
|
||||
const symbol = resolveEffectiveSymbol(command.symbol, exchange);
|
||||
const staticCapabilities = STATIC_CAPABILITIES[exchange];
|
||||
|
||||
try {
|
||||
const adapter = createAdapter(buildAdapterFromEnvFn, exchange, symbol);
|
||||
return {
|
||||
exchange,
|
||||
exchangeName: getExchangeDisplayName(exchange),
|
||||
symbol,
|
||||
capabilities: runtimeCapabilities(adapter),
|
||||
source: "runtime",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
exchange,
|
||||
exchangeName: getExchangeDisplayName(exchange),
|
||||
symbol,
|
||||
capabilities: staticCapabilities,
|
||||
source: "static",
|
||||
warning: extractMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMarketTicker(
|
||||
command: Extract<ParsedCliCommand, { kind: "market-ticker" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
const ticker = await waitForFirst<Ticker>(
|
||||
(cb) => adapter.watchTicker(symbol, cb),
|
||||
command.timeoutMs,
|
||||
"market ticker"
|
||||
);
|
||||
return { exchange, symbol, ticker };
|
||||
}
|
||||
|
||||
async function handleMarketDepth(
|
||||
command: Extract<ParsedCliCommand, { kind: "market-depth" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
const depth = await waitForFirst<Depth>(
|
||||
(cb) => adapter.watchDepth(symbol, cb),
|
||||
command.timeoutMs,
|
||||
"market depth"
|
||||
);
|
||||
const levels = command.levels && command.levels > 0 ? Math.floor(command.levels) : undefined;
|
||||
const boundedDepth = levels
|
||||
? {
|
||||
...depth,
|
||||
bids: depth.bids.slice(0, levels),
|
||||
asks: depth.asks.slice(0, levels),
|
||||
}
|
||||
: depth;
|
||||
return { exchange, symbol, levels: levels ?? null, depth: boundedDepth };
|
||||
}
|
||||
|
||||
async function handleMarketKline(
|
||||
command: Extract<ParsedCliCommand, { kind: "market-kline" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
const klines = await waitForFirst<Kline[]>(
|
||||
(cb) => adapter.watchKlines(symbol, command.interval, cb),
|
||||
command.timeoutMs,
|
||||
"market kline"
|
||||
);
|
||||
const limit = command.limit && command.limit > 0 ? Math.floor(command.limit) : undefined;
|
||||
const data = limit ? klines.slice(-limit) : klines;
|
||||
return { exchange, symbol, interval: command.interval, limit: limit ?? null, klines: data };
|
||||
}
|
||||
|
||||
async function handleAccountSnapshot(
|
||||
command: Extract<ParsedCliCommand, { kind: "account-snapshot" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
if (!adapter.queryAccountSnapshot) {
|
||||
throw new CommandExecutionError(
|
||||
"UNSUPPORTED",
|
||||
EXIT_CODE_UNSUPPORTED,
|
||||
`queryAccountSnapshot is not supported on exchange '${exchange}'`
|
||||
);
|
||||
}
|
||||
const snapshot = await adapter.queryAccountSnapshot();
|
||||
return { exchange, symbol, snapshot };
|
||||
}
|
||||
|
||||
async function handlePositionList(
|
||||
command: Extract<ParsedCliCommand, { kind: "position-list" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
if (!adapter.queryAccountSnapshot) {
|
||||
throw new CommandExecutionError(
|
||||
"UNSUPPORTED",
|
||||
EXIT_CODE_UNSUPPORTED,
|
||||
`queryAccountSnapshot is not supported on exchange '${exchange}'`
|
||||
);
|
||||
}
|
||||
const snapshot = await adapter.queryAccountSnapshot();
|
||||
const positions = snapshot?.positions ?? [];
|
||||
const filtered = symbol ? positions.filter((position) => position.symbol === symbol) : positions;
|
||||
return { exchange, symbol, positions: filtered };
|
||||
}
|
||||
|
||||
async function handleOrderOpen(
|
||||
command: Extract<ParsedCliCommand, { kind: "order-open" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
if (!adapter.queryOpenOrders) {
|
||||
throw new CommandExecutionError(
|
||||
"UNSUPPORTED",
|
||||
EXIT_CODE_UNSUPPORTED,
|
||||
`queryOpenOrders is not supported on exchange '${exchange}'`
|
||||
);
|
||||
}
|
||||
const orders = await adapter.queryOpenOrders();
|
||||
const filtered = symbol ? orders.filter((order) => order.symbol === symbol) : orders;
|
||||
return { exchange, symbol, orders: filtered };
|
||||
}
|
||||
|
||||
async function handleOrderCreate(
|
||||
command: Extract<ParsedCliCommand, { kind: "order-create" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol, dryRunAdapter } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
const execAdapter = dryRunAdapter ?? adapter;
|
||||
const payload = command.payload;
|
||||
|
||||
const baseIntent = {
|
||||
adapter: execAdapter,
|
||||
symbol,
|
||||
side: payload.side,
|
||||
quantity: payload.quantity,
|
||||
reduceOnly: payload.reduceOnly,
|
||||
closePosition: payload.closePosition,
|
||||
timeInForce: payload.timeInForce,
|
||||
};
|
||||
|
||||
let order: Order;
|
||||
switch (payload.type) {
|
||||
case "limit":
|
||||
order = await routeLimitOrder({
|
||||
...baseIntent,
|
||||
price: payload.price!,
|
||||
slPrice: payload.slPrice,
|
||||
tpPrice: payload.tpPrice,
|
||||
});
|
||||
break;
|
||||
case "market":
|
||||
order = await routeMarketOrder(baseIntent);
|
||||
break;
|
||||
case "stop":
|
||||
order = await routeStopOrder({
|
||||
...baseIntent,
|
||||
stopPrice: payload.stopPrice!,
|
||||
triggerType: payload.triggerType,
|
||||
});
|
||||
break;
|
||||
case "trailing-stop":
|
||||
order = await routeTrailingStopOrder({
|
||||
...baseIntent,
|
||||
activationPrice: payload.activationPrice!,
|
||||
callbackRate: payload.callbackRate!,
|
||||
});
|
||||
break;
|
||||
case "close":
|
||||
order = await routeCloseOrder({
|
||||
...baseIntent,
|
||||
reduceOnly: payload.reduceOnly ?? true,
|
||||
closePosition: payload.closePosition ?? true,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new CommandExecutionError("INVALID_ARGS", EXIT_CODE_INVALID_ARGS, "Unsupported order type");
|
||||
}
|
||||
|
||||
return {
|
||||
exchange,
|
||||
symbol,
|
||||
payload,
|
||||
order,
|
||||
dryRunActions: dryRunAdapter?.actions ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async function handleOrderCancel(
|
||||
command: Extract<ParsedCliCommand, { kind: "order-cancel" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol, dryRunAdapter } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
const execAdapter = dryRunAdapter ?? adapter;
|
||||
await execAdapter.cancelOrder({ symbol, orderId: command.orderId });
|
||||
return {
|
||||
exchange,
|
||||
symbol,
|
||||
orderId: command.orderId,
|
||||
dryRunActions: dryRunAdapter?.actions ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async function handleOrderCancelAll(
|
||||
command: Extract<ParsedCliCommand, { kind: "order-cancel-all" }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): Promise<unknown> {
|
||||
const { adapter, exchange, symbol, dryRunAdapter } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||
const execAdapter = dryRunAdapter ?? adapter;
|
||||
let forced = false;
|
||||
let forceResult: boolean | null = null;
|
||||
|
||||
if (execAdapter.forceCancelAllOrders) {
|
||||
forced = true;
|
||||
forceResult = await execAdapter.forceCancelAllOrders();
|
||||
} else {
|
||||
await execAdapter.cancelAllOrders({ symbol });
|
||||
}
|
||||
|
||||
return {
|
||||
exchange,
|
||||
symbol,
|
||||
forced,
|
||||
forceResult,
|
||||
dryRunActions: dryRunAdapter?.actions ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function createAdapterContext(
|
||||
command: Extract<ParsedCliCommand, { kind: Exclude<ParsedCliCommand["kind"], "help" | "exchange-list"> }>,
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||
): {
|
||||
exchange: SupportedExchangeId;
|
||||
symbol: string;
|
||||
adapter: ExchangeAdapter;
|
||||
dryRunAdapter?: DryRunExchangeAdapter;
|
||||
} {
|
||||
const exchange = resolveEffectiveExchange(command.exchange);
|
||||
const symbol = resolveEffectiveSymbol(command.symbol, exchange);
|
||||
const adapter = createAdapter(buildAdapterFromEnvFn, exchange, symbol);
|
||||
if (!command.dryRun) {
|
||||
return { exchange, symbol, adapter };
|
||||
}
|
||||
const dryRunAdapter = new DryRunExchangeAdapter(adapter);
|
||||
return { exchange, symbol, adapter, dryRunAdapter };
|
||||
}
|
||||
|
||||
function createAdapter(
|
||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv,
|
||||
exchange: SupportedExchangeId,
|
||||
symbol: string
|
||||
): ExchangeAdapter {
|
||||
return buildAdapterFromEnvFn({
|
||||
exchangeId: exchange,
|
||||
symbol,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveEffectiveExchange(explicit?: SupportedExchangeId): SupportedExchangeId {
|
||||
if (explicit) return explicit;
|
||||
return resolveExchangeId();
|
||||
}
|
||||
|
||||
function resolveEffectiveSymbol(explicit: string | undefined, exchange: SupportedExchangeId): string {
|
||||
if (explicit && explicit.trim()) {
|
||||
return explicit.trim();
|
||||
}
|
||||
return resolveSymbolFromEnv(exchange);
|
||||
}
|
||||
|
||||
function runtimeCapabilities(adapter: ExchangeAdapter): unknown {
|
||||
return {
|
||||
trailingStops: adapter.supportsTrailingStops(),
|
||||
fundingRate: typeof adapter.watchFundingRate === "function",
|
||||
precision: typeof adapter.getPrecision === "function",
|
||||
queryOpenOrders: typeof adapter.queryOpenOrders === "function",
|
||||
queryAccountSnapshot: typeof adapter.queryAccountSnapshot === "function",
|
||||
changeMarginMode: typeof adapter.changeMarginMode === "function",
|
||||
forceCancelAllOrders: typeof adapter.forceCancelAllOrders === "function",
|
||||
};
|
||||
}
|
||||
|
||||
function successPayload(command: ParsedCliCommand, nowMs: number, data: unknown): CommandSuccessPayload {
|
||||
return {
|
||||
success: true,
|
||||
command: command.kind,
|
||||
exchange: command.exchange,
|
||||
symbol: command.symbol,
|
||||
dryRun: command.dryRun,
|
||||
ts: new Date(nowMs).toISOString(),
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
function failurePayload(
|
||||
command: ParsedCliCommand,
|
||||
nowMs: number,
|
||||
error: CommandExecutionError
|
||||
): CommandFailurePayload {
|
||||
return {
|
||||
success: false,
|
||||
command: command.kind,
|
||||
exchange: command.exchange,
|
||||
symbol: command.symbol,
|
||||
dryRun: command.dryRun,
|
||||
ts: new Date(nowMs).toISOString(),
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
retryable: error.retryable,
|
||||
details: error.details,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mapToCommandExecutionError(error: unknown): CommandExecutionError {
|
||||
if (error instanceof CommandExecutionError) {
|
||||
return error;
|
||||
}
|
||||
const message = extractMessage(error);
|
||||
const lower = message.toLowerCase();
|
||||
|
||||
if (lower.includes("timeout")) {
|
||||
return new CommandExecutionError("TIMEOUT", EXIT_CODE_TIMEOUT, message, true);
|
||||
}
|
||||
if (lower.includes("unsupported") || lower.includes("not supported")) {
|
||||
return new CommandExecutionError("UNSUPPORTED", EXIT_CODE_UNSUPPORTED, message);
|
||||
}
|
||||
if (lower.includes("missing") && lower.includes("environment")) {
|
||||
return new CommandExecutionError("MISSING_ENV", EXIT_CODE_MISSING_ENV, message);
|
||||
}
|
||||
if (lower.includes("missing ") || lower.includes("required option")) {
|
||||
return new CommandExecutionError("INVALID_ARGS", EXIT_CODE_INVALID_ARGS, message);
|
||||
}
|
||||
return new CommandExecutionError("EXCHANGE_ERROR", EXIT_CODE_EXCHANGE_ERROR, message, true);
|
||||
}
|
||||
|
||||
function formatHumanSuccess(payload: CommandSuccessPayload): string {
|
||||
const lines = [
|
||||
`[OK] ${payload.command}`,
|
||||
`time: ${payload.ts}`,
|
||||
payload.exchange ? `exchange: ${payload.exchange}` : null,
|
||||
payload.symbol ? `symbol: ${payload.symbol}` : null,
|
||||
`dryRun: ${payload.dryRun ? "true" : "false"}`,
|
||||
"",
|
||||
safeJsonStringify(payload.data),
|
||||
].filter(Boolean) as string[];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatHumanError(payload: CommandFailurePayload): string {
|
||||
return [
|
||||
`[ERROR] ${payload.command}`,
|
||||
`time: ${payload.ts}`,
|
||||
`code: ${payload.error.code}`,
|
||||
`message: ${payload.error.message}`,
|
||||
payload.error.retryable != null ? `retryable: ${payload.error.retryable ? "true" : "false"}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function safeJsonStringify(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForFirst<T>(
|
||||
subscribe: (cb: (value: T) => void) => void,
|
||||
timeoutMs: number,
|
||||
context: string
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(
|
||||
new CommandExecutionError(
|
||||
"TIMEOUT",
|
||||
EXIT_CODE_TIMEOUT,
|
||||
`${context} timed out after ${timeoutMs}ms`,
|
||||
true
|
||||
)
|
||||
);
|
||||
}, timeoutMs);
|
||||
|
||||
try {
|
||||
subscribe((value) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(value);
|
||||
});
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function withExchangeOverride<T>(
|
||||
explicitExchange: SupportedExchangeId | undefined,
|
||||
task: () => Promise<T>
|
||||
): Promise<T> {
|
||||
if (!explicitExchange) {
|
||||
return task();
|
||||
}
|
||||
const prevExchange = process.env.EXCHANGE;
|
||||
const prevTradeExchange = process.env.TRADE_EXCHANGE;
|
||||
process.env.EXCHANGE = explicitExchange;
|
||||
process.env.TRADE_EXCHANGE = explicitExchange;
|
||||
try {
|
||||
return await task();
|
||||
} finally {
|
||||
if (prevExchange == null) {
|
||||
delete process.env.EXCHANGE;
|
||||
} else {
|
||||
process.env.EXCHANGE = prevExchange;
|
||||
}
|
||||
if (prevTradeExchange == null) {
|
||||
delete process.env.TRADE_EXCHANGE;
|
||||
} else {
|
||||
process.env.TRADE_EXCHANGE = prevTradeExchange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
@@ -1,528 +0,0 @@
|
||||
import { isSupportedExchangeId, type SupportedExchangeId } from "../exchanges/create-adapter";
|
||||
import type { StrategyId } from "./args";
|
||||
import type { CommandCommonOptions, OrderCreatePayload, OrderCreateType, ParsedCliCommand } from "./command-types";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 25_000;
|
||||
const ROOT_COMMANDS = new Set([
|
||||
"help",
|
||||
"doctor",
|
||||
"exchange",
|
||||
"market",
|
||||
"account",
|
||||
"position",
|
||||
"order",
|
||||
"strategy",
|
||||
]);
|
||||
|
||||
const GLOBAL_OPTION_NAMES = new Set([
|
||||
"exchange",
|
||||
"symbol",
|
||||
"json",
|
||||
"dry-run",
|
||||
"timeout",
|
||||
"help",
|
||||
]);
|
||||
|
||||
const SHORT_OPTION_ALIAS: Record<string, string> = {
|
||||
d: "dry-run",
|
||||
e: "exchange",
|
||||
h: "help",
|
||||
j: "json",
|
||||
q: "silent",
|
||||
s: "strategy",
|
||||
t: "timeout",
|
||||
};
|
||||
|
||||
const STRATEGY_VALUES = new Set<StrategyId>([
|
||||
"trend",
|
||||
"swing",
|
||||
"guardian",
|
||||
"maker",
|
||||
"maker-points",
|
||||
"offset-maker",
|
||||
"liquidity-maker",
|
||||
"basis",
|
||||
"grid",
|
||||
]);
|
||||
|
||||
export class CommandParseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "CommandParseError";
|
||||
}
|
||||
}
|
||||
|
||||
interface ParsedOptionBag {
|
||||
options: Record<string, string | boolean>;
|
||||
positionals: string[];
|
||||
}
|
||||
|
||||
export function parseCommandArgv(argv: string[]): ParsedCliCommand | null {
|
||||
if (!argv.length) return null;
|
||||
const root = (argv[0] ?? "").trim().toLowerCase();
|
||||
if (!root || root.startsWith("-")) return null;
|
||||
if (!ROOT_COMMANDS.has(root)) return null;
|
||||
|
||||
if (root === "help") {
|
||||
return parseHelpCommand(argv.slice(1));
|
||||
}
|
||||
if (root === "doctor") {
|
||||
const bag = parseOptionBag(argv.slice(1));
|
||||
assertAllowedOptions(bag.options, GLOBAL_OPTION_NAMES);
|
||||
const common = parseCommonOptions(bag.options);
|
||||
return { kind: "doctor", ...common };
|
||||
}
|
||||
|
||||
if (argv.length < 2) {
|
||||
throw new CommandParseError(`Missing action for command '${root}'`);
|
||||
}
|
||||
const action = (argv[1] ?? "").trim().toLowerCase();
|
||||
const bag = parseOptionBag(argv.slice(2));
|
||||
const common = parseCommonOptions(bag.options);
|
||||
|
||||
if (common.help) {
|
||||
return {
|
||||
kind: "help",
|
||||
topic: `${root} ${action}`.trim(),
|
||||
...common,
|
||||
};
|
||||
}
|
||||
|
||||
switch (root) {
|
||||
case "exchange":
|
||||
return parseExchangeCommand(action, bag.options, common);
|
||||
case "market":
|
||||
return parseMarketCommand(action, bag.options, common);
|
||||
case "account":
|
||||
return parseAccountCommand(action, bag.options, common);
|
||||
case "position":
|
||||
return parsePositionCommand(action, bag.options, common);
|
||||
case "order":
|
||||
return parseOrderCommand(action, bag.options, common);
|
||||
case "strategy":
|
||||
return parseStrategyCommand(action, bag, common);
|
||||
default:
|
||||
throw new CommandParseError(`Unsupported root command '${root}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function printCommandHelp(topic?: string): void {
|
||||
if (!topic) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log([
|
||||
"ritmex-bot command mode:",
|
||||
" ritmex-bot doctor",
|
||||
" ritmex-bot exchange list",
|
||||
" ritmex-bot exchange capabilities [--exchange <id>]",
|
||||
" ritmex-bot market ticker [--exchange <id>] [--symbol <symbol>]",
|
||||
" ritmex-bot market depth [--exchange <id>] [--symbol <symbol>] [--levels <n>]",
|
||||
" ritmex-bot market kline --interval <interval> [--limit <n>] [--exchange <id>] [--symbol <symbol>]",
|
||||
" ritmex-bot account snapshot [--exchange <id>]",
|
||||
" ritmex-bot position list [--exchange <id>] [--symbol <symbol>]",
|
||||
" ritmex-bot order open [--exchange <id>] [--symbol <symbol>]",
|
||||
" ritmex-bot order create --side <buy|sell> --type <limit|market|stop|trailing-stop|close> --quantity <n> [options]",
|
||||
" ritmex-bot order cancel --order-id <id> [--exchange <id>] [--symbol <symbol>]",
|
||||
" ritmex-bot order cancel-all [--exchange <id>] [--symbol <symbol>]",
|
||||
" ritmex-bot strategy run --strategy <id> [--exchange <id>] [--silent] [--dry-run]",
|
||||
"",
|
||||
"Global options:",
|
||||
" --exchange, -e Exchange id",
|
||||
" --symbol Trading symbol (passed through without normalization)",
|
||||
" --json, -j JSON output",
|
||||
" --dry-run, -d Simulate write operations",
|
||||
" --timeout, -t Timeout in milliseconds (default 25000)",
|
||||
" --help, -h Show command help",
|
||||
"",
|
||||
"Legacy mode remains available: bun run index.ts [--strategy ...] [--exchange ...]",
|
||||
].join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`ritmex-bot help: ${topic}`);
|
||||
}
|
||||
|
||||
function parseHelpCommand(argv: string[]): ParsedCliCommand {
|
||||
const bag = parseOptionBag(argv);
|
||||
assertAllowedOptions(bag.options, GLOBAL_OPTION_NAMES);
|
||||
const common = parseCommonOptions(bag.options);
|
||||
const topic = bag.positionals.length > 0 ? bag.positionals.join(" ") : undefined;
|
||||
return { kind: "help", topic, ...common };
|
||||
}
|
||||
|
||||
function parseExchangeCommand(
|
||||
action: string,
|
||||
options: Record<string, string | boolean>,
|
||||
common: CommandCommonOptions & { help?: boolean }
|
||||
): ParsedCliCommand {
|
||||
assertAllowedOptions(options, new Set(GLOBAL_OPTION_NAMES));
|
||||
if (action === "list") return { kind: "exchange-list", ...common };
|
||||
if (action === "capabilities") return { kind: "exchange-capabilities", ...common };
|
||||
throw new CommandParseError(`Unsupported exchange action '${action}'`);
|
||||
}
|
||||
|
||||
function parseMarketCommand(
|
||||
action: string,
|
||||
options: Record<string, string | boolean>,
|
||||
common: CommandCommonOptions & { help?: boolean }
|
||||
): ParsedCliCommand {
|
||||
assertAllowedOptions(options, new Set([...GLOBAL_OPTION_NAMES, "levels", "interval", "limit"]));
|
||||
if (action === "ticker") return { kind: "market-ticker", ...common };
|
||||
if (action === "depth") {
|
||||
const levels = readNumberOption(options, ["levels"]);
|
||||
return { kind: "market-depth", levels, ...common };
|
||||
}
|
||||
if (action === "kline") {
|
||||
const interval = requireStringOption(options, ["interval"], "Missing required option --interval");
|
||||
const limit = readNumberOption(options, ["limit"]);
|
||||
return { kind: "market-kline", interval, limit, ...common };
|
||||
}
|
||||
throw new CommandParseError(`Unsupported market action '${action}'`);
|
||||
}
|
||||
|
||||
function parseAccountCommand(
|
||||
action: string,
|
||||
options: Record<string, string | boolean>,
|
||||
common: CommandCommonOptions & { help?: boolean }
|
||||
): ParsedCliCommand {
|
||||
assertAllowedOptions(options, GLOBAL_OPTION_NAMES);
|
||||
if (action === "snapshot" || action === "summary") {
|
||||
return { kind: "account-snapshot", ...common };
|
||||
}
|
||||
throw new CommandParseError(`Unsupported account action '${action}'`);
|
||||
}
|
||||
|
||||
function parsePositionCommand(
|
||||
action: string,
|
||||
options: Record<string, string | boolean>,
|
||||
common: CommandCommonOptions & { help?: boolean }
|
||||
): ParsedCliCommand {
|
||||
assertAllowedOptions(options, GLOBAL_OPTION_NAMES);
|
||||
if (action === "list") {
|
||||
return { kind: "position-list", ...common };
|
||||
}
|
||||
throw new CommandParseError(`Unsupported position action '${action}'`);
|
||||
}
|
||||
|
||||
function parseOrderCommand(
|
||||
action: string,
|
||||
options: Record<string, string | boolean>,
|
||||
common: CommandCommonOptions & { help?: boolean }
|
||||
): ParsedCliCommand {
|
||||
if (action === "open") {
|
||||
assertAllowedOptions(options, GLOBAL_OPTION_NAMES);
|
||||
return { kind: "order-open", ...common };
|
||||
}
|
||||
if (action === "cancel") {
|
||||
assertAllowedOptions(options, new Set([...GLOBAL_OPTION_NAMES, "order-id"]));
|
||||
const orderId = requireStringOption(options, ["order-id"], "Missing required option --order-id");
|
||||
return { kind: "order-cancel", orderId, ...common };
|
||||
}
|
||||
if (action === "cancel-all") {
|
||||
assertAllowedOptions(options, GLOBAL_OPTION_NAMES);
|
||||
return { kind: "order-cancel-all", ...common };
|
||||
}
|
||||
if (action === "create") {
|
||||
assertAllowedOptions(
|
||||
options,
|
||||
new Set([
|
||||
...GLOBAL_OPTION_NAMES,
|
||||
"side",
|
||||
"type",
|
||||
"quantity",
|
||||
"qty",
|
||||
"price",
|
||||
"stop-price",
|
||||
"activation-price",
|
||||
"callback-rate",
|
||||
"time-in-force",
|
||||
"reduce-only",
|
||||
"close-position",
|
||||
"trigger-type",
|
||||
"sl-price",
|
||||
"tp-price",
|
||||
])
|
||||
);
|
||||
|
||||
const payload = parseOrderCreatePayload(options);
|
||||
return { kind: "order-create", payload, ...common };
|
||||
}
|
||||
throw new CommandParseError(`Unsupported order action '${action}'`);
|
||||
}
|
||||
|
||||
function parseStrategyCommand(
|
||||
action: string,
|
||||
bag: ParsedOptionBag,
|
||||
common: CommandCommonOptions & { help?: boolean }
|
||||
): ParsedCliCommand {
|
||||
if (action !== "run") {
|
||||
throw new CommandParseError(`Unsupported strategy action '${action}'`);
|
||||
}
|
||||
assertAllowedOptions(bag.options, new Set([...GLOBAL_OPTION_NAMES, "strategy", "silent"]));
|
||||
const strategyInput = readStringOption(bag.options, ["strategy"]) ?? bag.positionals[0];
|
||||
if (!strategyInput) {
|
||||
throw new CommandParseError("Missing required option --strategy for strategy run");
|
||||
}
|
||||
const strategy = normalizeStrategy(strategyInput);
|
||||
const silent = readBooleanOption(bag.options, ["silent"], false);
|
||||
return { kind: "strategy-run", strategy, silent, ...common };
|
||||
}
|
||||
|
||||
function parseOrderCreatePayload(options: Record<string, string | boolean>): OrderCreatePayload {
|
||||
const side = normalizeSide(requireStringOption(options, ["side"], "Missing required option --side"));
|
||||
const type = normalizeOrderType(requireStringOption(options, ["type"], "Missing required option --type"));
|
||||
const quantity = requireNumberOption(options, ["quantity", "qty"], "Missing required option --quantity/--qty");
|
||||
const payload: OrderCreatePayload = { side, type, quantity };
|
||||
|
||||
if (type === "limit") {
|
||||
payload.price = requireNumberOption(options, ["price"], "Missing required option --price for limit orders");
|
||||
}
|
||||
if (type === "stop") {
|
||||
payload.stopPrice = requireNumberOption(options, ["stop-price"], "Missing required option --stop-price for stop orders");
|
||||
}
|
||||
if (type === "trailing-stop") {
|
||||
payload.activationPrice = requireNumberOption(
|
||||
options,
|
||||
["activation-price"],
|
||||
"Missing required option --activation-price for trailing-stop orders"
|
||||
);
|
||||
payload.callbackRate = requireNumberOption(
|
||||
options,
|
||||
["callback-rate"],
|
||||
"Missing required option --callback-rate for trailing-stop orders"
|
||||
);
|
||||
}
|
||||
|
||||
payload.timeInForce = normalizeTimeInForce(readStringOption(options, ["time-in-force"]));
|
||||
payload.reduceOnly = readOptionalBooleanOption(options, ["reduce-only"]);
|
||||
payload.closePosition = readOptionalBooleanOption(options, ["close-position"]);
|
||||
payload.triggerType = normalizeTriggerType(readStringOption(options, ["trigger-type"]));
|
||||
payload.slPrice = readNumberOption(options, ["sl-price"]);
|
||||
payload.tpPrice = readNumberOption(options, ["tp-price"]);
|
||||
payload.price = payload.price ?? readNumberOption(options, ["price"]);
|
||||
payload.stopPrice = payload.stopPrice ?? readNumberOption(options, ["stop-price"]);
|
||||
payload.activationPrice = payload.activationPrice ?? readNumberOption(options, ["activation-price"]);
|
||||
payload.callbackRate = payload.callbackRate ?? readNumberOption(options, ["callback-rate"]);
|
||||
return payload;
|
||||
}
|
||||
|
||||
function parseCommonOptions(options: Record<string, string | boolean>): CommandCommonOptions & { help?: boolean } {
|
||||
const exchangeRaw = readStringOption(options, ["exchange"]);
|
||||
const symbol = readStringOption(options, ["symbol"]);
|
||||
const json = readBooleanOption(options, ["json"], false);
|
||||
const dryRun = readBooleanOption(options, ["dry-run"], false);
|
||||
const timeoutMs = readNumberOption(options, ["timeout"]) ?? DEFAULT_TIMEOUT_MS;
|
||||
const help = readBooleanOption(options, ["help"], false);
|
||||
return {
|
||||
exchange: normalizeExchange(exchangeRaw),
|
||||
symbol: symbol?.trim() || undefined,
|
||||
json,
|
||||
dryRun,
|
||||
timeoutMs,
|
||||
help,
|
||||
};
|
||||
}
|
||||
|
||||
function parseOptionBag(args: string[]): ParsedOptionBag {
|
||||
const options: Record<string, string | boolean> = {};
|
||||
const positionals: string[] = [];
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const token = args[i];
|
||||
if (!token) continue;
|
||||
if (token === "--") {
|
||||
positionals.push(...args.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
if (token.startsWith("--")) {
|
||||
const withoutPrefix = token.slice(2);
|
||||
if (!withoutPrefix) throw new CommandParseError("Invalid option '--'");
|
||||
const eqIndex = withoutPrefix.indexOf("=");
|
||||
const rawKey = eqIndex === -1 ? withoutPrefix : withoutPrefix.slice(0, eqIndex);
|
||||
const key = rawKey.trim().toLowerCase();
|
||||
if (!key) throw new CommandParseError(`Invalid option '${token}'`);
|
||||
if (eqIndex !== -1) {
|
||||
options[key] = withoutPrefix.slice(eqIndex + 1);
|
||||
continue;
|
||||
}
|
||||
const next = args[i + 1];
|
||||
if (next && !next.startsWith("-")) {
|
||||
options[key] = next;
|
||||
i += 1;
|
||||
} else {
|
||||
options[key] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith("-")) {
|
||||
const short = token.slice(1);
|
||||
if (short.length !== 1) {
|
||||
throw new CommandParseError(`Unsupported short option '${token}'`);
|
||||
}
|
||||
const alias = SHORT_OPTION_ALIAS[short];
|
||||
if (!alias) {
|
||||
throw new CommandParseError(`Unsupported short option '${token}'`);
|
||||
}
|
||||
const next = args[i + 1];
|
||||
if (next && !next.startsWith("-") && expectsValue(alias)) {
|
||||
options[alias] = next;
|
||||
i += 1;
|
||||
} else {
|
||||
options[alias] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
positionals.push(token);
|
||||
}
|
||||
|
||||
return { options, positionals };
|
||||
}
|
||||
|
||||
function expectsValue(optionName: string): boolean {
|
||||
return optionName === "exchange" || optionName === "strategy" || optionName === "timeout";
|
||||
}
|
||||
|
||||
function assertAllowedOptions(
|
||||
options: Record<string, string | boolean>,
|
||||
allowed: ReadonlySet<string>
|
||||
): void {
|
||||
for (const key of Object.keys(options)) {
|
||||
if (!allowed.has(key)) {
|
||||
throw new CommandParseError(`Unsupported option '--${key}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readStringOption(options: Record<string, string | boolean>, names: string[]): string | undefined {
|
||||
for (const name of names) {
|
||||
const raw = options[name];
|
||||
if (raw == null) continue;
|
||||
if (typeof raw !== "string") {
|
||||
throw new CommandParseError(`Option --${name} requires a value`);
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
throw new CommandParseError(`Option --${name} cannot be empty`);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function requireStringOption(
|
||||
options: Record<string, string | boolean>,
|
||||
names: string[],
|
||||
errorMessage: string
|
||||
): string {
|
||||
const value = readStringOption(options, names);
|
||||
if (!value) {
|
||||
throw new CommandParseError(errorMessage);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function readBooleanOption(options: Record<string, string | boolean>, names: string[], fallback: boolean): boolean {
|
||||
const value = readOptionalBooleanOption(options, names);
|
||||
return value == null ? fallback : value;
|
||||
}
|
||||
|
||||
function readOptionalBooleanOption(options: Record<string, string | boolean>, names: string[]): boolean | undefined {
|
||||
for (const name of names) {
|
||||
const raw = options[name];
|
||||
if (raw == null) continue;
|
||||
if (raw === true) return true;
|
||||
if (typeof raw !== "string") {
|
||||
throw new CommandParseError(`Option --${name} expects a boolean value`);
|
||||
}
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
if (!normalized) return true;
|
||||
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") return true;
|
||||
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") return false;
|
||||
throw new CommandParseError(`Option --${name} expects a boolean value`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readNumberOption(options: Record<string, string | boolean>, names: string[]): number | undefined {
|
||||
const value = readStringOption(options, names);
|
||||
if (!value) return undefined;
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
throw new CommandParseError(`Option --${names[0]} expects a numeric value`);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
function requireNumberOption(
|
||||
options: Record<string, string | boolean>,
|
||||
names: string[],
|
||||
errorMessage: string
|
||||
): number {
|
||||
const number = readNumberOption(options, names);
|
||||
if (number == null) {
|
||||
throw new CommandParseError(errorMessage);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
function normalizeExchange(value: string | undefined): SupportedExchangeId | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "gravity" || normalized === "grav" || normalized === "grv") return "grvt";
|
||||
if (normalized === "bnb") return "binance";
|
||||
if (normalized === "ondo" || normalized === "ondoperp") return "ondoperps";
|
||||
if (isSupportedExchangeId(normalized)) return normalized;
|
||||
throw new CommandParseError(`Unsupported exchange '${value}'`);
|
||||
}
|
||||
|
||||
function normalizeStrategy(value: string): StrategyId {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (STRATEGY_VALUES.has(normalized as StrategyId)) {
|
||||
return normalized as StrategyId;
|
||||
}
|
||||
if (normalized === "offset" || normalized === "offsetmaker" || normalized === "offset-maker") return "offset-maker";
|
||||
if (normalized === "makerpoints" || normalized === "maker_points") return "maker-points";
|
||||
if (normalized === "liquidity" || normalized === "liquiditymaker" || normalized === "liquidity_maker") {
|
||||
return "liquidity-maker";
|
||||
}
|
||||
throw new CommandParseError(`Unsupported strategy '${value}'`);
|
||||
}
|
||||
|
||||
function normalizeSide(value: string): "BUY" | "SELL" {
|
||||
const normalized = value.trim().toUpperCase();
|
||||
if (normalized === "BUY" || normalized === "SELL") return normalized;
|
||||
throw new CommandParseError(`Unsupported side '${value}', expected BUY or SELL`);
|
||||
}
|
||||
|
||||
function normalizeOrderType(value: string): OrderCreateType {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "limit" || normalized === "market" || normalized === "stop" || normalized === "close") {
|
||||
return normalized;
|
||||
}
|
||||
if (normalized === "trailing-stop" || normalized === "trailing_stop" || normalized === "trailingstop") {
|
||||
return "trailing-stop";
|
||||
}
|
||||
throw new CommandParseError(
|
||||
`Unsupported order type '${value}', expected limit|market|stop|trailing-stop|close`
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeTimeInForce(value: string | undefined): "GTC" | "IOC" | "FOK" | "GTX" | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.trim().toUpperCase();
|
||||
if (normalized === "GTC" || normalized === "IOC" || normalized === "FOK" || normalized === "GTX") {
|
||||
return normalized;
|
||||
}
|
||||
throw new CommandParseError(`Unsupported time in force '${value}'`);
|
||||
}
|
||||
|
||||
function normalizeTriggerType(
|
||||
value: string | undefined
|
||||
): "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS" | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.trim().toUpperCase();
|
||||
if (normalized === "UNSPECIFIED" || normalized === "TAKE_PROFIT" || normalized === "STOP_LOSS") {
|
||||
return normalized;
|
||||
}
|
||||
throw new CommandParseError(`Unsupported trigger type '${value}'`);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { SupportedExchangeId } from "../exchanges/create-adapter";
|
||||
import type { StrategyId } from "./args";
|
||||
|
||||
export interface CommandCommonOptions {
|
||||
exchange?: SupportedExchangeId;
|
||||
symbol?: string;
|
||||
json: boolean;
|
||||
dryRun: boolean;
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
export type OrderCreateType = "limit" | "market" | "stop" | "trailing-stop" | "close";
|
||||
|
||||
export interface OrderCreatePayload {
|
||||
side: "BUY" | "SELL";
|
||||
type: OrderCreateType;
|
||||
quantity: number;
|
||||
price?: number;
|
||||
stopPrice?: number;
|
||||
activationPrice?: number;
|
||||
callbackRate?: number;
|
||||
timeInForce?: "GTC" | "IOC" | "FOK" | "GTX";
|
||||
reduceOnly?: boolean;
|
||||
closePosition?: boolean;
|
||||
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||
slPrice?: number;
|
||||
tpPrice?: number;
|
||||
}
|
||||
|
||||
export type ParsedCliCommand =
|
||||
| ({ kind: "help"; topic?: string } & CommandCommonOptions)
|
||||
| ({ kind: "doctor" } & CommandCommonOptions)
|
||||
| ({ kind: "exchange-list" } & CommandCommonOptions)
|
||||
| ({ kind: "exchange-capabilities" } & CommandCommonOptions)
|
||||
| ({ kind: "market-ticker" } & CommandCommonOptions)
|
||||
| ({ kind: "market-depth"; levels?: number } & CommandCommonOptions)
|
||||
| ({ kind: "market-kline"; interval: string; limit?: number } & CommandCommonOptions)
|
||||
| ({ kind: "account-snapshot" } & CommandCommonOptions)
|
||||
| ({ kind: "position-list" } & CommandCommonOptions)
|
||||
| ({ kind: "order-open" } & CommandCommonOptions)
|
||||
| ({ kind: "order-create"; payload: OrderCreatePayload } & CommandCommonOptions)
|
||||
| ({ kind: "order-cancel"; orderId: string } & CommandCommonOptions)
|
||||
| ({ kind: "order-cancel-all" } & CommandCommonOptions)
|
||||
| ({ kind: "strategy-run"; strategy: StrategyId; silent: boolean } & CommandCommonOptions);
|
||||
|
||||
export interface CommandErrorPayload {
|
||||
code:
|
||||
| "INVALID_ARGS"
|
||||
| "MISSING_ENV"
|
||||
| "UNSUPPORTED"
|
||||
| "EXCHANGE_ERROR"
|
||||
| "TIMEOUT"
|
||||
| "RUNTIME_ERROR";
|
||||
message: string;
|
||||
retryable?: boolean;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
export interface CommandSuccessPayload {
|
||||
success: true;
|
||||
command: ParsedCliCommand["kind"];
|
||||
exchange?: SupportedExchangeId;
|
||||
symbol?: string;
|
||||
dryRun: boolean;
|
||||
ts: string;
|
||||
data: unknown;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface CommandFailurePayload {
|
||||
success: false;
|
||||
command: ParsedCliCommand["kind"] | "unknown";
|
||||
exchange?: SupportedExchangeId;
|
||||
symbol?: string;
|
||||
dryRun: boolean;
|
||||
ts: string;
|
||||
error: CommandErrorPayload;
|
||||
}
|
||||
|
||||
export type CommandPayload = CommandSuccessPayload | CommandFailurePayload;
|
||||
|
||||
export interface CommandExecutionResult {
|
||||
exitCode: number;
|
||||
payload: CommandPayload;
|
||||
forceExit: boolean;
|
||||
}
|
||||
+12
-29
@@ -2,7 +2,6 @@ import { basisConfig, gridConfig, isBasisStrategyEnabled, liquidityMakerConfig,
|
||||
import { getExchangeDisplayName, isBasisSupportedExchangeId, resolveExchangeId } from "../exchanges/create-adapter";
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||
import { DryRunExchangeAdapter } from "../exchanges/dry-run-adapter";
|
||||
import { MakerEngine, type MakerEngineSnapshot } from "../strategy/maker-engine";
|
||||
import { OffsetMakerEngine, type OffsetMakerEngineSnapshot } from "../strategy/offset-maker-engine";
|
||||
import { LiquidityMakerEngine, type LiquidityMakerEngineSnapshot } from "../strategy/liquidity-maker-engine";
|
||||
@@ -17,7 +16,6 @@ import type { StrategyId } from "./args";
|
||||
|
||||
interface RunnerOptions {
|
||||
silent?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
type StrategyRunner = (options: RunnerOptions) => Promise<void>;
|
||||
@@ -45,13 +43,12 @@ export async function startStrategy(strategyId: StrategyId, options: RunnerOptio
|
||||
const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
trend: async (opts) => {
|
||||
const config = tradingConfig;
|
||||
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(config.symbol);
|
||||
const engine = new TrendEngine(config, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "trend",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -59,13 +56,12 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
},
|
||||
swing: async (opts) => {
|
||||
const config = swingConfig;
|
||||
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(config.symbol);
|
||||
const engine = new SwingEngine(config, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "swing",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -73,13 +69,12 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
},
|
||||
guardian: async (opts) => {
|
||||
const config = tradingConfig;
|
||||
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(config.symbol);
|
||||
const engine = new GuardianEngine(config, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "guardian",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -87,13 +82,12 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
},
|
||||
maker: async (opts) => {
|
||||
const config = makerConfig;
|
||||
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(config.symbol);
|
||||
const engine = new MakerEngine(config, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "maker",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -105,13 +99,12 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
throw new Error("Maker Points strategy only supports the StandX exchange.");
|
||||
}
|
||||
const config = makerPointsConfig;
|
||||
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(config.symbol);
|
||||
const engine = new MakerPointsEngine(config, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "maker-points",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -119,13 +112,12 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
},
|
||||
"offset-maker": async (opts) => {
|
||||
const config = makerConfig;
|
||||
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(config.symbol);
|
||||
const engine = new OffsetMakerEngine(config, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "offset-maker",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -133,13 +125,12 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
},
|
||||
"liquidity-maker": async (opts) => {
|
||||
const config = liquidityMakerConfig;
|
||||
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(config.symbol);
|
||||
const engine = new LiquidityMakerEngine(config, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "liquidity-maker",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -153,13 +144,12 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
if (!isBasisSupportedExchangeId(exchangeId)) {
|
||||
throw new Error("Basis arbitrage strategy currently only supports the Aster, Nado, StandX, and Binance exchanges");
|
||||
}
|
||||
const adapter = createAdapterOrThrow(basisConfig.futuresSymbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(basisConfig.futuresSymbol);
|
||||
const engine = new BasisArbEngine(basisConfig, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "basis",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -167,13 +157,12 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
|
||||
},
|
||||
grid: async (opts) => {
|
||||
const config = gridConfig;
|
||||
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
|
||||
const adapter = createAdapterOrThrow(config.symbol);
|
||||
const engine = new GridEngine(config, adapter);
|
||||
await runEngine({
|
||||
engine,
|
||||
strategy: "grid",
|
||||
silent: opts.silent,
|
||||
dryRun: opts.dryRun,
|
||||
getSnapshot: () => engine.getSnapshot(),
|
||||
onUpdate: (emitter) => engine.on("update", emitter),
|
||||
offUpdate: (emitter) => engine.off("update", emitter),
|
||||
@@ -185,7 +174,6 @@ interface EngineHarness<TSnapshot> {
|
||||
engine: { start(): void; stop(): void };
|
||||
strategy: StrategyId;
|
||||
silent?: boolean;
|
||||
dryRun?: boolean;
|
||||
getSnapshot: () => TSnapshot;
|
||||
onUpdate: (handler: (snapshot: TSnapshot) => void) => void;
|
||||
offUpdate: (handler: (snapshot: TSnapshot) => void) => void;
|
||||
@@ -238,8 +226,7 @@ async function runEngine<
|
||||
onUpdate(emitter);
|
||||
engine.start();
|
||||
|
||||
const modeLabel = `${silent ? "silent" : "interactive"}${harness.dryRun ? "+dry-run" : ""}`;
|
||||
console.info(`[${label}] Starting on ${exchangeName}. Mode: ${modeLabel}. Press Ctrl+C to exit.`);
|
||||
console.info(`[${label}] Starting on ${exchangeName}. Mode: ${silent ? "silent" : "interactive"}. Press Ctrl+C to exit.`);
|
||||
|
||||
const shutdown = (signal: NodeJS.Signals) => {
|
||||
try {
|
||||
@@ -264,12 +251,8 @@ async function runEngine<
|
||||
});
|
||||
}
|
||||
|
||||
function createAdapterOrThrow(symbol: string, dryRun?: boolean): ExchangeAdapter {
|
||||
const adapter = buildAdapterFromEnv({ exchangeId: resolveExchangeId(), symbol });
|
||||
if (dryRun) {
|
||||
return new DryRunExchangeAdapter(adapter);
|
||||
}
|
||||
return adapter;
|
||||
function createAdapterOrThrow(symbol: string): ExchangeAdapter {
|
||||
return buildAdapterFromEnv({ exchangeId: resolveExchangeId(), symbol });
|
||||
}
|
||||
|
||||
type TradeLogEntry = { time: string; type: string; detail: string };
|
||||
|
||||
@@ -98,7 +98,6 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
|
||||
nado: { envKeys: ["NADO_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-PERP" },
|
||||
standx: { envKeys: ["STANDX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD" },
|
||||
binance: { envKeys: ["BINANCE_SYMBOL", "TRADE_SYMBOL"], fallback: "BTCUSDT" },
|
||||
ondoperps: { envKeys: ["ONDOPERPS_SYMBOL", "ONDOPERP_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD.P" },
|
||||
};
|
||||
|
||||
export function resolveSymbolFromEnv(explicitExchangeId?: SupportedExchangeId | string | null): string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Order } from "../../exchanges/types";
|
||||
import type { AsterOrder } from "../../exchanges/types";
|
||||
|
||||
export interface OrderTarget {
|
||||
side: "BUY" | "SELL";
|
||||
@@ -8,11 +8,11 @@ export interface OrderTarget {
|
||||
}
|
||||
|
||||
export function makeOrderPlan(
|
||||
openOrders: Order[],
|
||||
openOrders: AsterOrder[],
|
||||
targets: OrderTarget[]
|
||||
): { toCancel: Order[]; toPlace: OrderTarget[] } {
|
||||
): { toCancel: AsterOrder[]; toPlace: OrderTarget[] } {
|
||||
const unmatched = new Set(targets.map((_, idx) => idx));
|
||||
const toCancel: Order[] = [];
|
||||
const toCancel: AsterOrder[] = [];
|
||||
|
||||
for (const order of openOrders) {
|
||||
const orderPrice = String(order.price);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ExchangeAdapter } from "../../exchanges/adapter";
|
||||
import type { Order } from "../../exchanges/types";
|
||||
import type { AsterOrder } from "../../exchanges/types";
|
||||
import { isUnknownOrderError } from "../../utils/errors";
|
||||
|
||||
export async function safeCancelOrder(
|
||||
exchange: ExchangeAdapter,
|
||||
symbol: string,
|
||||
order: Order,
|
||||
order: AsterOrder,
|
||||
onResolved: (orderId: number | string) => void,
|
||||
onUnknown: () => void,
|
||||
onError: (err: unknown) => void
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type { Order } from "../exchanges/types";
|
||||
import type { AsterOrder } from "../exchanges/types";
|
||||
import {
|
||||
routeCloseOrder,
|
||||
routeLimitOrder,
|
||||
@@ -88,7 +88,7 @@ export function unlockOperating(
|
||||
export async function deduplicateOrders(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
openOrders: AsterOrder[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
@@ -134,13 +134,12 @@ type PlaceOrderOptions = {
|
||||
skipDedupe?: boolean;
|
||||
slPrice?: number;
|
||||
tpPrice?: number;
|
||||
clientOrderId?: string;
|
||||
};
|
||||
|
||||
export async function placeOrder(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
openOrders: AsterOrder[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
@@ -151,7 +150,7 @@ export async function placeOrder(
|
||||
reduceOnly = false,
|
||||
guard?: OrderGuardOptions,
|
||||
opts?: PlaceOrderOptions
|
||||
): Promise<Order | undefined> {
|
||||
): Promise<AsterOrder | undefined> {
|
||||
const type = "LIMIT";
|
||||
if (isOperating(locks, type)) return;
|
||||
const priceNum = Number(price);
|
||||
@@ -181,7 +180,6 @@ export async function placeOrder(
|
||||
closePosition,
|
||||
slPrice: opts?.slPrice,
|
||||
tpPrice: opts?.tpPrice,
|
||||
clientOrderId: opts?.clientOrderId,
|
||||
});
|
||||
pendings[type] = String(order.orderId);
|
||||
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${opts?.slPrice ? ` sl=${opts.slPrice}` : ""}`);
|
||||
@@ -199,7 +197,7 @@ export async function placeOrder(
|
||||
export async function placeMarketOrder(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
openOrders: AsterOrder[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
@@ -209,7 +207,7 @@ export async function placeMarketOrder(
|
||||
reduceOnly = false,
|
||||
guard?: OrderGuardOptions,
|
||||
opts?: { qtyStep: number }
|
||||
): Promise<Order | undefined> {
|
||||
): Promise<AsterOrder | undefined> {
|
||||
const type = "MARKET";
|
||||
if (isOperating(locks, type)) return;
|
||||
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
|
||||
@@ -249,7 +247,7 @@ export async function placeMarketOrder(
|
||||
export async function placeStopLossOrder(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
openOrders: AsterOrder[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
@@ -260,7 +258,7 @@ export async function placeStopLossOrder(
|
||||
log: LogHandler,
|
||||
guard?: OrderGuardOptions,
|
||||
opts?: { priceTick: number; qtyStep: number }
|
||||
): Promise<Order | undefined> {
|
||||
): Promise<AsterOrder | undefined> {
|
||||
const type = "STOP_MARKET";
|
||||
if (isOperating(locks, type)) return;
|
||||
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
|
||||
@@ -316,7 +314,7 @@ export async function placeStopLossOrder(
|
||||
export async function placeTrailingStopOrder(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
openOrders: AsterOrder[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
@@ -327,7 +325,7 @@ export async function placeTrailingStopOrder(
|
||||
log: LogHandler,
|
||||
guard?: OrderGuardOptions,
|
||||
opts?: { priceTick: number; qtyStep: number }
|
||||
): Promise<Order | undefined> {
|
||||
): Promise<AsterOrder | undefined> {
|
||||
const type = "TRAILING_STOP_MARKET";
|
||||
if (isOperating(locks, type)) return;
|
||||
if (!adapter.supportsTrailingStops()) {
|
||||
@@ -377,7 +375,7 @@ export async function placeTrailingStopOrder(
|
||||
export async function marketClose(
|
||||
adapter: ExchangeAdapter,
|
||||
symbol: string,
|
||||
openOrders: Order[],
|
||||
openOrders: AsterOrder[],
|
||||
locks: OrderLockMap,
|
||||
timers: OrderTimerMap,
|
||||
pendings: OrderPendingMap,
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
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
@@ -1,30 +1,30 @@
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Order,
|
||||
Depth,
|
||||
Ticker,
|
||||
Kline,
|
||||
AsterAccountSnapshot,
|
||||
AsterOrder,
|
||||
AsterDepth,
|
||||
AsterTicker,
|
||||
AsterKline,
|
||||
CreateOrderParams,
|
||||
} from "./types";
|
||||
|
||||
export interface AccountListener {
|
||||
(snapshot: AccountSnapshot): void;
|
||||
(snapshot: AsterAccountSnapshot): void;
|
||||
}
|
||||
|
||||
export interface OrderListener {
|
||||
(orders: Order[]): void;
|
||||
(orders: AsterOrder[]): void;
|
||||
}
|
||||
|
||||
export interface DepthListener {
|
||||
(depth: Depth): void;
|
||||
(depth: AsterDepth): void;
|
||||
}
|
||||
|
||||
export interface TickerListener {
|
||||
(ticker: Ticker): void;
|
||||
(ticker: AsterTicker): void;
|
||||
}
|
||||
|
||||
export interface KlineListener {
|
||||
(klines: Kline[]): void;
|
||||
(klines: AsterKline[]): void;
|
||||
}
|
||||
|
||||
export interface FundingRateSnapshot {
|
||||
@@ -72,18 +72,18 @@ export interface ExchangeAdapter {
|
||||
watchTicker(symbol: string, cb: TickerListener): void;
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void;
|
||||
watchFundingRate?(symbol: string, cb: FundingRateListener): void;
|
||||
createOrder(params: CreateOrderParams): Promise<Order>;
|
||||
createOrder(params: CreateOrderParams): Promise<AsterOrder>;
|
||||
cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>;
|
||||
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
||||
cancelAllOrders(params: { symbol: string }): Promise<void>;
|
||||
getPrecision?(): Promise<ExchangePrecision | null>;
|
||||
// Connection protection methods (optional)
|
||||
// 连接保护相关方法(可选,仅 StandX 支持)
|
||||
onConnectionEvent?(listener: ConnectionEventListener): void;
|
||||
offConnectionEvent?(listener: ConnectionEventListener): void;
|
||||
onRestHealthEvent?(listener: RestHealthListener): void;
|
||||
offRestHealthEvent?(listener: RestHealthListener): void;
|
||||
queryOpenOrders?(): Promise<Order[]>;
|
||||
queryAccountSnapshot?(): Promise<AccountSnapshot | null>;
|
||||
queryOpenOrders?(): Promise<AsterOrder[]>;
|
||||
queryAccountSnapshot?(): Promise<AsterAccountSnapshot | null>;
|
||||
changeMarginMode?(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void>;
|
||||
forceCancelAllOrders?(): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
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,16 +1,11 @@
|
||||
import crypto from "crypto";
|
||||
import { setInterval, clearInterval, setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountPosition,
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
CreateOrderParams,
|
||||
PositionSide,
|
||||
} from "../types";
|
||||
import type {
|
||||
AsterAccountPosition,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterSpotAccount,
|
||||
AsterSpotAggTrade,
|
||||
AsterSpotBookTicker,
|
||||
@@ -23,15 +18,18 @@ import type {
|
||||
AsterSpotTicker24h,
|
||||
AsterSpotTrade,
|
||||
AsterSpotUserTrade,
|
||||
AsterTicker,
|
||||
AsterFuturesExchangeInfo,
|
||||
AsterFuturesSymbolInfo,
|
||||
CancelSpotOrderParams,
|
||||
CreateOrderParams,
|
||||
CreateSpotOrderParams,
|
||||
PositionSide,
|
||||
QuerySpotOrderParams,
|
||||
SpotAllOrdersParams,
|
||||
SpotOpenOrdersParams,
|
||||
SpotUserTradesParams,
|
||||
} from "./types";
|
||||
} from "../types";
|
||||
import { decimalsOf } from "../../utils/math";
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
@@ -246,7 +244,7 @@ export class AsterSpotRestClient {
|
||||
};
|
||||
}
|
||||
|
||||
async createOrder(params: CreateSpotOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateSpotOrderParams): Promise<AsterOrder> {
|
||||
const response = await this.request<any>({
|
||||
path: "/api/v1/order",
|
||||
method: "POST",
|
||||
@@ -257,7 +255,7 @@ export class AsterSpotRestClient {
|
||||
return toOrderFromRest(response);
|
||||
}
|
||||
|
||||
async cancelOrder(params: CancelSpotOrderParams): Promise<Order> {
|
||||
async cancelOrder(params: CancelSpotOrderParams): Promise<AsterOrder> {
|
||||
const response = await this.request<any>({
|
||||
path: "/api/v1/order",
|
||||
method: "DELETE",
|
||||
@@ -272,7 +270,7 @@ export class AsterSpotRestClient {
|
||||
return toOrderFromRest(response);
|
||||
}
|
||||
|
||||
async getOrder(params: QuerySpotOrderParams): Promise<Order> {
|
||||
async getOrder(params: QuerySpotOrderParams): Promise<AsterOrder> {
|
||||
const response = await this.request<any>({
|
||||
path: "/api/v1/order",
|
||||
method: "GET",
|
||||
@@ -287,7 +285,7 @@ export class AsterSpotRestClient {
|
||||
return toOrderFromRest(response);
|
||||
}
|
||||
|
||||
async getOpenOrders(params: SpotOpenOrdersParams = {}): Promise<Order[]> {
|
||||
async getOpenOrders(params: SpotOpenOrdersParams = {}): Promise<AsterOrder[]> {
|
||||
const response = await this.request<any[]>({
|
||||
path: "/api/v1/openOrders",
|
||||
method: "GET",
|
||||
@@ -321,7 +319,7 @@ export class AsterSpotRestClient {
|
||||
});
|
||||
}
|
||||
|
||||
async getAllOrders(params: SpotAllOrdersParams): Promise<Order[]> {
|
||||
async getAllOrders(params: SpotAllOrdersParams): Promise<AsterOrder[]> {
|
||||
const response = await this.request<any[]>({
|
||||
path: "/api/v1/allOrders",
|
||||
method: "GET",
|
||||
@@ -545,13 +543,13 @@ export class AsterSpotRestClient {
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
throw new Error(`[AsterSpotRestClient] 无法解析响应: ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toDepth(streamSymbol: string, data: any): Depth {
|
||||
function toDepth(streamSymbol: string, data: any): AsterDepth {
|
||||
return {
|
||||
eventType: data.e,
|
||||
eventTime: data.E,
|
||||
@@ -563,7 +561,7 @@ function toDepth(streamSymbol: string, data: any): Depth {
|
||||
};
|
||||
}
|
||||
|
||||
function toTicker(data: any): Ticker {
|
||||
function toTicker(data: any): AsterTicker {
|
||||
return {
|
||||
eventType: data.e,
|
||||
eventTime: data.E,
|
||||
@@ -586,7 +584,7 @@ function toTicker(data: any): Ticker {
|
||||
};
|
||||
}
|
||||
|
||||
function toKline(data: any): Kline {
|
||||
function toKline(data: any): AsterKline {
|
||||
return {
|
||||
eventType: data.e,
|
||||
eventTime: data.E,
|
||||
@@ -609,7 +607,7 @@ function toKline(data: any): Kline {
|
||||
};
|
||||
}
|
||||
|
||||
function fromRestKline(entry: any[], interval: string, symbol: string): Kline {
|
||||
function fromRestKline(entry: any[], interval: string, symbol: string): AsterKline {
|
||||
return {
|
||||
eventType: undefined,
|
||||
eventTime: undefined,
|
||||
@@ -630,7 +628,7 @@ function fromRestKline(entry: any[], interval: string, symbol: string): Kline {
|
||||
};
|
||||
}
|
||||
|
||||
function toOrderFromRest(raw: any): Order {
|
||||
function toOrderFromRest(raw: any): AsterOrder {
|
||||
return {
|
||||
avgPrice: raw.avgPrice ?? "0",
|
||||
clientOrderId: raw.clientOrderId ?? "",
|
||||
@@ -658,7 +656,7 @@ function toOrderFromRest(raw: any): Order {
|
||||
};
|
||||
}
|
||||
|
||||
function toOrderFromEvent(event: any): Order {
|
||||
function toOrderFromEvent(event: any): AsterOrder {
|
||||
return {
|
||||
avgPrice: event.ap ?? "0",
|
||||
clientOrderId: event.c ?? "",
|
||||
@@ -686,7 +684,7 @@ function toOrderFromEvent(event: any): Order {
|
||||
};
|
||||
}
|
||||
|
||||
function toPositionFromRisk(raw: any): AccountPosition {
|
||||
function toPositionFromRisk(raw: any): AsterAccountPosition {
|
||||
const positionSide = String(raw.positionSide ?? raw.ps ?? "BOTH").toUpperCase() as PositionSide;
|
||||
return {
|
||||
symbol: raw.symbol ?? raw.s ?? "",
|
||||
@@ -710,16 +708,16 @@ function toPositionFromRisk(raw: any): AccountPosition {
|
||||
};
|
||||
}
|
||||
|
||||
function deepCloneAccount(snapshot: AccountSnapshot | null): AccountSnapshot | null {
|
||||
function deepCloneAccount(snapshot: AsterAccountSnapshot | null): AsterAccountSnapshot | null {
|
||||
return snapshot ? JSON.parse(JSON.stringify(snapshot)) : null;
|
||||
}
|
||||
|
||||
function sumUnrealizedProfit(positions: AccountPosition[]): string {
|
||||
function sumUnrealizedProfit(positions: AsterAccountPosition[]): string {
|
||||
const total = positions.reduce((acc, position) => acc + Number(position.unrealizedProfit ?? 0), 0);
|
||||
return total.toFixed(8);
|
||||
}
|
||||
|
||||
function clonePositions(positions: AccountPosition[]): AccountPosition[] {
|
||||
function clonePositions(positions: AsterAccountPosition[]): AsterAccountPosition[] {
|
||||
return positions.map((position) => ({
|
||||
...position,
|
||||
updateTime: position.updateTime ?? Date.now(),
|
||||
@@ -765,18 +763,18 @@ export class AsterRestClient {
|
||||
this.apiSecret = requireEnv(options.apiSecret ?? process.env.ASTER_API_SECRET, "ASTER_API_SECRET");
|
||||
}
|
||||
|
||||
async getAccount(): Promise<AccountSnapshot> {
|
||||
return this.signedRequest<AccountSnapshot>({ path: "/fapi/v2/account", method: "GET", params: {} });
|
||||
async getAccount(): Promise<AsterAccountSnapshot> {
|
||||
return this.signedRequest<AsterAccountSnapshot>({ path: "/fapi/v2/account", method: "GET", params: {} });
|
||||
}
|
||||
|
||||
async getOpenOrders(symbol?: string): Promise<Order[]> {
|
||||
async getOpenOrders(symbol?: string): Promise<AsterOrder[]> {
|
||||
const params: Record<string, unknown> = {};
|
||||
if (symbol) params.symbol = symbol;
|
||||
const raw = await this.signedRequest<any[]>({ path: "/fapi/v1/openOrders", method: "GET", params });
|
||||
return raw.map(toOrderFromRest);
|
||||
}
|
||||
|
||||
async getPositions(symbol?: string): Promise<AccountPosition[]> {
|
||||
async getPositions(symbol?: string): Promise<AsterAccountPosition[]> {
|
||||
const params: Record<string, unknown> = {};
|
||||
if (symbol) params.symbol = symbol.toUpperCase();
|
||||
const raw = await this.signedRequest<any[]>({ path: "/fapi/v2/positionRisk", method: "GET", params });
|
||||
@@ -797,12 +795,12 @@ export class AsterRestClient {
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text) as AsterFuturesExchangeInfo;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
throw new Error(`[AsterRestClient] 无法解析交易规则响应: ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
// Sanitize and normalize params for Aster futures API. Paradex-specific flags
|
||||
// like reduceOnly/closePosition on STOP/TRAILING should not leak here.
|
||||
const payload: Record<string, unknown> = {};
|
||||
@@ -815,7 +813,6 @@ export class AsterRestClient {
|
||||
if (params.activationPrice !== undefined) payload.activationPrice = params.activationPrice;
|
||||
if (params.callbackRate !== undefined) payload.callbackRate = params.callbackRate;
|
||||
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).
|
||||
// Keep the behavior exchange-specific by stripping them here for Aster.
|
||||
@@ -833,12 +830,12 @@ export class AsterRestClient {
|
||||
return toOrderFromRest(response);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<Order> {
|
||||
async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<AsterOrder> {
|
||||
const response = await this.signedRequest<any>({ path: "/fapi/v1/order", method: "DELETE", params });
|
||||
return toOrderFromRest(response);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList?: Array<number | string>; origClientOrderIdList?: string[] }): Promise<Order[]> {
|
||||
async cancelOrders(params: { symbol: string; orderIdList?: Array<number | string>; origClientOrderIdList?: string[] }): Promise<AsterOrder[]> {
|
||||
const payload: Record<string, unknown> = { symbol: params.symbol };
|
||||
if (params.orderIdList?.length) {
|
||||
payload.orderIdList = `[${params.orderIdList
|
||||
@@ -856,7 +853,7 @@ export class AsterRestClient {
|
||||
await this.signedRequest({ path: "/fapi/v1/allOpenOrders", method: "DELETE", params });
|
||||
}
|
||||
|
||||
async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise<Kline[]> {
|
||||
async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise<AsterKline[]> {
|
||||
const upper = symbol.toUpperCase();
|
||||
const url = `${FUTURES_REST_BASE}/fapi/v1/continuousKlines?pair=${upper}&contractType=PERPETUAL&interval=${encodeURIComponent(interval)}&limit=${limit}`;
|
||||
let response: Response;
|
||||
@@ -872,7 +869,7 @@ export class AsterRestClient {
|
||||
try {
|
||||
const payload = JSON.parse(text) as any[];
|
||||
return payload.map((entry) => fromRestKline(entry, interval, upper));
|
||||
} catch {
|
||||
} catch (error) {
|
||||
throw new Error(`[AsterRestClient] 无法解析K线响应: ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
@@ -902,7 +899,7 @@ export class AsterRestClient {
|
||||
const payload = JSON.parse(text) as any;
|
||||
// The response shape mirrors Binance: { symbol, markPrice, indexPrice, lastFundingRate, nextFundingTime, time }
|
||||
return payload;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
throw new Error(`[AsterRestClient] 无法解析资金费率响应: ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
@@ -945,16 +942,16 @@ export class AsterRestClient {
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
throw new Error(`[AsterRestClient] 无法解析响应: ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type DepthHandler = (depth: Depth) => void;
|
||||
type TickerHandler = (ticker: Ticker) => void;
|
||||
type KlineHandler = (kline: Kline) => void;
|
||||
type DepthHandler = (depth: AsterDepth) => void;
|
||||
type TickerHandler = (ticker: AsterTicker) => void;
|
||||
type KlineHandler = (kline: AsterKline) => void;
|
||||
|
||||
type StreamKind = "depth" | "ticker" | "kline";
|
||||
|
||||
@@ -1247,7 +1244,7 @@ export class AsterUserStream {
|
||||
}
|
||||
}
|
||||
|
||||
function updateAccountSnapshot(snapshot: AccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AccountSnapshot | null {
|
||||
function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AsterAccountSnapshot | null {
|
||||
if (!snapshot) return snapshot;
|
||||
const next = deepCloneAccount(snapshot);
|
||||
if (!next) return snapshot;
|
||||
@@ -1297,7 +1294,7 @@ function updateAccountSnapshot(snapshot: AccountSnapshot | null, event: { eventT
|
||||
return next;
|
||||
}
|
||||
|
||||
function mergeOrderSnapshot(map: Map<string, Order>, order: Order): void {
|
||||
function mergeOrderSnapshot(map: Map<string, AsterOrder>, order: AsterOrder): void {
|
||||
const rawId = order.orderId;
|
||||
if (rawId === undefined || rawId === null) return;
|
||||
const key = String(rawId);
|
||||
@@ -1314,18 +1311,18 @@ export class AsterGateway {
|
||||
private readonly publicStreams: AsterPublicStreams;
|
||||
private readonly userStream: AsterUserStream;
|
||||
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private readonly openOrders = new Map<string, Order>();
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private readonly openOrders = new Map<string, AsterOrder>();
|
||||
private positionSyncTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private positionSyncInFlight = false;
|
||||
|
||||
private readonly accountEvent = new SimpleEvent<AccountSnapshot>();
|
||||
private readonly ordersEvent = new SimpleEvent<Order[]>();
|
||||
private readonly depthEvents = new Map<string, SimpleEvent<Depth>>();
|
||||
private readonly tickerEvents = new Map<string, SimpleEvent<Ticker>>();
|
||||
private readonly klineEvents = new Map<string, SimpleEvent<Kline[]>>();
|
||||
private readonly accountEvent = new SimpleEvent<AsterAccountSnapshot>();
|
||||
private readonly ordersEvent = new SimpleEvent<AsterOrder[]>();
|
||||
private readonly depthEvents = new Map<string, SimpleEvent<AsterDepth>>();
|
||||
private readonly tickerEvents = new Map<string, SimpleEvent<AsterTicker>>();
|
||||
private readonly klineEvents = new Map<string, SimpleEvent<AsterKline[]>>();
|
||||
|
||||
private readonly klineStores = new Map<string, Kline[]>();
|
||||
private readonly klineStores = new Map<string, AsterKline[]>();
|
||||
private readonly klineRefreshTimers = new Map<string, ReturnType<typeof setInterval>>();
|
||||
private readonly klineInitialFetches = new Map<string, Promise<void>>();
|
||||
private initialized = false;
|
||||
@@ -1369,7 +1366,7 @@ export class AsterGateway {
|
||||
});
|
||||
}
|
||||
|
||||
async ensureInitialized(_symbol: string): Promise<void> {
|
||||
async ensureInitialized(symbol: string): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
if (this.initializing) return this.initializing;
|
||||
this.initializing = (async () => {
|
||||
@@ -1384,21 +1381,21 @@ export class AsterGateway {
|
||||
return this.initializing;
|
||||
}
|
||||
|
||||
onAccount(listener: (snapshot: AccountSnapshot) => void): void {
|
||||
onAccount(listener: (snapshot: AsterAccountSnapshot) => void): void {
|
||||
this.accountEvent.add(listener);
|
||||
if (this.accountSnapshot) listener(this.accountSnapshot);
|
||||
}
|
||||
|
||||
onOrders(listener: (orders: Order[]) => void): void {
|
||||
onOrders(listener: (orders: AsterOrder[]) => void): void {
|
||||
this.ordersEvent.add(listener);
|
||||
listener(Array.from(this.openOrders.values()));
|
||||
}
|
||||
|
||||
onDepth(symbol: string, listener: (depth: Depth) => void): void {
|
||||
onDepth(symbol: string, listener: (depth: AsterDepth) => void): void {
|
||||
const upper = symbol.toUpperCase();
|
||||
let event = this.depthEvents.get(upper);
|
||||
if (!event) {
|
||||
event = new SimpleEvent<Depth>();
|
||||
event = new SimpleEvent<AsterDepth>();
|
||||
this.depthEvents.set(upper, event);
|
||||
this.publicStreams.subscribeDepth(upper, (depth) => {
|
||||
event?.emit(depth);
|
||||
@@ -1407,11 +1404,11 @@ export class AsterGateway {
|
||||
event.add(listener);
|
||||
}
|
||||
|
||||
onTicker(symbol: string, listener: (ticker: Ticker) => void): void {
|
||||
onTicker(symbol: string, listener: (ticker: AsterTicker) => void): void {
|
||||
const upper = symbol.toUpperCase();
|
||||
let event = this.tickerEvents.get(upper);
|
||||
if (!event) {
|
||||
event = new SimpleEvent<Ticker>();
|
||||
event = new SimpleEvent<AsterTicker>();
|
||||
this.tickerEvents.set(upper, event);
|
||||
this.publicStreams.subscribeTicker(upper, (ticker) => {
|
||||
event?.emit(ticker);
|
||||
@@ -1420,12 +1417,12 @@ export class AsterGateway {
|
||||
event.add(listener);
|
||||
}
|
||||
|
||||
onKlines(symbol: string, interval: string, listener: (klines: Kline[]) => void): void {
|
||||
onKlines(symbol: string, interval: string, listener: (klines: AsterKline[]) => void): void {
|
||||
const upper = symbol.toUpperCase();
|
||||
const key = `${upper}:${interval}`;
|
||||
let event = this.klineEvents.get(key);
|
||||
if (!event) {
|
||||
event = new SimpleEvent<Kline[]>();
|
||||
event = new SimpleEvent<AsterKline[]>();
|
||||
this.klineEvents.set(key, event);
|
||||
this.publicStreams.subscribeKline(symbol, interval, (kline) => {
|
||||
const storeKey = `${upper}:${interval}`;
|
||||
@@ -1512,7 +1509,7 @@ export class AsterGateway {
|
||||
console.error("[AsterGateway] 刷新持仓失败", positionError);
|
||||
}
|
||||
const normalizedPositions = clonePositions(positions);
|
||||
const snapshot: AccountSnapshot = {
|
||||
const snapshot: AsterAccountSnapshot = {
|
||||
...account,
|
||||
positions: normalizedPositions,
|
||||
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
|
||||
@@ -1550,7 +1547,7 @@ export class AsterGateway {
|
||||
if (!Array.isArray(positions)) return;
|
||||
const normalizedPositions = clonePositions(positions);
|
||||
if (!this.accountSnapshot) {
|
||||
const snapshot: AccountSnapshot = {
|
||||
const snapshot: AsterAccountSnapshot = {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
canWithdraw: true,
|
||||
@@ -1564,7 +1561,7 @@ export class AsterGateway {
|
||||
this.accountEvent.emit(snapshot);
|
||||
return;
|
||||
}
|
||||
const nextSnapshot: AccountSnapshot = {
|
||||
const nextSnapshot: AsterAccountSnapshot = {
|
||||
...this.accountSnapshot,
|
||||
positions: normalizedPositions,
|
||||
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
|
||||
@@ -1579,15 +1576,15 @@ export class AsterGateway {
|
||||
}
|
||||
}
|
||||
|
||||
getAccountSnapshot(): AccountSnapshot | null {
|
||||
getAccountSnapshot(): AsterAccountSnapshot | null {
|
||||
return this.accountSnapshot;
|
||||
}
|
||||
|
||||
getOpenOrdersSnapshot(): Order[] {
|
||||
getOpenOrdersSnapshot(): AsterOrder[] {
|
||||
return Array.from(this.openOrders.values());
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
const normalized = await this.normalizeOrderParams(params);
|
||||
const order = await this.rest.createOrder(normalized);
|
||||
mergeOrderSnapshot(this.openOrders, order);
|
||||
@@ -1,17 +1,101 @@
|
||||
import { createOrderHandlers } from "../order-handlers";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
const handlers = createOrderHandlers({
|
||||
exchangeName: "Aster",
|
||||
defaultLimitTimeInForce: "GTX",
|
||||
supportsTrailingStop: true,
|
||||
supportsTriggerType: true,
|
||||
});
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export const {
|
||||
createLimitOrder,
|
||||
createMarketOrder,
|
||||
createStopOrder,
|
||||
createTrailingStopOrder,
|
||||
createClosePositionOrder,
|
||||
} = handlers;
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "GTX",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "MARKET",
|
||||
quantity: intent.quantity,
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -6,10 +6,9 @@ import type {
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { BackpackGateway, type BackpackGatewayOptions } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export interface BackpackCredentials {
|
||||
apiKey?: string;
|
||||
@@ -24,8 +23,11 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
||||
readonly id = "backpack";
|
||||
private readonly gateway: BackpackGateway;
|
||||
private readonly symbol: string;
|
||||
private readonly safeInvoke = createSafeInvoke("BackpackExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private lastInitErrorAt = 0;
|
||||
|
||||
constructor(credentials: BackpackCredentials = {}) {
|
||||
const apiKey = credentials.apiKey ?? process.env.BACKPACK_API_KEY;
|
||||
@@ -52,9 +54,6 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
this.gateway = new BackpackGateway(gatewayOptions);
|
||||
this.symbol = symbol;
|
||||
this.init = createInitManager("BackpackExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -62,50 +61,118 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
void this.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
void this.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||
void this.init.ensureInitialized("watchDepth");
|
||||
void this.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||
void this.init.ensureInitialized("watchTicker");
|
||||
void this.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.init.ensureInitialized(`watchKlines:${interval}`);
|
||||
void this.ensureInitialized(`watchKlines:${interval}`);
|
||||
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
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.init.ensureInitialized("cancelOrder");
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder({ orderId: params.orderId });
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.init.ensureInitialized("cancelOrders");
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
|
||||
}
|
||||
|
||||
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.ensureInitialized("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 {
|
||||
if (process.env.BACKPACK_DEBUG === "1" || process.env.BACKPACK_DEBUG === "true") {
|
||||
console.error(`[BackpackExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
|
||||
|
||||
@@ -7,13 +7,14 @@ import ccxt, {
|
||||
import NodeWebSocket from "ws";
|
||||
import { sign, utils as edUtils, hashes as edHashes } from "@noble/ed25519";
|
||||
import { sha512 } from "@noble/hashes/sha512";
|
||||
import { randomBytes } from "crypto";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
AccountPosition,
|
||||
Order,
|
||||
Depth,
|
||||
Ticker,
|
||||
Kline,
|
||||
AsterAccountSnapshot,
|
||||
AsterAccountPosition,
|
||||
AsterOrder,
|
||||
AsterDepth,
|
||||
AsterTicker,
|
||||
AsterKline,
|
||||
CreateOrderParams,
|
||||
OrderType,
|
||||
} from "../types";
|
||||
@@ -92,8 +93,8 @@ export class BackpackGateway {
|
||||
private tickerPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private readonly klinePollTimers = new Map<string, ReturnType<typeof setInterval>>();
|
||||
|
||||
private readonly localOrders = new Map<string, Order>();
|
||||
private lastBalanceSnapshot: AccountSnapshot | null = null;
|
||||
private readonly localOrders = new Map<string, AsterOrder>();
|
||||
private lastBalanceSnapshot: AsterAccountSnapshot | null = null;
|
||||
private marketId = "";
|
||||
|
||||
private ws: WebSocket | null = null;
|
||||
@@ -136,16 +137,17 @@ export class BackpackGateway {
|
||||
}
|
||||
|
||||
private async doInitialize(symbol?: string): Promise<void> {
|
||||
await this.exchange.loadMarkets();
|
||||
const requested = (symbol ?? this.symbol).toUpperCase();
|
||||
const market = this.findMarket(requested);
|
||||
if (!market) {
|
||||
throw new Error(`Symbol ${requested} not found in Backpack markets`);
|
||||
}
|
||||
this.market = market;
|
||||
this.marketSymbol = market.symbol;
|
||||
this.marketId = market.id;
|
||||
this.isContractMarket = Boolean(market.contract);
|
||||
try {
|
||||
await this.exchange.loadMarkets();
|
||||
const requested = (symbol ?? this.symbol).toUpperCase();
|
||||
const market = this.findMarket(requested);
|
||||
if (!market) {
|
||||
throw new Error(`Symbol ${requested} not found in Backpack markets`);
|
||||
}
|
||||
this.market = market;
|
||||
this.marketSymbol = market.symbol;
|
||||
this.marketId = market.id;
|
||||
this.isContractMarket = Boolean(market.contract);
|
||||
if (process.env.BACKPACK_DEBUG === "1") {
|
||||
console.debug("[BackpackGateway] marketInfo", {
|
||||
userSymbol: this.symbol,
|
||||
@@ -153,7 +155,10 @@ export class BackpackGateway {
|
||||
marketId: this.marketId,
|
||||
});
|
||||
}
|
||||
this.initialized = true;
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private findMarket(requested: string): any | null {
|
||||
@@ -244,7 +249,7 @@ export class BackpackGateway {
|
||||
this.exchange.fetchOpenOrders(this.marketSymbol),
|
||||
this.exchange.fetchOrders(this.marketSymbol, undefined, 200, {}),
|
||||
]);
|
||||
const active = new Map<string, Order>();
|
||||
const active = new Map<string, AsterOrder>();
|
||||
for (const entry of [...openOrders, ...allOrders]) {
|
||||
const status = this.normalizeStatus(entry.status ?? (entry.info?.status as string));
|
||||
if (this.isTerminalStatus(status)) continue;
|
||||
@@ -315,7 +320,7 @@ export class BackpackGateway {
|
||||
|
||||
// ---- Order actions -----------------------------------------------------
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
await this.ensureInitialized();
|
||||
const symbol = this.marketSymbol;
|
||||
const normalizedType = this.normalizeOrderType(params.type);
|
||||
@@ -399,11 +404,11 @@ export class BackpackGateway {
|
||||
|
||||
// ---- Mapping helpers ---------------------------------------------------
|
||||
|
||||
private mapBalanceToAccountSnapshot(balance: Balances): AccountSnapshot {
|
||||
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
|
||||
return this.mapBalanceToAccountSnapshotWithPositions(balance, []);
|
||||
}
|
||||
|
||||
private async fetchAccountSnapshot(): Promise<AccountSnapshot> {
|
||||
private async fetchAccountSnapshot(): Promise<AsterAccountSnapshot> {
|
||||
await this.ensureInitialized();
|
||||
const [balance, positions] = await Promise.all([
|
||||
this.exchange.fetchBalance(),
|
||||
@@ -417,7 +422,7 @@ export class BackpackGateway {
|
||||
return this.mapBalanceToAccountSnapshotWithPositions(balance, positions ?? []);
|
||||
}
|
||||
|
||||
private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AccountSnapshot {
|
||||
private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AsterAccountSnapshot {
|
||||
const now = Date.now();
|
||||
const assets = this.normalizeAssets(balance, now);
|
||||
const positions = this.normalizePositions(rawPositions, now);
|
||||
@@ -425,7 +430,7 @@ export class BackpackGateway {
|
||||
const totalUnrealized = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0"));
|
||||
const availableBalance = this.sumStrings(assets.map((asset) => asset.availableBalance));
|
||||
|
||||
const snapshot: AccountSnapshot = {
|
||||
const snapshot: AsterAccountSnapshot = {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
canWithdraw: true,
|
||||
@@ -447,9 +452,9 @@ export class BackpackGateway {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private normalizeAssets(balance: Balances, now: number): AccountSnapshot["assets"] {
|
||||
private normalizeAssets(balance: Balances, now: number): AsterAccountSnapshot["assets"] {
|
||||
const metaKeys = new Set(["free", "used", "total", "info", "timestamp", "datetime", "debt"]);
|
||||
const assets: AccountSnapshot["assets"] = [];
|
||||
const assets: AsterAccountSnapshot["assets"] = [];
|
||||
for (const [currency, value] of Object.entries(balance)) {
|
||||
if (metaKeys.has(currency)) continue;
|
||||
if (!value || typeof value !== "object") continue;
|
||||
@@ -460,29 +465,27 @@ export class BackpackGateway {
|
||||
return assets;
|
||||
}
|
||||
|
||||
private normalizePositions(rawPositions: any[], now: number): AccountSnapshot["positions"] {
|
||||
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] {
|
||||
if (!Array.isArray(rawPositions)) return [];
|
||||
const positions: AccountSnapshot["positions"] = [];
|
||||
const positions: AsterAccountSnapshot["positions"] = [];
|
||||
for (const raw of rawPositions) {
|
||||
const info = raw?.info ?? raw ?? {};
|
||||
const quantity = this.toNumber(raw?.contracts ?? info.netExposureQuantity ?? info.netQuantity);
|
||||
if (!quantity) continue;
|
||||
const sideRaw = String(raw?.side ?? info.side ?? this.deriveSideFromExposure(info)).toLowerCase();
|
||||
const isShort = sideRaw.includes("short") || quantity < 0;
|
||||
const isLong = sideRaw.includes("long") || (!sideRaw.includes("short") && quantity > 0);
|
||||
const positionAmt = isShort ? -Math.abs(quantity) : Math.abs(quantity);
|
||||
const entryPrice = this.toStringAmount(raw?.entryPrice ?? info.entryPrice ?? "0");
|
||||
const unrealized = this.toStringAmount(raw?.unrealizedPnl ?? info.pnlUnrealized ?? "0");
|
||||
const markPrice = this.toOptionalString(raw?.markPrice ?? info.markPrice);
|
||||
const liquidationPrice = this.toOptionalString(raw?.liquidationPrice ?? info.estLiquidationPrice);
|
||||
const leverage = this.toOptionalString(raw?.leverage ?? info.leverage);
|
||||
const symbol = String(raw?.symbol ?? info.symbol ?? this.marketSymbol ?? this.symbol);
|
||||
positions.push({
|
||||
symbol,
|
||||
symbol: this.symbol,
|
||||
positionAmt: positionAmt.toString(),
|
||||
entryPrice,
|
||||
unrealizedProfit: unrealized,
|
||||
positionSide: isLong ? "LONG" : isShort ? "SHORT" : "BOTH",
|
||||
positionSide: "BOTH",
|
||||
updateTime: now,
|
||||
markPrice,
|
||||
liquidationPrice,
|
||||
@@ -493,7 +496,7 @@ export class BackpackGateway {
|
||||
return positions;
|
||||
}
|
||||
|
||||
private mapOrderBookToDepth(orderbook: CcxtOrderBook): Depth {
|
||||
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
|
||||
return {
|
||||
lastUpdateId: orderbook.nonce || Date.now(),
|
||||
bids: (orderbook.bids ?? [])
|
||||
@@ -506,7 +509,7 @@ export class BackpackGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapTickerToAsterTicker(ticker: CcxtTicker): Ticker {
|
||||
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
|
||||
return {
|
||||
symbol: ticker.symbol,
|
||||
lastPrice: ticker.last?.toString() ?? "0",
|
||||
@@ -522,7 +525,7 @@ export class BackpackGateway {
|
||||
private mapOHLCVToKline(
|
||||
candle: [number, number, number, number, number, number],
|
||||
interval: string
|
||||
): Kline {
|
||||
): AsterKline {
|
||||
const [openTime, open, high, low, close, volume] = candle;
|
||||
return {
|
||||
openTime,
|
||||
@@ -536,7 +539,7 @@ export class BackpackGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapRestOrder(order: CcxtOrder): Order {
|
||||
private mapRestOrder(order: CcxtOrder): AsterOrder {
|
||||
const info = (order.info ?? {}) as Record<string, unknown>;
|
||||
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
|
||||
let type = this.normalizeOrderType(order.type ?? (info.o as string)) as OrderType;
|
||||
@@ -579,7 +582,7 @@ export class BackpackGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapWsOrder(data: Record<string, unknown>): Order {
|
||||
private mapWsOrder(data: Record<string, unknown>): AsterOrder {
|
||||
const sideRaw = String(data.S ?? "").toUpperCase();
|
||||
const side: "BUY" | "SELL" = sideRaw === "BID" ? "BUY" : "SELL";
|
||||
const triggerPresent = data.P != null || data.B != null;
|
||||
@@ -626,7 +629,7 @@ export class BackpackGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private mapWsPosition(data: Record<string, unknown>): AccountPosition | null {
|
||||
private mapWsPosition(data: Record<string, unknown>): AsterAccountPosition | null {
|
||||
const quantityRaw = data.q ?? data.Q;
|
||||
const qty = Number(this.toStringAmount(quantityRaw));
|
||||
if (!Number.isFinite(qty)) return null;
|
||||
@@ -651,8 +654,8 @@ export class BackpackGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mergeWsPosition(position: AccountPosition): void {
|
||||
const snapshot: AccountSnapshot = this.lastBalanceSnapshot
|
||||
private mergeWsPosition(position: AsterAccountPosition): void {
|
||||
const snapshot: AsterAccountSnapshot = this.lastBalanceSnapshot
|
||||
? {
|
||||
...this.lastBalanceSnapshot,
|
||||
positions: this.lastBalanceSnapshot.positions ? [...this.lastBalanceSnapshot.positions] : [],
|
||||
@@ -692,7 +695,7 @@ export class BackpackGateway {
|
||||
this.emitAccount(snapshot);
|
||||
}
|
||||
|
||||
private emitAccount(snapshot: AccountSnapshot): void {
|
||||
private emitAccount(snapshot: AsterAccountSnapshot): void {
|
||||
for (const listener of this.accountListeners) {
|
||||
try {
|
||||
listener(snapshot);
|
||||
@@ -769,7 +772,7 @@ export class BackpackGateway {
|
||||
void this.resubscribeAllTopics();
|
||||
};
|
||||
|
||||
private handleWsClose = (_event: any): void => {
|
||||
private handleWsClose = (event: any): void => {
|
||||
if (this.wsCleanup) {
|
||||
try {
|
||||
this.wsCleanup();
|
||||
|
||||
@@ -1,16 +1,84 @@
|
||||
import { createOrderHandlers } from "../order-handlers";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
const handlers = createOrderHandlers({
|
||||
exchangeName: "Backpack",
|
||||
defaultLimitTimeInForce: "GTX",
|
||||
supportsTrailingStop: false,
|
||||
supportsTriggerType: false,
|
||||
});
|
||||
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);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export const {
|
||||
createLimitOrder,
|
||||
createMarketOrder,
|
||||
createStopOrder,
|
||||
createTrailingStopOrder,
|
||||
createClosePositionOrder,
|
||||
} = handlers;
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "GTX",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "MARKET",
|
||||
quantity: intent.quantity,
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { clearTimeout, setTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
@@ -8,9 +9,9 @@ import type {
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { BinanceGateway, type BinanceGatewayOptions } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export interface BinanceCredentials {
|
||||
apiKey?: string;
|
||||
@@ -31,8 +32,11 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
||||
private readonly gateway: BinanceGateway;
|
||||
private readonly symbol: string;
|
||||
private readonly marketType: "spot" | "perp" | "auto";
|
||||
private readonly safeInvoke = createSafeInvoke("BinanceExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private lastInitErrorAt = 0;
|
||||
|
||||
constructor(credentials: BinanceCredentials = {}) {
|
||||
const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY;
|
||||
@@ -57,9 +61,6 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
||||
futuresWsUrl: credentials.futuresWsUrl,
|
||||
logger: credentials.logger,
|
||||
});
|
||||
this.init = createInitManager("BinanceExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -68,94 +69,163 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
const safe = this.safeInvoke("watchAccount", cb);
|
||||
void this.init.ensureInitialized("watchAccount")
|
||||
void this.ensureInitialized("watchAccount")
|
||||
.then(() => {
|
||||
this.gateway.onAccount(safe);
|
||||
});
|
||||
})
|
||||
.catch((error) => this.handleInitError("watchAccount", error));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
const safe = this.safeInvoke("watchOrders", cb);
|
||||
void this.init.ensureInitialized("watchOrders")
|
||||
void this.ensureInitialized("watchOrders")
|
||||
.then(() => {
|
||||
this.gateway.onOrders(safe);
|
||||
});
|
||||
})
|
||||
.catch((error) => this.handleInitError("watchOrders", error));
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
const safe = this.safeInvoke("watchDepth", cb);
|
||||
void this.init.ensureInitialized(`watchDepth:${symbol}`)
|
||||
void this.ensureInitialized(`watchDepth:${symbol}`)
|
||||
.then(() => {
|
||||
this.gateway.onDepth(symbol, safe);
|
||||
});
|
||||
})
|
||||
.catch((error) => this.handleInitError("watchDepth", error));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
const safe = this.safeInvoke("watchTicker", cb);
|
||||
void this.init.ensureInitialized(`watchTicker:${symbol}`)
|
||||
void this.ensureInitialized(`watchTicker:${symbol}`)
|
||||
.then(() => {
|
||||
this.gateway.onTicker(symbol, safe);
|
||||
});
|
||||
})
|
||||
.catch((error) => this.handleInitError("watchTicker", error));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
const safe = this.safeInvoke("watchKlines", cb);
|
||||
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`)
|
||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`)
|
||||
.then(() => {
|
||||
this.gateway.onKlines(symbol, interval, safe);
|
||||
});
|
||||
})
|
||||
.catch((error) => this.handleInitError("watchKlines", error));
|
||||
}
|
||||
|
||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||
const safe = this.safeInvoke("watchFundingRate", cb);
|
||||
void this.init.ensureInitialized(`watchFundingRate:${symbol}`)
|
||||
void this.ensureInitialized(`watchFundingRate:${symbol}`)
|
||||
.then(() => {
|
||||
this.gateway.onFundingRate(symbol, safe);
|
||||
});
|
||||
})
|
||||
.catch((error) => this.handleInitError("watchFundingRate", error));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
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.init.ensureInitialized("cancelOrder");
|
||||
await this.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.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
async getPrecision(): Promise<ExchangePrecision | null> {
|
||||
await this.init.ensureInitialized("getPrecision");
|
||||
await this.ensureInitialized("getPrecision");
|
||||
return this.gateway.getPrecision(this.symbol);
|
||||
}
|
||||
|
||||
async queryOpenOrders(): Promise<Order[]> {
|
||||
await this.init.ensureInitialized("queryOpenOrders");
|
||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
||||
await this.ensureInitialized("queryOpenOrders");
|
||||
return this.gateway.queryOpenOrders();
|
||||
}
|
||||
|
||||
async queryAccountSnapshot() {
|
||||
await this.init.ensureInitialized("queryAccountSnapshot");
|
||||
await this.ensureInitialized("queryAccountSnapshot");
|
||||
return this.gateway.queryAccountSnapshot();
|
||||
}
|
||||
|
||||
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||
await this.init.ensureInitialized("changeMarginMode");
|
||||
await this.ensureInitialized("changeMarginMode");
|
||||
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
||||
}
|
||||
|
||||
async forceCancelAllOrders(): Promise<boolean> {
|
||||
await this.init.ensureInitialized("forceCancelAllOrders");
|
||||
await this.ensureInitialized("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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import ccxt, {
|
||||
type Balances,
|
||||
type Order as CcxtOrder,
|
||||
type OrderBook as CcxtOrderBook,
|
||||
type OHLCV as CcxtOhlcv,
|
||||
type Ticker as CcxtTicker,
|
||||
} from "ccxt";
|
||||
import axios from "axios";
|
||||
import { createHash } from "crypto";
|
||||
import NodeWebSocket from "ws";
|
||||
import type {
|
||||
AccountAsset,
|
||||
AccountPosition,
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountAsset,
|
||||
AsterAccountPosition,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
OrderType,
|
||||
PositionSide,
|
||||
@@ -225,9 +228,9 @@ export class BinanceGateway {
|
||||
|
||||
private readonly accountListeners = new Set<AccountListener>();
|
||||
private readonly orderListeners = new Set<OrderListener>();
|
||||
private readonly depthSubs = new Map<string, PublicSubscription<Depth>>();
|
||||
private readonly tickerSubs = new Map<string, PublicSubscription<Ticker>>();
|
||||
private readonly klineSubs = new Map<string, PublicSubscription<Kline[]>>();
|
||||
private readonly depthSubs = new Map<string, PublicSubscription<AsterDepth>>();
|
||||
private readonly tickerSubs = new Map<string, PublicSubscription<AsterTicker>>();
|
||||
private readonly klineSubs = new Map<string, PublicSubscription<AsterKline[]>>();
|
||||
private readonly fundingSubs = new Map<string, PublicSubscription<{ symbol: string; fundingRate: number; updateTime: number }>>();
|
||||
|
||||
private accountPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -254,15 +257,15 @@ export class BinanceGateway {
|
||||
},
|
||||
};
|
||||
|
||||
private readonly localOrders = new Map<string, Order>();
|
||||
private readonly localOrders = new Map<string, AsterOrder>();
|
||||
|
||||
private readonly spotBalances = new Map<string, { free: number; locked: number }>();
|
||||
private readonly perpBalances = new Map<string, { wallet: number; available: number }>();
|
||||
private readonly perpPositions = new Map<string, AccountPosition>();
|
||||
private readonly perpPositions = new Map<string, AsterAccountPosition>();
|
||||
private readonly lastMarkPriceBySymbol = new Map<string, number>();
|
||||
|
||||
private lastSpotSnapshot: AccountSnapshot | null = null;
|
||||
private lastPerpSnapshot: AccountSnapshot | null = null;
|
||||
private lastSpotSnapshot: AsterAccountSnapshot | null = null;
|
||||
private lastPerpSnapshot: AsterAccountSnapshot | null = null;
|
||||
|
||||
constructor(options: BinanceGatewayOptions) {
|
||||
this.apiKey = options.apiKey;
|
||||
@@ -465,7 +468,7 @@ export class BinanceGateway {
|
||||
});
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
await this.ensureInitialized(params.symbol);
|
||||
const market = this.resolveMarket(params.symbol);
|
||||
const exchange = this.getExchange(market.kind);
|
||||
@@ -487,9 +490,6 @@ export class BinanceGateway {
|
||||
if (params.callbackRate != null) {
|
||||
extra.callbackRate = params.callbackRate;
|
||||
}
|
||||
if (params.clientOrderId != null) {
|
||||
extra.newClientOrderId = params.clientOrderId;
|
||||
}
|
||||
|
||||
if (market.kind === "perp") {
|
||||
if (params.reduceOnly != null) {
|
||||
@@ -604,10 +604,10 @@ export class BinanceGateway {
|
||||
};
|
||||
}
|
||||
|
||||
async queryOpenOrders(): Promise<Order[]> {
|
||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
||||
await this.ensureInitialized(this.defaultSymbol);
|
||||
const kinds = this.getPrivateKinds();
|
||||
const result: Order[] = [];
|
||||
const result: AsterOrder[] = [];
|
||||
for (const kind of kinds) {
|
||||
const exchange = this.getExchange(kind);
|
||||
const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[];
|
||||
@@ -622,7 +622,7 @@ export class BinanceGateway {
|
||||
return result;
|
||||
}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
await this.ensureInitialized(this.defaultSymbol);
|
||||
const kinds = this.getPrivateKinds();
|
||||
for (const kind of kinds) {
|
||||
@@ -913,7 +913,7 @@ export class BinanceGateway {
|
||||
this.lastPerpSnapshot = this.buildPerpSnapshot();
|
||||
}
|
||||
|
||||
private mapSpotExecutionReport(payload: any): Order | null {
|
||||
private mapSpotExecutionReport(payload: any): AsterOrder | null {
|
||||
const order = payload;
|
||||
const symbolRaw = String(order?.s ?? "").toUpperCase();
|
||||
if (!symbolRaw) return null;
|
||||
@@ -943,7 +943,7 @@ export class BinanceGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapPerpOrderTradeUpdate(payload: any): Order | null {
|
||||
private mapPerpOrderTradeUpdate(payload: any): AsterOrder | null {
|
||||
const order = payload?.o;
|
||||
if (!order) return null;
|
||||
const symbolRaw = String(order?.s ?? "").toUpperCase();
|
||||
@@ -1114,7 +1114,7 @@ export class BinanceGateway {
|
||||
(ws as any).onerror = (event: any) => onError(event?.error ?? event);
|
||||
}
|
||||
|
||||
private mapDepthPayload(payload: unknown, market: BinanceMarketRef): Depth | null {
|
||||
private mapDepthPayload(payload: unknown, market: BinanceMarketRef): AsterDepth | null {
|
||||
const data = payload as any;
|
||||
const bids = Array.isArray(data?.b) ? data.b : [];
|
||||
const asks = Array.isArray(data?.a) ? data.a : [];
|
||||
@@ -1132,7 +1132,7 @@ export class BinanceGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapTickerPayload(payload: unknown, market: BinanceMarketRef): Ticker | null {
|
||||
private mapTickerPayload(payload: unknown, market: BinanceMarketRef): AsterTicker | null {
|
||||
const data = payload as any;
|
||||
const lastPrice = String(data?.c ?? "");
|
||||
if (!lastPrice) return null;
|
||||
@@ -1159,7 +1159,7 @@ export class BinanceGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapKlinePayload(payload: unknown, interval: string): Kline[] | null {
|
||||
private mapKlinePayload(payload: unknown, interval: string): AsterKline[] | null {
|
||||
const data = payload as any;
|
||||
const kline = data?.k;
|
||||
if (!kline) return null;
|
||||
@@ -1291,7 +1291,7 @@ export class BinanceGateway {
|
||||
}
|
||||
|
||||
private async attachPerpPositions(): Promise<void> {
|
||||
const next = new Map<string, AccountPosition>();
|
||||
const next = new Map<string, AsterAccountPosition>();
|
||||
try {
|
||||
const raw = (await this.perpExchange.fetchPositions()) as any[];
|
||||
for (const row of raw ?? []) {
|
||||
@@ -1328,7 +1328,7 @@ export class BinanceGateway {
|
||||
private async fetchAndUpdateOrders(kind: MarketKind): Promise<void> {
|
||||
const exchange = this.getExchange(kind);
|
||||
const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[];
|
||||
const remote = new Map<string, Order>();
|
||||
const remote = new Map<string, AsterOrder>();
|
||||
for (const order of openOrders) {
|
||||
const market = this.resolveMarketByCcxtSymbol(kind, String(order?.symbol ?? ""));
|
||||
const mapped = this.mapCcxtOrder(order, kind, market?.id ?? String(order?.symbol ?? ""));
|
||||
@@ -1370,7 +1370,7 @@ export class BinanceGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private upsertOrder(order: Order, kind: MarketKind, marketId: string): void {
|
||||
private upsertOrder(order: AsterOrder, kind: MarketKind, marketId: string): void {
|
||||
const id = String(order.orderId);
|
||||
const symbol = this.resolveDisplaySymbol(kind, marketId);
|
||||
const normalized = { ...order, symbol };
|
||||
@@ -1405,7 +1405,7 @@ export class BinanceGateway {
|
||||
if (changed) this.emitOrders();
|
||||
}
|
||||
|
||||
private isOrderActive(order: Order): boolean {
|
||||
private isOrderActive(order: AsterOrder): boolean {
|
||||
const status = String(order.status ?? "").toUpperCase();
|
||||
if (!status) return true;
|
||||
if (status === "FILLED" || status === "CANCELED" || status === "CANCELLED" || status === "REJECTED" || status === "EXPIRED") {
|
||||
@@ -1415,7 +1415,7 @@ export class BinanceGateway {
|
||||
return true;
|
||||
}
|
||||
|
||||
private buildCombinedAccountSnapshot(): AccountSnapshot | null {
|
||||
private buildCombinedAccountSnapshot(): AsterAccountSnapshot | null {
|
||||
const kinds = this.getPrivateKinds();
|
||||
if (kinds.length === 0) return null;
|
||||
if (kinds.length === 1) {
|
||||
@@ -1424,7 +1424,7 @@ export class BinanceGateway {
|
||||
|
||||
const spot = this.buildSpotSnapshot();
|
||||
const perp = this.buildPerpSnapshot();
|
||||
const perpAssetsTagged: AccountAsset[] = perp.assets.map((asset) => ({
|
||||
const perpAssetsTagged: AsterAccountAsset[] = perp.assets.map((asset) => ({
|
||||
...asset,
|
||||
asset: `${asset.asset}0`,
|
||||
}));
|
||||
@@ -1444,8 +1444,8 @@ export class BinanceGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private buildSpotSnapshot(): AccountSnapshot {
|
||||
const assets: AccountAsset[] = [];
|
||||
private buildSpotSnapshot(): AsterAccountSnapshot {
|
||||
const assets: AsterAccountAsset[] = [];
|
||||
let totalWallet = 0;
|
||||
const now = Date.now();
|
||||
for (const [asset, balance] of this.spotBalances.entries()) {
|
||||
@@ -1474,9 +1474,9 @@ export class BinanceGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private buildPerpSnapshot(): AccountSnapshot {
|
||||
private buildPerpSnapshot(): AsterAccountSnapshot {
|
||||
const now = Date.now();
|
||||
const assets: AccountAsset[] = [];
|
||||
const assets: AsterAccountAsset[] = [];
|
||||
let totalWallet = 0;
|
||||
for (const [asset, balance] of this.perpBalances.entries()) {
|
||||
totalWallet += balance.wallet;
|
||||
@@ -1509,7 +1509,7 @@ export class BinanceGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapCcxtOrder(order: CcxtOrder, kind: MarketKind, marketId: string): Order {
|
||||
private mapCcxtOrder(order: CcxtOrder, kind: MarketKind, marketId: string): AsterOrder {
|
||||
const symbol = this.resolveDisplaySymbol(kind, marketId);
|
||||
const side = String(order.side ?? "buy").toUpperCase() === "SELL" ? "SELL" : "BUY";
|
||||
return {
|
||||
|
||||
@@ -1,16 +1,100 @@
|
||||
import { createOrderHandlers } from "../order-handlers";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
const handlers = createOrderHandlers({
|
||||
exchangeName: "Binance",
|
||||
defaultLimitTimeInForce: "GTX",
|
||||
supportsTrailingStop: true,
|
||||
supportsTriggerType: true,
|
||||
});
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export const {
|
||||
createLimitOrder,
|
||||
createMarketOrder,
|
||||
createStopOrder,
|
||||
createTrailingStopOrder,
|
||||
createClosePositionOrder,
|
||||
} = handlers;
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "GTX",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "MARKET",
|
||||
quantity: intent.quantity,
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
|
||||
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
|
||||
@@ -7,7 +7,6 @@ import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapt
|
||||
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
|
||||
import { StandxExchangeAdapter, type StandxCredentials } from "./standx/adapter";
|
||||
import { BinanceExchangeAdapter, type BinanceCredentials } from "./binance/adapter";
|
||||
import { OndoperpsExchangeAdapter, type OndoperpsCredentials } from "./ondoperps/adapter";
|
||||
|
||||
export const SUPPORTED_EXCHANGE_IDS = [
|
||||
"aster",
|
||||
@@ -18,7 +17,6 @@ export const SUPPORTED_EXCHANGE_IDS = [
|
||||
"nado",
|
||||
"standx",
|
||||
"binance",
|
||||
"ondoperps",
|
||||
] as const;
|
||||
|
||||
export const BASIS_SUPPORTED_EXCHANGE_IDS = [
|
||||
@@ -39,8 +37,6 @@ export interface ExchangeFactoryOptions {
|
||||
nado?: NadoCredentials;
|
||||
standx?: StandxCredentials;
|
||||
binance?: BinanceCredentials;
|
||||
ondoperps?: OndoperpsCredentials;
|
||||
ondoperp?: OndoperpsCredentials;
|
||||
}
|
||||
|
||||
export type SupportedExchangeId = (typeof SUPPORTED_EXCHANGE_IDS)[number];
|
||||
@@ -55,7 +51,6 @@ const EXCHANGE_DISPLAY_NAME: Record<SupportedExchangeId, string> = {
|
||||
nado: "Nado",
|
||||
standx: "StandX",
|
||||
binance: "Binance",
|
||||
ondoperps: "Ondo Perps",
|
||||
};
|
||||
|
||||
const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
|
||||
@@ -68,9 +63,6 @@ const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
|
||||
standx: "standx",
|
||||
binance: "binance",
|
||||
bnb: "binance",
|
||||
ondoperps: "ondoperps",
|
||||
ondoperp: "ondoperps",
|
||||
ondo: "ondoperps",
|
||||
};
|
||||
|
||||
export function isSupportedExchangeId(value: string): value is SupportedExchangeId {
|
||||
@@ -112,10 +104,5 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
|
||||
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
|
||||
case "binance":
|
||||
return new BinanceExchangeAdapter({ ...options.binance, symbol: options.symbol });
|
||||
case "ondoperps":
|
||||
return new OndoperpsExchangeAdapter({
|
||||
...(options.ondoperps ?? options.ondoperp),
|
||||
symbol: options.symbol,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import type {
|
||||
AccountListener,
|
||||
ConnectionEventListener,
|
||||
DepthListener,
|
||||
ExchangeAdapter,
|
||||
ExchangePrecision,
|
||||
FundingRateListener,
|
||||
KlineListener,
|
||||
OrderListener,
|
||||
RestHealthListener,
|
||||
TickerListener,
|
||||
} from "./adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Order,
|
||||
CreateOrderParams,
|
||||
} from "./types";
|
||||
|
||||
export interface DryRunAction {
|
||||
method:
|
||||
| "createOrder"
|
||||
| "cancelOrder"
|
||||
| "cancelOrders"
|
||||
| "cancelAllOrders"
|
||||
| "changeMarginMode"
|
||||
| "forceCancelAllOrders";
|
||||
params: unknown;
|
||||
at: number;
|
||||
}
|
||||
|
||||
export class DryRunExchangeAdapter implements ExchangeAdapter {
|
||||
readonly id: string;
|
||||
readonly actions: DryRunAction[] = [];
|
||||
private syntheticCounter = 0;
|
||||
|
||||
constructor(private readonly inner: ExchangeAdapter) {
|
||||
this.id = inner.id;
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return this.inner.supportsTrailingStops();
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
this.inner.watchAccount(cb);
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
this.inner.watchOrders(cb);
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
this.inner.watchDepth(symbol, cb);
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
this.inner.watchTicker(symbol, cb);
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
this.inner.watchKlines(symbol, interval, cb);
|
||||
}
|
||||
|
||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||
if (!this.inner.watchFundingRate) return;
|
||||
this.inner.watchFundingRate(symbol, cb);
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
this.record("createOrder", params);
|
||||
return createSyntheticOrder(params, ++this.syntheticCounter);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
this.record("cancelOrder", params);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
this.record("cancelOrders", params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
this.record("cancelAllOrders", params);
|
||||
}
|
||||
|
||||
async getPrecision(): Promise<ExchangePrecision | null> {
|
||||
if (!this.inner.getPrecision) return null;
|
||||
return this.inner.getPrecision();
|
||||
}
|
||||
|
||||
onConnectionEvent(listener: ConnectionEventListener): void {
|
||||
this.inner.onConnectionEvent?.(listener);
|
||||
}
|
||||
|
||||
offConnectionEvent(listener: ConnectionEventListener): void {
|
||||
this.inner.offConnectionEvent?.(listener);
|
||||
}
|
||||
|
||||
onRestHealthEvent(listener: RestHealthListener): void {
|
||||
this.inner.onRestHealthEvent?.(listener);
|
||||
}
|
||||
|
||||
offRestHealthEvent(listener: RestHealthListener): void {
|
||||
this.inner.offRestHealthEvent?.(listener);
|
||||
}
|
||||
|
||||
async queryOpenOrders(): Promise<Order[]> {
|
||||
if (!this.inner.queryOpenOrders) return [];
|
||||
return this.inner.queryOpenOrders();
|
||||
}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
if (!this.inner.queryAccountSnapshot) return null;
|
||||
return this.inner.queryAccountSnapshot();
|
||||
}
|
||||
|
||||
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||
this.record("changeMarginMode", params);
|
||||
}
|
||||
|
||||
async forceCancelAllOrders(): Promise<boolean> {
|
||||
this.record("forceCancelAllOrders", {});
|
||||
return true;
|
||||
}
|
||||
|
||||
private record(method: DryRunAction["method"], params: unknown): void {
|
||||
this.actions.push({ method, params, at: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
function createSyntheticOrder(params: CreateOrderParams, counter: number): Order {
|
||||
const now = Date.now();
|
||||
const orderId = params.clientOrderId ?? `dry-run-${now}-${counter}`;
|
||||
return {
|
||||
orderId,
|
||||
clientOrderId: params.clientOrderId ?? orderId,
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
type: params.type,
|
||||
status: "NEW",
|
||||
price: toStringNumber(params.price),
|
||||
origQty: toStringNumber(params.quantity),
|
||||
executedQty: "0",
|
||||
stopPrice: toStringNumber(params.stopPrice),
|
||||
time: now,
|
||||
updateTime: now,
|
||||
reduceOnly: params.reduceOnly === "true",
|
||||
closePosition: params.closePosition === "true",
|
||||
activationPrice: toOptionalString(params.activationPrice),
|
||||
timeInForce: params.timeInForce,
|
||||
workingType: "MARK_PRICE",
|
||||
};
|
||||
}
|
||||
|
||||
function toStringNumber(value: number | undefined): string {
|
||||
if (value == null) return "0";
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function toOptionalString(value: number | undefined): string | undefined {
|
||||
if (value == null) return undefined;
|
||||
return String(value);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import path from "path";
|
||||
import { createRequire } from "module";
|
||||
import type {
|
||||
@@ -8,7 +9,8 @@ import type {
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import {
|
||||
GrvtGateway,
|
||||
type GrvtEnvironment,
|
||||
@@ -16,7 +18,6 @@ import {
|
||||
type GrvtHostsOverride,
|
||||
type GrvtSignatureProvider,
|
||||
} from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export interface GrvtCredentials {
|
||||
cookie?: string;
|
||||
@@ -39,8 +40,11 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
||||
private readonly gateway: GrvtGateway;
|
||||
private readonly symbol: string;
|
||||
private readonly instrument: string;
|
||||
private readonly safeInvoke = createSafeInvoke("GrvtExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private lastInitErrorAt = 0;
|
||||
private klineInterval = "1m";
|
||||
|
||||
constructor(credentials: GrvtCredentials = {}) {
|
||||
@@ -87,9 +91,6 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
||||
pollIntervals: credentials.pollIntervals,
|
||||
logger: credentials.logger,
|
||||
});
|
||||
this.init = createInitManager("GrvtExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.klineInterval),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -97,50 +98,115 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
void this.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
void this.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||
void this.init.ensureInitialized("watchDepth");
|
||||
void this.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||
void this.init.ensureInitialized("watchTicker");
|
||||
void this.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||
this.klineInterval = interval ?? this.klineInterval;
|
||||
void this.init.ensureInitialized("watchKlines");
|
||||
void this.ensureInitialized("watchKlines", this.klineInterval);
|
||||
this.gateway.onKlines(this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
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.init.ensureInitialized("cancelOrder");
|
||||
await this.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.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.ensureInitialized("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 {
|
||||
|
||||
@@ -23,23 +23,21 @@ import type {
|
||||
IOrder,
|
||||
} from "@grvt/client/interfaces";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
AccountPosition,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountSnapshot,
|
||||
AsterAccountPosition,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
OrderSide,
|
||||
} from "../types";
|
||||
import type {
|
||||
GrvtSignedOrder,
|
||||
GrvtSignature,
|
||||
GrvtUnsignedOrder,
|
||||
GrvtTimeInForce,
|
||||
GrvtOrderMetadataInput,
|
||||
GrvtTriggerMetadata,
|
||||
} from "./types";
|
||||
} from "../types";
|
||||
|
||||
const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000;
|
||||
const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500;
|
||||
@@ -174,7 +172,7 @@ interface HostsConfig {
|
||||
|
||||
interface KlineCache {
|
||||
interval: string;
|
||||
values: Kline[];
|
||||
values: AsterKline[];
|
||||
}
|
||||
|
||||
interface InstrumentInfo {
|
||||
@@ -217,12 +215,12 @@ export class GrvtGateway {
|
||||
private instrumentInfo: InstrumentInfo | null = null;
|
||||
private sessionPromise: Promise<void> | null = null;
|
||||
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private openOrders: Order[] = [];
|
||||
private positions: AccountPosition[] = [];
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private openOrders: AsterOrder[] = [];
|
||||
private positions: AsterAccountPosition[] = [];
|
||||
private lastPositionsUpdateAt: number = 0;
|
||||
private depthSnapshot: Depth | null = null;
|
||||
private tickerSnapshot: Ticker | null = null;
|
||||
private depthSnapshot: AsterDepth | null = null;
|
||||
private tickerSnapshot: AsterTicker | null = null;
|
||||
private klineCache: KlineCache | null = null;
|
||||
|
||||
// WebSocket state
|
||||
@@ -230,11 +228,11 @@ export class GrvtGateway {
|
||||
private wsConnected = false;
|
||||
private positionsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
private accountListeners = new Set<(snapshot: AccountSnapshot) => void>();
|
||||
private ordersListeners = new Set<(orders: Order[]) => void>();
|
||||
private depthListeners = new Set<(depth: Depth) => void>();
|
||||
private tickerListeners = new Set<(ticker: Ticker) => void>();
|
||||
private klineListeners = new Set<(klines: Kline[]) => void>();
|
||||
private accountListeners = new Set<(snapshot: AsterAccountSnapshot) => void>();
|
||||
private ordersListeners = new Set<(orders: AsterOrder[]) => void>();
|
||||
private depthListeners = new Set<(depth: AsterDepth) => void>();
|
||||
private tickerListeners = new Set<(ticker: AsterTicker) => void>();
|
||||
private klineListeners = new Set<(klines: AsterKline[]) => void>();
|
||||
|
||||
private accountTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private ordersTimer: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -318,7 +316,7 @@ export class GrvtGateway {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
onAccount(listener: (snapshot: AccountSnapshot) => void): () => void {
|
||||
onAccount(listener: (snapshot: AsterAccountSnapshot) => void): () => void {
|
||||
this.accountListeners.add(listener);
|
||||
if (this.accountSnapshot) listener(cloneAccount(this.accountSnapshot));
|
||||
return () => {
|
||||
@@ -326,7 +324,7 @@ export class GrvtGateway {
|
||||
};
|
||||
}
|
||||
|
||||
onOrders(listener: (orders: Order[]) => void): () => void {
|
||||
onOrders(listener: (orders: AsterOrder[]) => void): () => void {
|
||||
this.ordersListeners.add(listener);
|
||||
if (this.openOrders.length) listener(cloneOrders(this.openOrders));
|
||||
return () => {
|
||||
@@ -334,7 +332,7 @@ export class GrvtGateway {
|
||||
};
|
||||
}
|
||||
|
||||
onDepth(listener: (depth: Depth) => void): () => void {
|
||||
onDepth(listener: (depth: AsterDepth) => void): () => void {
|
||||
this.depthListeners.add(listener);
|
||||
if (this.depthSnapshot) listener(cloneDepth(this.depthSnapshot));
|
||||
return () => {
|
||||
@@ -342,7 +340,7 @@ export class GrvtGateway {
|
||||
};
|
||||
}
|
||||
|
||||
onTicker(listener: (ticker: Ticker) => void): () => void {
|
||||
onTicker(listener: (ticker: AsterTicker) => void): () => void {
|
||||
this.tickerListeners.add(listener);
|
||||
if (this.tickerSnapshot) listener(cloneTicker(this.tickerSnapshot));
|
||||
return () => {
|
||||
@@ -350,7 +348,7 @@ export class GrvtGateway {
|
||||
};
|
||||
}
|
||||
|
||||
onKlines(listener: (klines: Kline[]) => void): () => void {
|
||||
onKlines(listener: (klines: AsterKline[]) => void): () => void {
|
||||
this.klineListeners.add(listener);
|
||||
if (this.klineCache) listener(cloneKlines(this.klineCache.values));
|
||||
return () => {
|
||||
@@ -358,19 +356,19 @@ export class GrvtGateway {
|
||||
};
|
||||
}
|
||||
|
||||
getAccountSnapshot(): AccountSnapshot | null {
|
||||
getAccountSnapshot(): AsterAccountSnapshot | null {
|
||||
return this.accountSnapshot ? cloneAccount(this.accountSnapshot) : null;
|
||||
}
|
||||
|
||||
getOpenOrders(): Order[] {
|
||||
getOpenOrders(): AsterOrder[] {
|
||||
return cloneOrders(this.openOrders);
|
||||
}
|
||||
|
||||
getPositions(): AccountPosition[] {
|
||||
getPositions(): AsterAccountPosition[] {
|
||||
return this.positions.map((position) => ({ ...position }));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
if (params.type === "TRAILING_STOP_MARKET") {
|
||||
throw new Error(TRAILING_NOT_SUPPORTED_ERROR);
|
||||
}
|
||||
@@ -662,7 +660,7 @@ export class GrvtGateway {
|
||||
this.emitKlines(klines);
|
||||
}
|
||||
|
||||
private mergeOrder(order: Order): void {
|
||||
private mergeOrder(order: AsterOrder): void {
|
||||
const index = this.openOrders.findIndex((item) => String(item.orderId) === String(order.orderId));
|
||||
if (index >= 0) {
|
||||
this.openOrders[index] = order;
|
||||
@@ -680,7 +678,7 @@ export class GrvtGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private emitAccount(snapshot: AccountSnapshot): void {
|
||||
private emitAccount(snapshot: AsterAccountSnapshot): void {
|
||||
const cloned = cloneAccount(snapshot);
|
||||
this.accountListeners.forEach((listener) => {
|
||||
try {
|
||||
@@ -691,7 +689,7 @@ export class GrvtGateway {
|
||||
});
|
||||
}
|
||||
|
||||
private emitOrders(orders: Order[]): void {
|
||||
private emitOrders(orders: AsterOrder[]): void {
|
||||
const cloned = cloneOrders(orders);
|
||||
this.ordersListeners.forEach((listener) => {
|
||||
try {
|
||||
@@ -702,7 +700,7 @@ export class GrvtGateway {
|
||||
});
|
||||
}
|
||||
|
||||
private emitDepth(depth: Depth): void {
|
||||
private emitDepth(depth: AsterDepth): void {
|
||||
const cloned = cloneDepth(depth);
|
||||
this.depthListeners.forEach((listener) => {
|
||||
try {
|
||||
@@ -713,7 +711,7 @@ export class GrvtGateway {
|
||||
});
|
||||
}
|
||||
|
||||
private emitTicker(ticker: Ticker): void {
|
||||
private emitTicker(ticker: AsterTicker): void {
|
||||
const cloned = cloneTicker(ticker);
|
||||
this.tickerListeners.forEach((listener) => {
|
||||
try {
|
||||
@@ -724,7 +722,7 @@ export class GrvtGateway {
|
||||
});
|
||||
}
|
||||
|
||||
private emitKlines(klines: Kline[]): void {
|
||||
private emitKlines(klines: AsterKline[]): void {
|
||||
const cloned = cloneKlines(klines);
|
||||
this.klineListeners.forEach((listener) => {
|
||||
try {
|
||||
@@ -976,15 +974,15 @@ function normalizeHex(value: string): string {
|
||||
return `0x${trimmed.toLowerCase()}`;
|
||||
}
|
||||
|
||||
function cloneAccount(snapshot: AccountSnapshot): AccountSnapshot {
|
||||
function cloneAccount(snapshot: AsterAccountSnapshot): AsterAccountSnapshot {
|
||||
return JSON.parse(JSON.stringify(snapshot));
|
||||
}
|
||||
|
||||
function cloneOrders(orders: Order[]): Order[] {
|
||||
function cloneOrders(orders: AsterOrder[]): AsterOrder[] {
|
||||
return orders.map((order) => ({ ...order }));
|
||||
}
|
||||
|
||||
function cloneDepth(depth: Depth): Depth {
|
||||
function cloneDepth(depth: AsterDepth): AsterDepth {
|
||||
return {
|
||||
...depth,
|
||||
bids: depth.bids.map((level) => [...level] as [string, string]),
|
||||
@@ -992,11 +990,11 @@ function cloneDepth(depth: Depth): Depth {
|
||||
};
|
||||
}
|
||||
|
||||
function cloneTicker(ticker: Ticker): Ticker {
|
||||
function cloneTicker(ticker: AsterTicker): AsterTicker {
|
||||
return { ...ticker };
|
||||
}
|
||||
|
||||
function cloneKlines(klines: Kline[]): Kline[] {
|
||||
function cloneKlines(klines: AsterKline[]): AsterKline[] {
|
||||
return klines.map((kline) => ({ ...kline }));
|
||||
}
|
||||
|
||||
@@ -1025,11 +1023,11 @@ function scaleDecimal(value: string | number | undefined, decimals: number): big
|
||||
return scaleDecimal(fixed.toFixed(decimals), decimals);
|
||||
}
|
||||
|
||||
function sumUnrealized(positions: AccountPosition[]): number {
|
||||
function sumUnrealized(positions: AsterAccountPosition[]): number {
|
||||
return positions.reduce((total, position) => total + Number(position.unrealizedProfit ?? 0), 0);
|
||||
}
|
||||
|
||||
function getNewestPositionEventTime(positions: AccountPosition[]): number {
|
||||
function getNewestPositionEventTime(positions: AsterAccountPosition[]): number {
|
||||
if (!positions.length) return Date.now();
|
||||
return positions.reduce((max, p) => Math.max(max, Number(p.updateTime) || 0), 0);
|
||||
}
|
||||
@@ -1038,7 +1036,7 @@ function mapAccountSnapshot(
|
||||
response: IApiSubAccountSummaryResponse,
|
||||
symbol: string,
|
||||
instrument: string
|
||||
): AccountSnapshot {
|
||||
): AsterAccountSnapshot {
|
||||
const result = response.result;
|
||||
if (!result) {
|
||||
return emptyAccount(symbol);
|
||||
@@ -1070,7 +1068,7 @@ function mapPositions(
|
||||
response: IApiPositionsResponse,
|
||||
symbol: string,
|
||||
instrument: string
|
||||
): AccountPosition[] {
|
||||
): AsterAccountPosition[] {
|
||||
return (response.result ?? [])
|
||||
.filter((entry) => !entry.instrument || entry.instrument === instrument)
|
||||
.map((entry) => ({
|
||||
@@ -1086,11 +1084,11 @@ function mapPositions(
|
||||
}));
|
||||
}
|
||||
|
||||
function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): Order[] {
|
||||
function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): AsterOrder[] {
|
||||
return (response.result ?? []).map((order) => mapOrder(order, symbol));
|
||||
}
|
||||
|
||||
function mapOrder(order: IOrder, symbol: string): Order {
|
||||
function mapOrder(order: IOrder, symbol: string): AsterOrder {
|
||||
const leg = order.legs?.[0];
|
||||
const state = order.state;
|
||||
const metadata = order.metadata;
|
||||
@@ -1128,7 +1126,7 @@ function mapOrder(order: IOrder, symbol: string): Order {
|
||||
};
|
||||
}
|
||||
|
||||
function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): Depth | null {
|
||||
function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): AsterDepth | null {
|
||||
const result = response.result;
|
||||
if (!result) return null;
|
||||
const toLevel = (entries: Array<{ price?: string; size?: string }> | undefined) =>
|
||||
@@ -1142,7 +1140,7 @@ function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): Depth
|
||||
};
|
||||
}
|
||||
|
||||
function mapTicker(response: IApiTickerResponse, symbol: string): Ticker | null {
|
||||
function mapTicker(response: IApiTickerResponse, symbol: string): AsterTicker | null {
|
||||
const result = response.result;
|
||||
if (!result) return null;
|
||||
const buyVolume = Number(result.buy_volume_24h_b ?? 0);
|
||||
@@ -1163,7 +1161,7 @@ function mapTicker(response: IApiTickerResponse, symbol: string): Ticker | null
|
||||
};
|
||||
}
|
||||
|
||||
function mapKlines(response: IApiCandlestickResponse, _symbol: string): Kline[] {
|
||||
function mapKlines(response: IApiCandlestickResponse, symbol: string): AsterKline[] {
|
||||
return (response.result ?? []).reverse().map((entry) => ({
|
||||
openTime: nsToMs(entry.open_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
|
||||
closeTime: nsToMs(entry.close_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
|
||||
@@ -1176,7 +1174,7 @@ function mapKlines(response: IApiCandlestickResponse, _symbol: string): Kline[]
|
||||
}));
|
||||
}
|
||||
|
||||
function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): Order {
|
||||
function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): AsterOrder {
|
||||
const order = response.result ?? (response as unknown as { order?: IOrder }).order;
|
||||
if (!order) {
|
||||
return {
|
||||
@@ -1553,7 +1551,7 @@ function padPrivateKey(value: string): string {
|
||||
return hex;
|
||||
}
|
||||
|
||||
function emptyAccount(symbol: string): AccountSnapshot {
|
||||
function emptyAccount(symbol: string): AsterAccountSnapshot {
|
||||
return {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
@@ -1570,5 +1568,5 @@ function emptyAccount(symbol: string): AccountSnapshot {
|
||||
updateTime: Date.now(),
|
||||
},
|
||||
],
|
||||
} as AccountSnapshot;
|
||||
} as AsterAccountSnapshot;
|
||||
}
|
||||
|
||||
+89
-18
@@ -1,21 +1,92 @@
|
||||
import { createOrderHandlers } from "../order-handlers";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
const handlers = createOrderHandlers({
|
||||
exchangeName: "GRVT",
|
||||
defaultLimitTimeInForce: "GTX",
|
||||
supportsTrailingStop: false,
|
||||
supportsTriggerType: true,
|
||||
defaultStopTriggerType: "STOP_LOSS",
|
||||
stopDefaultReduceOnly: true,
|
||||
stopDefaultClosePosition: true,
|
||||
closeDefaultClosePosition: true,
|
||||
});
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export const {
|
||||
createLimitOrder,
|
||||
createMarketOrder,
|
||||
createStopOrder,
|
||||
createTrailingStopOrder,
|
||||
createClosePositionOrder,
|
||||
} = handlers;
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "GTX",
|
||||
},
|
||||
intent
|
||||
);
|
||||
return intent.adapter.createOrder(params);
|
||||
}
|
||||
|
||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "MARKET",
|
||||
quantity: intent.quantity,
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { LighterGateway, type LighterGatewayOptions } from "./gateway";
|
||||
|
||||
@@ -102,7 +102,7 @@ export class LighterExchangeAdapter implements ExchangeAdapter {
|
||||
this.gateway.watchKlines(interval, handler);
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
@@ -8,18 +8,20 @@ import type {
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type {
|
||||
AccountAsset,
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountAsset,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
} from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import type { OrderSide, OrderType } from "../types";
|
||||
import { LighterHttpClient } from "./http-client";
|
||||
import { HttpNonceManager } from "./nonce-manager";
|
||||
import { LighterSigner, type CreateOrderSignParams } from "./signer";
|
||||
import { bytesToHex } from "./bytes";
|
||||
import type {
|
||||
LighterAccountDetails,
|
||||
LighterAccountAsset,
|
||||
@@ -37,6 +39,7 @@ import {
|
||||
LIGHTER_HOSTS,
|
||||
LIGHTER_ORDER_TYPE,
|
||||
LIGHTER_TIME_IN_FORCE,
|
||||
DEFAULT_ORDER_EXPIRY_PLACEHOLDER,
|
||||
IMMEDIATE_OR_CANCEL_EXPIRY_PLACEHOLDER,
|
||||
type LighterEnvironment,
|
||||
} from "./constants";
|
||||
@@ -189,12 +192,12 @@ export class LighterGateway {
|
||||
private accountPollInFlight = false;
|
||||
private ordersResyncTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private ordersResyncInFlight = false;
|
||||
private readonly klineCache = new Map<string, Kline[]>();
|
||||
private readonly accountEvent = createEvent<AccountSnapshot>();
|
||||
private readonly ordersEvent = createEvent<Order[]>();
|
||||
private readonly depthEvent = createEvent<Depth>();
|
||||
private readonly tickerEvent = createEvent<Ticker>();
|
||||
private readonly klinesEvent = createEvent<Kline[]>();
|
||||
private readonly klineCache = new Map<string, AsterKline[]>();
|
||||
private readonly accountEvent = createEvent<AsterAccountSnapshot>();
|
||||
private readonly ordersEvent = createEvent<AsterOrder[]>();
|
||||
private readonly depthEvent = createEvent<AsterDepth>();
|
||||
private readonly tickerEvent = createEvent<AsterTicker>();
|
||||
private readonly klinesEvent = createEvent<AsterKline[]>();
|
||||
private readonly auth = { token: null as string | null, expiresAt: 0 };
|
||||
private readonly l1Address: string | null;
|
||||
private loggedCreateOrderPayload = false;
|
||||
@@ -359,8 +362,8 @@ export class LighterGateway {
|
||||
this.klinesEvent.add(handler);
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
const run = async (): Promise<Order> => {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
const run = async (): Promise<AsterOrder> => {
|
||||
await this.ensureInitialized();
|
||||
const conversion = this.mapCreateOrderParams(params);
|
||||
const { baseAmountScaledString, priceScaledString, triggerPriceScaledString, ...signParams } = conversion;
|
||||
@@ -1710,7 +1713,7 @@ export class LighterGateway {
|
||||
const bestAsk = getBestPrice(this.orderBook.asks, "ask");
|
||||
if (bestBid == null && bestAsk == null) return;
|
||||
const last = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : (bestBid ?? bestAsk ?? 0);
|
||||
const ticker: Ticker = {
|
||||
const ticker: AsterTicker = {
|
||||
symbol: this.displaySymbol,
|
||||
eventType: "lighterSyntheticTicker",
|
||||
eventTime: Date.now(),
|
||||
@@ -1736,10 +1739,10 @@ export class LighterGateway {
|
||||
this.tickerEvent.emit(ticker);
|
||||
}
|
||||
|
||||
private buildAccountAssets(): AccountAsset[] {
|
||||
private buildAccountAssets(): AsterAccountAsset[] {
|
||||
if (!this.assets.size) return [];
|
||||
const now = Date.now();
|
||||
const list: AccountAsset[] = [];
|
||||
const list: AsterAccountAsset[] = [];
|
||||
for (const asset of this.assets.values()) {
|
||||
const balanceNum = parseNumber(asset.balance);
|
||||
const lockedNum = parseNumber(asset.locked_balance ?? 0);
|
||||
@@ -2039,7 +2042,7 @@ function mergeLevels(existing: LighterOrderBookLevel[], updates: LighterOrderBoo
|
||||
return Array.from(map.entries()).map(([price, size]) => ({ price, size } as LighterOrderBookLevel));
|
||||
}
|
||||
|
||||
function cloneKlines(klines: Kline[]): Kline[] {
|
||||
function cloneKlines(klines: AsterKline[]): AsterKline[] {
|
||||
return klines.map((kline) => ({ ...kline }));
|
||||
}
|
||||
|
||||
@@ -2207,7 +2210,7 @@ function extractJsonRequestId(message: any): string | null {
|
||||
function tryParseTxInfo(value: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
} catch (_) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type {
|
||||
AccountAsset,
|
||||
AccountPosition,
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
DepthLevel,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountAsset,
|
||||
AsterAccountPosition,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterDepthLevel,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
OrderSide,
|
||||
OrderType,
|
||||
} from "../types";
|
||||
@@ -23,8 +23,8 @@ import { coerceBooleanFlag, normalizeBooleanFlag } from "./flags";
|
||||
import { normalizeOrderIdentity } from "./order-identity";
|
||||
import { normalizeOrderStatus } from "./status";
|
||||
|
||||
export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): Depth {
|
||||
const toLevels = (levels: LighterOrderBookLevel[]): DepthLevel[] =>
|
||||
export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): AsterDepth {
|
||||
const toLevels = (levels: LighterOrderBookLevel[]): AsterDepthLevel[] =>
|
||||
levels.map((level) => [level.price, level.size]);
|
||||
return {
|
||||
symbol,
|
||||
@@ -36,7 +36,7 @@ export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): Dep
|
||||
};
|
||||
}
|
||||
|
||||
export function toTicker(symbol: string, stats: LighterMarketStats): Ticker {
|
||||
export function toTicker(symbol: string, stats: LighterMarketStats): AsterTicker {
|
||||
return {
|
||||
symbol,
|
||||
eventType: "lighterTicker",
|
||||
@@ -50,10 +50,10 @@ export function toTicker(symbol: string, stats: LighterMarketStats): Ticker {
|
||||
priceChange: stats.daily_price_change != null ? String(stats.daily_price_change) : undefined,
|
||||
markPrice: stats.mid_price ?? stats.mark_price ?? stats.index_price,
|
||||
weightedAvgPrice: undefined,
|
||||
} as Ticker;
|
||||
} as AsterTicker;
|
||||
}
|
||||
|
||||
export function toKlines(symbol: string, interval: string, klines: LighterKline[]): Kline[] {
|
||||
export function toKlines(symbol: string, interval: string, klines: LighterKline[]): AsterKline[] {
|
||||
return klines.map((entry) => ({
|
||||
symbol,
|
||||
eventType: "lighterKline",
|
||||
@@ -72,11 +72,11 @@ export function toKlines(symbol: string, interval: string, klines: LighterKline[
|
||||
}));
|
||||
}
|
||||
|
||||
export function toOrders(symbol: string, orders: LighterOrder[]): Order[] {
|
||||
export function toOrders(symbol: string, orders: LighterOrder[]): AsterOrder[] {
|
||||
return orders.map((order) => lighterOrderToAster(symbol, order));
|
||||
}
|
||||
|
||||
export function lighterOrderToAster(symbol: string, order: LighterOrder): Order {
|
||||
export function lighterOrderToAster(symbol: string, order: LighterOrder): AsterOrder {
|
||||
const booleanIsAsk = normalizeBooleanFlag(order.is_ask);
|
||||
const normalizedSide = order.side?.toLowerCase();
|
||||
const side: OrderSide =
|
||||
@@ -128,7 +128,7 @@ function computeExecutedQty(order: LighterOrder): string {
|
||||
if (Number.isFinite(initial) && Number.isFinite(remaining)) {
|
||||
return (initial - remaining).toString();
|
||||
}
|
||||
} catch {
|
||||
} catch (_) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export function toAccountSnapshot(
|
||||
symbol: string,
|
||||
details: LighterAccountDetails,
|
||||
positions: LighterPosition[] = [],
|
||||
assets: AccountAsset[] = [],
|
||||
assets: AsterAccountAsset[] = [],
|
||||
options?: {
|
||||
marketSymbol?: string | null;
|
||||
marketId?: number | null;
|
||||
@@ -172,7 +172,7 @@ export function toAccountSnapshot(
|
||||
baseAssetId?: number | null;
|
||||
quoteAssetId?: number | null;
|
||||
}
|
||||
): AccountSnapshot {
|
||||
): AsterAccountSnapshot {
|
||||
const targetSymbol = options?.marketSymbol ?? null;
|
||||
const targetMarketId =
|
||||
options?.marketId != null && Number.isFinite(Number(options.marketId))
|
||||
@@ -228,7 +228,7 @@ export function toAccountSnapshot(
|
||||
};
|
||||
}
|
||||
|
||||
function defaultAsset(details: LighterAccountDetails, quoteAsset: string): AccountAsset[] {
|
||||
function defaultAsset(details: LighterAccountDetails, quoteAsset: string): AsterAccountAsset[] {
|
||||
return [
|
||||
{
|
||||
asset: quoteAsset || "USDC",
|
||||
@@ -239,7 +239,7 @@ function defaultAsset(details: LighterAccountDetails, quoteAsset: string): Accou
|
||||
];
|
||||
}
|
||||
|
||||
function computeTotalWalletBalance(assets: AccountAsset[], details: LighterAccountDetails): string {
|
||||
function computeTotalWalletBalance(assets: AsterAccountAsset[], details: LighterAccountDetails): string {
|
||||
const sum = assets.reduce((acc, asset) => acc + Number(asset.walletBalance ?? 0), 0);
|
||||
if (Number.isFinite(sum) && sum > 0) {
|
||||
return sum.toString();
|
||||
@@ -247,7 +247,7 @@ function computeTotalWalletBalance(assets: AccountAsset[], details: LighterAccou
|
||||
return details.total_asset_value ?? details.collateral ?? "0";
|
||||
}
|
||||
|
||||
function findAsset(assets: AccountAsset[], target: string | undefined | null): AccountAsset | undefined {
|
||||
function findAsset(assets: AsterAccountAsset[], target: string | undefined | null): AsterAccountAsset | undefined {
|
||||
if (!target) return undefined;
|
||||
const normalizedTarget = target.toUpperCase().split(/[-:/]/)[0];
|
||||
return assets.find((asset) => asset.asset.toUpperCase().split(/[-:/]/)[0] === normalizedTarget);
|
||||
@@ -261,7 +261,7 @@ function normalizeMarketType(value: string | null | undefined): "perp" | "spot"
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function lighterPositionToAster(symbol: string, position: LighterPosition): AccountPosition {
|
||||
function lighterPositionToAster(symbol: string, position: LighterPosition): AsterAccountPosition {
|
||||
const sign = position.sign ?? 0;
|
||||
const positionSide = sign > 0 ? "LONG" : sign < 0 ? "SHORT" : "BOTH";
|
||||
const magnitude = Number(position.position ?? 0);
|
||||
|
||||
@@ -1,22 +1,93 @@
|
||||
import { createOrderHandlers } from "../order-handlers";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
const handlers = createOrderHandlers({
|
||||
exchangeName: "Lighter",
|
||||
defaultLimitTimeInForce: "GTC",
|
||||
defaultMarketTimeInForce: "IOC",
|
||||
defaultCloseTimeInForce: "IOC",
|
||||
supportsTrailingStop: false,
|
||||
supportsTriggerType: false,
|
||||
stopDefaultReduceOnly: true,
|
||||
stopDefaultClosePosition: true,
|
||||
closeDefaultClosePosition: true,
|
||||
});
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export const {
|
||||
createLimitOrder,
|
||||
createMarketOrder,
|
||||
createStopOrder,
|
||||
createTrailingStopOrder,
|
||||
createClosePositionOrder,
|
||||
} = handlers;
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "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);
|
||||
}
|
||||
|
||||
|
||||
@@ -100,13 +100,6 @@ class PythonSignerBridge {
|
||||
pending.reject(new Error(String(error)));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(payload, "txHash") ||
|
||||
Object.prototype.hasOwnProperty.call(payload, "messageToSign")
|
||||
) {
|
||||
pending.resolve(payload);
|
||||
return;
|
||||
}
|
||||
pending.resolve(payload.result ?? null);
|
||||
}
|
||||
|
||||
@@ -188,7 +181,7 @@ export class LighterSigner {
|
||||
await this.ensureReady();
|
||||
const apiKeyIndex = params.apiKeyIndex ?? this.defaultKeyIndex;
|
||||
|
||||
const bridgePayload = await this.bridge.call("sign_create_order", {
|
||||
const result = await this.bridge.call("sign_create_order", {
|
||||
apiKeyIndex,
|
||||
marketIndex: params.marketIndex,
|
||||
clientOrderIndex: params.clientOrderIndex.toString(),
|
||||
@@ -200,14 +193,13 @@ export class LighterSigner {
|
||||
reduceOnly: params.reduceOnly,
|
||||
triggerPrice: params.triggerPrice,
|
||||
orderExpiry: params.orderExpiry.toString(),
|
||||
expiredAt: params.expiredAt?.toString(),
|
||||
nonce: params.nonce.toString(),
|
||||
accountIndex: this.accountIndex.toString(),
|
||||
});
|
||||
|
||||
const txInfo = this.resolveTxInfo(bridgePayload);
|
||||
const txInfo = String(result);
|
||||
let signature: string | undefined;
|
||||
let txHash: string | undefined = this.resolveTxHash(bridgePayload);
|
||||
let txHash: string | undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(txInfo);
|
||||
if (typeof parsed?.Sig === "string") signature = parsed.Sig;
|
||||
@@ -228,7 +220,7 @@ export class LighterSigner {
|
||||
await this.ensureReady();
|
||||
const apiKeyIndex = params.apiKeyIndex ?? this.defaultKeyIndex;
|
||||
|
||||
const bridgePayload = await this.bridge.call("sign_cancel_order", {
|
||||
const result = await this.bridge.call("sign_cancel_order", {
|
||||
apiKeyIndex,
|
||||
marketIndex: params.marketIndex,
|
||||
orderIndex: params.orderIndex.toString(),
|
||||
@@ -236,7 +228,7 @@ export class LighterSigner {
|
||||
accountIndex: this.accountIndex.toString(),
|
||||
});
|
||||
|
||||
const txInfo = this.resolveTxInfo(bridgePayload);
|
||||
const txInfo = String(result);
|
||||
let signature: string | undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(txInfo);
|
||||
@@ -256,7 +248,7 @@ export class LighterSigner {
|
||||
await this.ensureReady();
|
||||
const apiKeyIndex = params.apiKeyIndex ?? this.defaultKeyIndex;
|
||||
|
||||
const bridgePayload = await this.bridge.call("sign_cancel_all", {
|
||||
const result = await this.bridge.call("sign_cancel_all", {
|
||||
apiKeyIndex,
|
||||
timeInForce: params.timeInForce,
|
||||
scheduledTime: params.scheduledTime.toString(),
|
||||
@@ -264,7 +256,7 @@ export class LighterSigner {
|
||||
accountIndex: this.accountIndex.toString(),
|
||||
});
|
||||
|
||||
const txInfo = this.resolveTxInfo(bridgePayload);
|
||||
const txInfo = String(result);
|
||||
let signature: string | undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(txInfo);
|
||||
@@ -290,17 +282,4 @@ export class LighterSigner {
|
||||
});
|
||||
return String(result ?? "");
|
||||
}
|
||||
|
||||
private resolveTxInfo(payload: unknown): string {
|
||||
if (payload && typeof payload === "object" && "result" in payload) {
|
||||
return String((payload as { result?: unknown }).result ?? "");
|
||||
}
|
||||
return String(payload ?? "");
|
||||
}
|
||||
|
||||
private resolveTxHash(payload: unknown): string | undefined {
|
||||
if (!payload || typeof payload !== "object") return undefined;
|
||||
const hash = (payload as { txHash?: unknown }).txHash;
|
||||
return typeof hash === "string" && hash.length > 0 ? hash : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
@@ -8,10 +9,9 @@ import type {
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { NadoGateway, type NadoGatewayOptions } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
import type { ChainEnv } from "@nadohq/shared";
|
||||
import type { Address } from "viem";
|
||||
|
||||
@@ -35,8 +35,11 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
private readonly gateway: NadoGateway;
|
||||
private readonly symbol: string;
|
||||
private readonly safeInvoke = createSafeInvoke("NadoExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private lastInitErrorAt = 0;
|
||||
|
||||
constructor(credentials: NadoCredentials = {}) {
|
||||
const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY;
|
||||
@@ -76,9 +79,6 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
||||
(process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined),
|
||||
logger: (context, error) => this.logError(context, error),
|
||||
});
|
||||
this.init = createInitManager("NadoExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -86,52 +86,52 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
void this.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
void this.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
void this.init.ensureInitialized(`watchDepth:${symbol}`);
|
||||
void this.ensureInitialized(`watchDepth:${symbol}`);
|
||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
void this.init.ensureInitialized(`watchTicker:${symbol}`);
|
||||
void this.ensureInitialized(`watchTicker:${symbol}`);
|
||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||
void this.init.ensureInitialized(`watchFundingRate:${symbol}`);
|
||||
void this.ensureInitialized(`watchFundingRate:${symbol}`);
|
||||
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
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.init.ensureInitialized("cancelOrder");
|
||||
await this.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.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,70 @@ 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 {
|
||||
const detail = extractMessage(error);
|
||||
const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`;
|
||||
|
||||
@@ -23,13 +23,13 @@ import type {
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type {
|
||||
AccountAsset,
|
||||
AccountPosition,
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountAsset,
|
||||
AsterAccountPosition,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
TimeInForce,
|
||||
} from "../types";
|
||||
@@ -331,7 +331,7 @@ export class NadoGateway {
|
||||
private readonly openOrdersByDigest = new Map<string, LocalOrder>();
|
||||
private readonly triggerOrdersByDigest = new Map<string, TriggerOrder>();
|
||||
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private lastAccountSyncAt = 0;
|
||||
|
||||
private gatewayWs: NodeWebSocket | null = null;
|
||||
@@ -356,7 +356,7 @@ export class NadoGateway {
|
||||
private accountPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private ordersPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private triggerOrdersPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private klinesState = new Map<string, { productId: number; periodSec: number; klines: Kline[] }>();
|
||||
private klinesState = new Map<string, { productId: number; periodSec: number; klines: AsterKline[] }>();
|
||||
|
||||
private subscriptionRequestId = 1;
|
||||
private subscriptionAuthComplete = false;
|
||||
@@ -567,7 +567,7 @@ export class NadoGateway {
|
||||
};
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
await this.ensureInitialized(params.symbol);
|
||||
const meta = this.getSymbolMetaOrThrow(params.symbol);
|
||||
if (params.type === "STOP_MARKET") {
|
||||
@@ -1139,7 +1139,7 @@ export class NadoGateway {
|
||||
return Number.isFinite(asNumber) && asNumber > 0 ? Math.floor(asNumber) : 60;
|
||||
}
|
||||
|
||||
private async fetchCandlesticks(productId: number, periodSec: number, limit: number): Promise<Kline[]> {
|
||||
private async fetchCandlesticks(productId: number, periodSec: number, limit: number): Promise<AsterKline[]> {
|
||||
try {
|
||||
const result = await this.indexer.getCandlesticks({ productId, period: periodSec, limit });
|
||||
const reversed = Array.from(result).reverse();
|
||||
@@ -1262,8 +1262,8 @@ export class NadoGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private buildAsterOrdersSnapshot(): Order[] {
|
||||
const orders: Order[] = [];
|
||||
private buildAsterOrdersSnapshot(): AsterOrder[] {
|
||||
const orders: AsterOrder[] = [];
|
||||
for (const order of this.openOrdersByDigest.values()) {
|
||||
const meta = this.symbolMetaByProductId.get(order.productId);
|
||||
if (!meta) continue;
|
||||
@@ -1322,7 +1322,7 @@ export class NadoGateway {
|
||||
return orders;
|
||||
}
|
||||
|
||||
private buildDepthSnapshot(productId: number, symbol: string): Depth | null {
|
||||
private buildDepthSnapshot(productId: number, symbol: string): AsterDepth | null {
|
||||
const bbo = this.bestBidOfferByProductId.get(productId);
|
||||
if (!bbo) return null;
|
||||
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): Ticker | null {
|
||||
private buildTickerSnapshot(productId: number, symbol: string): AsterTicker | null {
|
||||
const bbo = this.bestBidOfferByProductId.get(productId);
|
||||
if (!bbo) return null;
|
||||
const trade = this.lastTradePriceByProductId.get(productId);
|
||||
@@ -1360,10 +1360,10 @@ export class NadoGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapSubaccountInfoToAsterSnapshot(data: NonNullable<NadoSubaccountInfoResponse["data"]>): AccountSnapshot {
|
||||
private mapSubaccountInfoToAsterSnapshot(data: NonNullable<NadoSubaccountInfoResponse["data"]>): AsterAccountSnapshot {
|
||||
const now = nowMs();
|
||||
const assets: AccountAsset[] = [];
|
||||
const positions: AccountPosition[] = [];
|
||||
const assets: AsterAccountAsset[] = [];
|
||||
const positions: AsterAccountPosition[] = [];
|
||||
|
||||
const spotBalanceByProductId = new Map<number, string>();
|
||||
for (const entry of data.spot_balances ?? []) {
|
||||
@@ -1684,7 +1684,7 @@ export class NadoGateway {
|
||||
try {
|
||||
const text = typeof data === "string" ? data : data.toString("utf8");
|
||||
message = JSON.parse(text);
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
return;
|
||||
}
|
||||
if (!message || typeof message !== "object") return;
|
||||
@@ -1771,7 +1771,7 @@ export class NadoGateway {
|
||||
if (state.periodSec !== event.granularity) continue;
|
||||
const openTime = event.timestamp * 1000;
|
||||
const closeTime = openTime + state.periodSec * 1000 - 1;
|
||||
const next: Kline = {
|
||||
const next: AsterKline = {
|
||||
openTime,
|
||||
closeTime,
|
||||
open: fromX18(event.open_x18).toFixed(),
|
||||
@@ -1795,7 +1795,7 @@ export class NadoGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
|
||||
private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
|
||||
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
|
||||
const side = params.side;
|
||||
const qty = params.quantity ?? 0;
|
||||
@@ -1905,7 +1905,7 @@ export class NadoGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
|
||||
private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
|
||||
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
|
||||
const side = params.side;
|
||||
const qty = params.quantity ?? 0;
|
||||
@@ -2017,7 +2017,7 @@ export class NadoGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
|
||||
private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
|
||||
if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)");
|
||||
const side = params.side;
|
||||
const qty = params.quantity ?? 0;
|
||||
|
||||
+90
-19
@@ -1,22 +1,93 @@
|
||||
import { createOrderHandlers } from "../order-handlers";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
const handlers = createOrderHandlers({
|
||||
exchangeName: "Nado",
|
||||
defaultLimitTimeInForce: "GTC",
|
||||
defaultMarketTimeInForce: "IOC",
|
||||
defaultCloseTimeInForce: "IOC",
|
||||
supportsTrailingStop: false,
|
||||
supportsTriggerType: false,
|
||||
stopDefaultReduceOnly: true,
|
||||
stopDefaultClosePosition: true,
|
||||
closeDefaultClosePosition: true,
|
||||
});
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export const {
|
||||
createLimitOrder,
|
||||
createMarketOrder,
|
||||
createStopOrder,
|
||||
createTrailingStopOrder,
|
||||
createClosePositionOrder,
|
||||
} = handlers;
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
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
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
@@ -1,173 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ExchangeAdapter } from "./adapter";
|
||||
import type { Order } from "./types";
|
||||
import type { AsterOrder } from "./types";
|
||||
import { SUPPORTED_EXCHANGE_IDS, type SupportedExchangeId } from "./create-adapter";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
@@ -17,16 +17,15 @@ import * as paradexOrders from "./paradex/order";
|
||||
import * as nadoOrders from "./nado/order";
|
||||
import * as standxOrders from "./standx/order";
|
||||
import * as binanceOrders from "./binance/order";
|
||||
import * as ondoperpsOrders from "./ondoperps/order";
|
||||
|
||||
type ExchangeKey = SupportedExchangeId;
|
||||
|
||||
interface ExchangeOrderHandlers {
|
||||
limit(intent: LimitOrderIntent): Promise<Order>;
|
||||
market(intent: MarketOrderIntent): Promise<Order>;
|
||||
stop(intent: StopOrderIntent): Promise<Order>;
|
||||
trailingStop?: (intent: TrailingStopOrderIntent) => Promise<Order>;
|
||||
close(intent: ClosePositionIntent): Promise<Order>;
|
||||
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
|
||||
market(intent: MarketOrderIntent): Promise<AsterOrder>;
|
||||
stop(intent: StopOrderIntent): Promise<AsterOrder>;
|
||||
trailingStop?: (intent: TrailingStopOrderIntent) => Promise<AsterOrder>;
|
||||
close(intent: ClosePositionIntent): Promise<AsterOrder>;
|
||||
}
|
||||
|
||||
const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
||||
@@ -86,21 +85,13 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
||||
trailingStop: binanceOrders.createTrailingStopOrder,
|
||||
close: binanceOrders.createClosePositionOrder,
|
||||
},
|
||||
ondoperps: {
|
||||
limit: ondoperpsOrders.createLimitOrder,
|
||||
market: ondoperpsOrders.createMarketOrder,
|
||||
stop: ondoperpsOrders.createStopOrder,
|
||||
trailingStop: ondoperpsOrders.createTrailingStopOrder,
|
||||
close: ondoperpsOrders.createClosePositionOrder,
|
||||
},
|
||||
};
|
||||
|
||||
const knownExchanges: ExchangeKey[] = [...SUPPORTED_EXCHANGE_IDS];
|
||||
|
||||
function normalizeExchangeId(value: string | undefined | null): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "ondoperp" ? "ondoperps" : normalized;
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function resolveExchangeKey(adapter: ExchangeAdapter): ExchangeKey {
|
||||
@@ -126,19 +117,19 @@ function getHandlers(intent: BaseOrderIntent): ExchangeOrderHandlers {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
export function routeLimitOrder(intent: LimitOrderIntent): Promise<Order> {
|
||||
export function routeLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
return getHandlers(intent).limit(intent);
|
||||
}
|
||||
|
||||
export function routeMarketOrder(intent: MarketOrderIntent): Promise<Order> {
|
||||
export function routeMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
||||
return getHandlers(intent).market(intent);
|
||||
}
|
||||
|
||||
export function routeStopOrder(intent: StopOrderIntent): Promise<Order> {
|
||||
export function routeStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
||||
return getHandlers(intent).stop(intent);
|
||||
}
|
||||
|
||||
export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order> {
|
||||
export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
||||
const handlers = getHandlers(intent);
|
||||
if (!handlers.trailingStop) {
|
||||
throw new Error("Trailing stop orders are not supported by the current exchange");
|
||||
@@ -146,6 +137,6 @@ export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise
|
||||
return handlers.trailingStop(intent);
|
||||
}
|
||||
|
||||
export function routeCloseOrder(intent: ClosePositionIntent): Promise<Order> {
|
||||
export function routeCloseOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
||||
return getHandlers(intent).close(intent);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ export interface BaseOrderIntent {
|
||||
reduceOnly?: boolean;
|
||||
closePosition?: boolean;
|
||||
timeInForce?: TimeInForce | "GTX";
|
||||
clientOrderId?: string;
|
||||
}
|
||||
|
||||
export interface LimitOrderIntent extends BaseOrderIntent {
|
||||
price: number;
|
||||
slPrice?: number;
|
||||
tpPrice?: number;
|
||||
// StandX TPSL 参数
|
||||
slPrice?: number; // 止损价格
|
||||
tpPrice?: number; // 止盈价格
|
||||
}
|
||||
|
||||
export interface MarketOrderIntent extends BaseOrderIntent {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
@@ -6,10 +7,9 @@ import type {
|
||||
OrderListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { ParadexGateway, type ParadexGatewayOptions } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export interface ParadexCredentials {
|
||||
privateKey?: string;
|
||||
@@ -26,8 +26,11 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
private readonly gateway: ParadexGateway;
|
||||
private readonly symbol: string;
|
||||
private readonly safeInvoke = createSafeInvoke("ParadexExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private lastInitErrorAt = 0;
|
||||
|
||||
constructor(credentials: ParadexCredentials = {}) {
|
||||
const privateKey = credentials.privateKey ?? process.env.PARADEX_PRIVATE_KEY;
|
||||
@@ -51,9 +54,6 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
||||
});
|
||||
|
||||
this.symbol = symbol;
|
||||
this.init = createInitManager("ParadexExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -61,50 +61,114 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
void this.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
void this.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
void this.init.ensureInitialized(`watchDepth:${symbol}`);
|
||||
void this.ensureInitialized(`watchDepth:${symbol}`);
|
||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
void this.init.ensureInitialized(`watchTicker:${symbol}`);
|
||||
void this.ensureInitialized(`watchTicker:${symbol}`);
|
||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
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.init.ensureInitialized("cancelOrder");
|
||||
await this.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.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
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 {
|
||||
const detail = extractMessage(error);
|
||||
if (context === "initialize" && typeof error === "string" && /initialized/i.test(error)) {
|
||||
|
||||
@@ -7,12 +7,12 @@ import ccxt, {
|
||||
} from "ccxt";
|
||||
import { createRequire } from "module";
|
||||
import type {
|
||||
AccountAsset,
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountAsset,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
OrderType,
|
||||
} from "../types";
|
||||
@@ -32,7 +32,7 @@ function loadCcxtPro(): any | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const mod = require("ccxt.pro");
|
||||
return mod?.default ?? mod;
|
||||
} catch {
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -91,8 +91,8 @@ export class ParadexGateway {
|
||||
private depthListeners = new Set<DepthListener>();
|
||||
private tickerListeners = new Set<TickerListener>();
|
||||
private klineListeners = new Map<string, Set<KlineListener>>();
|
||||
private readonly localOrders = new Map<string, Order>();
|
||||
private lastBalanceSnapshot: AccountSnapshot | null = null;
|
||||
private readonly localOrders = new Map<string, AsterOrder>();
|
||||
private lastBalanceSnapshot: AsterAccountSnapshot | null = null;
|
||||
|
||||
private accountPollTimer: 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));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
await this.ensureInitialized(params.symbol);
|
||||
const symbol = this.marketSymbol;
|
||||
const type = this.mapOrderTypeToCcxt(params.type);
|
||||
@@ -571,6 +571,7 @@ export class ParadexGateway {
|
||||
const market = typeof (this.exchange as any).market === "function"
|
||||
? (this.exchange as any).market(symbol)
|
||||
: (this.exchange.markets ?? {})[symbol];
|
||||
const precisionDigits = Number((market?.precision?.amount ?? market?.amountPrecision));
|
||||
const limitMin = Number(market?.limits?.amount?.min);
|
||||
// Only trust explicit exchange min limit; do NOT infer 1 from precision=0
|
||||
const minAmount = Number.isFinite(limitMin) && limitMin > 0 ? limitMin : undefined;
|
||||
@@ -592,7 +593,7 @@ export class ParadexGateway {
|
||||
if (typeof (this.exchange as any).amountToPrecision === "function" && Number.isFinite(Number(amount))) {
|
||||
amount = Number((this.exchange as any).amountToPrecision(symbol, amount));
|
||||
}
|
||||
} catch {
|
||||
} catch (_normalizeError) {
|
||||
// Swallow precision normalization errors and let exchange validation surface if any
|
||||
}
|
||||
|
||||
@@ -678,7 +679,7 @@ export class ParadexGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private mapBalanceToAccountSnapshot(balance: Balances): AccountSnapshot {
|
||||
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
|
||||
const now = Date.now();
|
||||
|
||||
const rawPositions = (() => {
|
||||
@@ -711,7 +712,7 @@ export class ParadexGateway {
|
||||
...Object.keys(total),
|
||||
]);
|
||||
|
||||
const assets: AccountAsset[] = Array.from(assetKeys).map((asset) => ({
|
||||
const assets: AsterAccountAsset[] = Array.from(assetKeys).map((asset) => ({
|
||||
asset,
|
||||
walletBalance: String(total[asset] ?? 0),
|
||||
availableBalance: String(free[asset] ?? 0),
|
||||
@@ -737,7 +738,7 @@ export class ParadexGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapBalanceToAccountSnapshotFromPositions(rawPositions: any[]): AccountSnapshot {
|
||||
private mapBalanceToAccountSnapshotFromPositions(rawPositions: any[]): AsterAccountSnapshot {
|
||||
const now = Date.now();
|
||||
const positions = this.normalizePositions(rawPositions, now);
|
||||
this.logger("positions", JSON.stringify({ raw: rawPositions, mapped: positions }));
|
||||
@@ -761,7 +762,7 @@ export class ParadexGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private normalizePositions(rawPositions: any[], now: number): AccountSnapshot["positions"] {
|
||||
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] {
|
||||
return rawPositions
|
||||
.filter((pos) => pos)
|
||||
.map((pos: any) => {
|
||||
@@ -807,7 +808,7 @@ export class ParadexGateway {
|
||||
});
|
||||
}
|
||||
|
||||
private mapOrderToAsterOrder(order: CcxtOrder): Order {
|
||||
private mapOrderToAsterOrder(order: CcxtOrder): AsterOrder {
|
||||
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
|
||||
const mappedType = this.mapCcxtOrderTypeToAster(order.type);
|
||||
return {
|
||||
@@ -830,7 +831,7 @@ export class ParadexGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapOrderBookToDepth(orderbook: CcxtOrderBook): Depth {
|
||||
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
|
||||
return {
|
||||
lastUpdateId: orderbook.nonce || Date.now(),
|
||||
bids: (orderbook.bids || [])
|
||||
@@ -843,7 +844,7 @@ export class ParadexGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapTickerToAsterTicker(ticker: CcxtTicker): Ticker {
|
||||
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
|
||||
return {
|
||||
symbol: ticker.symbol,
|
||||
lastPrice: ticker.last?.toString() || "0",
|
||||
@@ -856,7 +857,7 @@ export class ParadexGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapOHLCVToKline(candle: CcxtOhlcv, interval: string): Kline {
|
||||
private mapOHLCVToKline(candle: CcxtOhlcv, interval: string): AsterKline {
|
||||
const [timestampRaw, openRaw, highRaw, lowRaw, closeRaw, volumeRaw] = candle;
|
||||
const timestamp = typeof timestampRaw === "number" && Number.isFinite(timestampRaw)
|
||||
? timestampRaw
|
||||
@@ -926,7 +927,7 @@ export class ParadexGateway {
|
||||
return base[interval] ?? 60 * 1000;
|
||||
}
|
||||
|
||||
private upsertLocalOrder(order: Order): void {
|
||||
private upsertLocalOrder(order: AsterOrder): void {
|
||||
const key = String(order.orderId);
|
||||
if (this.isOrderClosed(order)) {
|
||||
this.localOrders.delete(key);
|
||||
@@ -943,8 +944,8 @@ export class ParadexGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private updateOrdersFromRemote(open: CcxtOrder[], _closed: CcxtOrder[]): void {
|
||||
const nextOpen = new Map<string, Order>();
|
||||
private updateOrdersFromRemote(open: CcxtOrder[], closed: CcxtOrder[]): void {
|
||||
const nextOpen = new Map<string, AsterOrder>();
|
||||
|
||||
for (const order of open) {
|
||||
const mapped = this.mapOrderToAsterOrder(order);
|
||||
@@ -973,7 +974,7 @@ export class ParadexGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private isOrderClosed(order: Order): boolean {
|
||||
private isOrderClosed(order: AsterOrder): boolean {
|
||||
const status = (order.status ?? "").toUpperCase();
|
||||
if (
|
||||
status.includes("CLOSE") ||
|
||||
|
||||
@@ -1,20 +1,93 @@
|
||||
import { createOrderHandlers } from "../order-handlers";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
const handlers = createOrderHandlers({
|
||||
exchangeName: "Paradex",
|
||||
defaultLimitTimeInForce: "GTC",
|
||||
supportsTrailingStop: false,
|
||||
supportsTriggerType: false,
|
||||
stopDefaultReduceOnly: true,
|
||||
stopDefaultClosePosition: true,
|
||||
stopPriceAsPrice: true,
|
||||
closeDefaultClosePosition: true,
|
||||
});
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export const {
|
||||
createLimitOrder,
|
||||
createMarketOrder,
|
||||
createStopOrder,
|
||||
createTrailingStopOrder,
|
||||
createClosePositionOrder,
|
||||
} = handlers;
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "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);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { ExchangeAdapter } from "./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 { BackpackCredentials } from "./backpack/adapter";
|
||||
import type { ParadexCredentials } from "./paradex/adapter";
|
||||
import type { NadoCredentials } from "./nado/adapter";
|
||||
import type { StandxCredentials } from "./standx/adapter";
|
||||
import type { BinanceCredentials } from "./binance/adapter";
|
||||
import type { OndoperpsCredentials } from "./ondoperps/adapter";
|
||||
import { t } from "../i18n";
|
||||
import type { Address } from "viem";
|
||||
|
||||
@@ -51,10 +50,6 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
|
||||
const credentials = resolveBinanceCredentials(symbol);
|
||||
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
|
||||
}
|
||||
case "ondoperps": {
|
||||
const credentials = resolveOndoperpsCredentials(symbol);
|
||||
return createExchangeAdapter({ exchange: id, symbol, ondoperps: credentials });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,26 +200,6 @@ function resolveBinanceCredentials(symbol: string): BinanceCredentials {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveOndoperpsCredentials(symbol: string): OndoperpsCredentials {
|
||||
const apiKeyId = process.env.ONDOPERPS_API_KEY_ID ?? process.env.ONDOPERP_API_KEY_ID ?? process.env.ONDO_KEY_ID;
|
||||
const apiSecret = process.env.ONDOPERPS_API_SECRET ?? process.env.ONDOPERP_API_SECRET ?? process.env.ONDO_API_SECRET;
|
||||
if (!apiKeyId || !apiSecret) {
|
||||
throw new Error(t("env.missingOndoperps"));
|
||||
}
|
||||
const sandbox = parseOptionalBoolean(process.env.ONDOPERPS_SANDBOX ?? process.env.ONDOPERP_SANDBOX) === true;
|
||||
return {
|
||||
apiKeyId,
|
||||
apiSecret,
|
||||
symbol: process.env.ONDOPERPS_SYMBOL ?? process.env.ONDOPERP_SYMBOL ?? symbol,
|
||||
baseUrl: process.env.ONDOPERPS_BASE_URL ?? process.env.ONDOPERP_BASE_URL ?? (sandbox ? "https://api.ondoperps-sandbox.xyz" : undefined),
|
||||
wsUrl: process.env.ONDOPERPS_WS_URL ?? process.env.ONDOPERP_WS_URL ?? (sandbox ? "wss://api.ondoperps-sandbox.xyz/ws" : undefined),
|
||||
builderCode: process.env.ONDOPERPS_BUILDER_CODE ?? process.env.ONDOPERP_BUILDER_CODE ?? undefined,
|
||||
builderFeeRateBps: parseOptionalNumber(
|
||||
process.env.ONDOPERPS_BUILDER_FEE_RATE_BPS ?? process.env.ONDOPERP_BUILDER_FEE_RATE_BPS,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function isHex32(value: string): boolean {
|
||||
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
@@ -9,9 +10,9 @@ import type {
|
||||
RestHealthListener,
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { StandxGateway, type StandxGatewayOptions, type ConnectionEventListener, type ConnectionEventType } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export type { ConnectionEventListener, ConnectionEventType };
|
||||
|
||||
@@ -30,8 +31,11 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
private readonly gateway: StandxGateway;
|
||||
private readonly symbol: string;
|
||||
private readonly safeInvoke = createSafeInvoke("StandxExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private lastInitErrorAt = 0;
|
||||
|
||||
constructor(credentials: StandxCredentials = {}) {
|
||||
const token = credentials.token ?? process.env.STANDX_TOKEN;
|
||||
@@ -48,9 +52,6 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
signingKey: credentials.signingKey,
|
||||
logger: credentials.logger,
|
||||
});
|
||||
this.init = createInitManager("StandxExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -58,52 +59,52 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
void this.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
void this.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
void this.init.ensureInitialized("watchDepth");
|
||||
void this.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
void this.init.ensureInitialized("watchTicker");
|
||||
void this.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.init.ensureInitialized("watchKlines");
|
||||
void this.ensureInitialized("watchKlines");
|
||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||
void this.init.ensureInitialized("watchFundingRate");
|
||||
void this.ensureInitialized("watchFundingRate");
|
||||
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
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.init.ensureInitialized("cancelOrder");
|
||||
await this.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.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
@@ -150,18 +151,18 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
* 查询当前真实的挂单状态(通过 HTTP API)
|
||||
* 用于验证实际挂单情况,防止取消请求丢失
|
||||
*/
|
||||
async queryOpenOrders(): Promise<Order[]> {
|
||||
await this.init.ensureInitialized("queryOpenOrders");
|
||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
||||
await this.ensureInitialized("queryOpenOrders");
|
||||
return this.gateway.queryOpenOrders(this.symbol);
|
||||
}
|
||||
|
||||
async queryAccountSnapshot() {
|
||||
await this.init.ensureInitialized("queryAccountSnapshot");
|
||||
await this.ensureInitialized("queryAccountSnapshot");
|
||||
return this.gateway.queryAccountSnapshot();
|
||||
}
|
||||
|
||||
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||
await this.init.ensureInitialized("changeMarginMode");
|
||||
await this.ensureInitialized("changeMarginMode");
|
||||
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
||||
}
|
||||
|
||||
@@ -170,7 +171,69 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
* 会查询当前挂单然后取消,并验证取消成功
|
||||
*/
|
||||
async forceCancelAllOrders(): Promise<boolean> {
|
||||
await this.init.ensureInitialized("forceCancelAllOrders");
|
||||
await this.ensureInitialized("forceCancelAllOrders");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ import type {
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type {
|
||||
AccountAsset,
|
||||
AccountPosition,
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountAsset,
|
||||
AsterAccountPosition,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
OrderSide,
|
||||
OrderType,
|
||||
@@ -77,7 +77,7 @@ type FundingState = {
|
||||
};
|
||||
|
||||
type VirtualStop = {
|
||||
order: Order;
|
||||
order: AsterOrder;
|
||||
stopPrice: number;
|
||||
side: OrderSide;
|
||||
symbol: string;
|
||||
@@ -279,7 +279,7 @@ function resolutionFromInterval(interval: string): { resolution: string; seconds
|
||||
return { resolution: "1", seconds: 60 };
|
||||
}
|
||||
|
||||
function mergeOrderSnapshot(map: Map<string, Order>, order: Order): void {
|
||||
function mergeOrderSnapshot(map: Map<string, AsterOrder>, order: AsterOrder): void {
|
||||
const key = String(order.orderId);
|
||||
const existing = map.get(key);
|
||||
if (!existing) {
|
||||
@@ -418,12 +418,12 @@ export class StandxGateway {
|
||||
private readonly fundingListeners = new Map<string, Set<FundingRateListener>>();
|
||||
private readonly connectionListeners = new Set<ConnectionEventListener>();
|
||||
|
||||
private readonly openOrders = new Map<string, Order>();
|
||||
private readonly positions = new Map<string, AccountPosition>();
|
||||
private readonly balances = new Map<string, AccountAsset>();
|
||||
private readonly openOrders = new Map<string, AsterOrder>();
|
||||
private readonly positions = new Map<string, AsterAccountPosition>();
|
||||
private readonly balances = new Map<string, AsterAccountAsset>();
|
||||
private readonly virtualStops = new Map<string, VirtualStop>();
|
||||
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private readonly restHealthListeners = new Set<RestHealthListener>();
|
||||
private restConsecutiveErrors = 0;
|
||||
private restUnhealthy = false;
|
||||
@@ -582,14 +582,14 @@ export class StandxGateway {
|
||||
* 查询当前真实的挂单状态(通过 HTTP API)
|
||||
* 用于在网络恢复后验证实际挂单情况
|
||||
*/
|
||||
async queryOpenOrders(symbol: string): Promise<Order[]> {
|
||||
async queryOpenOrders(symbol: string): Promise<AsterOrder[]> {
|
||||
const normalized = normalizeSymbol(symbol);
|
||||
const ordersPayload = await this.requestJson<unknown>("/api/query_open_orders", {
|
||||
method: "GET",
|
||||
params: { symbol: normalized },
|
||||
});
|
||||
const orders = extractOrders(ordersPayload);
|
||||
const result: Order[] = [];
|
||||
const result: AsterOrder[] = [];
|
||||
for (const raw of orders) {
|
||||
const order = this.mapOrder(raw);
|
||||
result.push(order);
|
||||
@@ -618,7 +618,7 @@ export class StandxGateway {
|
||||
}
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
const normalizedSymbol = normalizeSymbol(params.symbol);
|
||||
if (params.type === "STOP_MARKET") {
|
||||
return this.createVirtualStopOrder(normalizedSymbol, params);
|
||||
@@ -785,7 +785,7 @@ export class StandxGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private async createVirtualStopOrder(symbol: string, params: CreateOrderParams): Promise<Order> {
|
||||
private async createVirtualStopOrder(symbol: string, params: CreateOrderParams): Promise<AsterOrder> {
|
||||
const stopPrice = Number(params.stopPrice);
|
||||
if (!Number.isFinite(stopPrice)) {
|
||||
throw new Error("STOP_MARKET requires stopPrice for StandX");
|
||||
@@ -796,7 +796,7 @@ export class StandxGateway {
|
||||
}
|
||||
const now = Date.now();
|
||||
const clientOrderId = crypto.randomUUID();
|
||||
const order: Order = {
|
||||
const order: AsterOrder = {
|
||||
orderId: clientOrderId,
|
||||
clientOrderId,
|
||||
symbol,
|
||||
@@ -825,7 +825,7 @@ export class StandxGateway {
|
||||
return order;
|
||||
}
|
||||
|
||||
private async submitOrder(symbol: string, params: CreateOrderParams): Promise<Order> {
|
||||
private async submitOrder(symbol: string, params: CreateOrderParams): Promise<AsterOrder> {
|
||||
const orderType = params.type === "MARKET" ? "market" : "limit";
|
||||
const clientOrderId = crypto.randomUUID();
|
||||
const qty = toDecimalString(params.quantity);
|
||||
@@ -870,7 +870,7 @@ export class StandxGateway {
|
||||
throw new Error(response.message ?? "StandX order rejected");
|
||||
}
|
||||
const now = Date.now();
|
||||
const order: Order = {
|
||||
const order: AsterOrder = {
|
||||
orderId: clientOrderId,
|
||||
clientOrderId,
|
||||
symbol,
|
||||
@@ -1038,7 +1038,7 @@ export class StandxGateway {
|
||||
}
|
||||
this.logDebug("ws depth stats", detail);
|
||||
}
|
||||
const depth: Depth = {
|
||||
const depth: AsterDepth = {
|
||||
lastUpdateId: Number(message.seq ?? Date.now()),
|
||||
bids: finalBids,
|
||||
asks: finalAsks,
|
||||
@@ -1355,7 +1355,7 @@ export class StandxGateway {
|
||||
console.log(`[StandxGateway] ws raw`, output);
|
||||
}
|
||||
|
||||
private emitDepth(symbol: string, depth: Depth): void {
|
||||
private emitDepth(symbol: string, depth: AsterDepth): void {
|
||||
const listeners = this.depthListeners.get(normalizeSymbol(symbol));
|
||||
if (!listeners) return;
|
||||
for (const listener of listeners) {
|
||||
@@ -1367,7 +1367,7 @@ export class StandxGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private emitTicker(symbol: string, ticker: Ticker): void {
|
||||
private emitTicker(symbol: string, ticker: AsterTicker): void {
|
||||
const listeners = this.tickerListeners.get(normalizeSymbol(symbol));
|
||||
if (!listeners) return;
|
||||
const price = Number(ticker.lastPrice);
|
||||
@@ -1406,7 +1406,7 @@ export class StandxGateway {
|
||||
(sum, position) => sum + Number(position.unrealizedProfit ?? 0),
|
||||
0
|
||||
);
|
||||
const snapshot: AccountSnapshot = {
|
||||
const snapshot: AsterAccountSnapshot = {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
canWithdraw: true,
|
||||
@@ -1427,7 +1427,7 @@ export class StandxGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
private async refreshAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
try {
|
||||
const [balance, positions] = await Promise.all([
|
||||
this.requestJson<StandxBalanceSnapshot>("/api/query_balance", { method: "GET" }),
|
||||
@@ -1444,7 +1444,7 @@ export class StandxGateway {
|
||||
}
|
||||
if (balance) {
|
||||
const token = "DUSD";
|
||||
const asset: AccountAsset = {
|
||||
const asset: AsterAccountAsset = {
|
||||
asset: token,
|
||||
walletBalance: String(balance.balance ?? "0"),
|
||||
availableBalance: String(balance.cross_available ?? balance.balance ?? "0"),
|
||||
@@ -1461,7 +1461,7 @@ export class StandxGateway {
|
||||
}
|
||||
}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
return await this.refreshAccountSnapshot();
|
||||
}
|
||||
|
||||
@@ -1515,7 +1515,7 @@ export class StandxGateway {
|
||||
if (!data?.symbol) return;
|
||||
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 depth: Depth = {
|
||||
const depth: AsterDepth = {
|
||||
lastUpdateId: Date.now(),
|
||||
bids,
|
||||
asks,
|
||||
@@ -1554,7 +1554,7 @@ export class StandxGateway {
|
||||
},
|
||||
});
|
||||
if (!response || response.s !== "ok" || !Array.isArray(response.t)) return;
|
||||
const klines: Kline[] = response.t.map((openTime, index) => {
|
||||
const klines: AsterKline[] = response.t.map((openTime, index) => {
|
||||
const o = response.o?.[index];
|
||||
const h = response.h?.[index];
|
||||
const l = response.l?.[index];
|
||||
@@ -1647,7 +1647,7 @@ export class StandxGateway {
|
||||
}
|
||||
}
|
||||
|
||||
private mapOrder(data: StandxOrder): Order {
|
||||
private mapOrder(data: StandxOrder): AsterOrder {
|
||||
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 normalizedClientId = clientOrderId || orderId;
|
||||
@@ -1675,7 +1675,7 @@ export class StandxGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapPosition(data: StandxPosition): AccountPosition {
|
||||
private mapPosition(data: StandxPosition): AsterAccountPosition {
|
||||
return {
|
||||
symbol: data.symbol,
|
||||
positionAmt: String(data.qty ?? "0"),
|
||||
@@ -1690,7 +1690,7 @@ export class StandxGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapBalance(data: StandxBalance): AccountAsset {
|
||||
private mapBalance(data: StandxBalance): AsterAccountAsset {
|
||||
const walletBalance = data.total ?? data.free ?? "0";
|
||||
const availableBalance = data.free ?? walletBalance;
|
||||
return {
|
||||
@@ -1701,7 +1701,7 @@ export class StandxGateway {
|
||||
};
|
||||
}
|
||||
|
||||
private mapTicker(data: StandxPrice): Ticker {
|
||||
private mapTicker(data: StandxPrice): AsterTicker {
|
||||
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";
|
||||
return {
|
||||
|
||||
@@ -1,21 +1,94 @@
|
||||
import { createOrderHandlers } from "../order-handlers";
|
||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
||||
import type {
|
||||
BaseOrderIntent,
|
||||
ClosePositionIntent,
|
||||
LimitOrderIntent,
|
||||
MarketOrderIntent,
|
||||
StopOrderIntent,
|
||||
TrailingStopOrderIntent,
|
||||
} from "../order-schema";
|
||||
import { toStringBoolean } from "../order-schema";
|
||||
|
||||
const handlers = createOrderHandlers({
|
||||
exchangeName: "StandX",
|
||||
defaultLimitTimeInForce: "GTX",
|
||||
defaultMarketTimeInForce: "IOC",
|
||||
defaultCloseTimeInForce: "IOC",
|
||||
supportsTrailingStop: false,
|
||||
supportsTriggerType: false,
|
||||
stopDefaultReduceOnly: true,
|
||||
stopDefaultClosePosition: true,
|
||||
closeDefaultClosePosition: true,
|
||||
});
|
||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||
if (params.quantity === undefined) {
|
||||
params.quantity = intent.quantity;
|
||||
}
|
||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||
params.timeInForce = intent.timeInForce;
|
||||
}
|
||||
if (intent.reduceOnly !== undefined) {
|
||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||
}
|
||||
if (intent.closePosition !== undefined) {
|
||||
params.closePosition = toStringBoolean(intent.closePosition);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export const {
|
||||
createLimitOrder,
|
||||
createMarketOrder,
|
||||
createStopOrder,
|
||||
createTrailingStopOrder,
|
||||
createClosePositionOrder,
|
||||
} = handlers;
|
||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
||||
const params: CreateOrderParams = applyCommonFields(
|
||||
{
|
||||
symbol: intent.symbol,
|
||||
side: intent.side,
|
||||
type: "LIMIT",
|
||||
quantity: intent.quantity,
|
||||
price: intent.price,
|
||||
timeInForce: intent.timeInForce ?? "GTX",
|
||||
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);
|
||||
}
|
||||
|
||||
+465
-15
@@ -25,12 +25,12 @@ export interface CreateOrderParams {
|
||||
reduceOnly?: StringBoolean;
|
||||
closePosition?: StringBoolean;
|
||||
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||
slPrice?: number;
|
||||
tpPrice?: number;
|
||||
clientOrderId?: string;
|
||||
// StandX TPSL 参数
|
||||
slPrice?: number; // 止损价格
|
||||
tpPrice?: number; // 止盈价格
|
||||
}
|
||||
|
||||
export interface AccountPosition {
|
||||
export interface AsterAccountPosition {
|
||||
symbol: string;
|
||||
positionAmt: string;
|
||||
entryPrice: string;
|
||||
@@ -51,7 +51,210 @@ export interface AccountPosition {
|
||||
markPrice?: string;
|
||||
}
|
||||
|
||||
export interface AccountAsset {
|
||||
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;
|
||||
}
|
||||
|
||||
export interface AsterAccountAsset {
|
||||
asset: string;
|
||||
walletBalance: string;
|
||||
availableBalance: string;
|
||||
@@ -69,7 +272,7 @@ export interface AccountAsset {
|
||||
marginAvailable?: boolean;
|
||||
}
|
||||
|
||||
export interface AccountSnapshot {
|
||||
export interface AsterAccountSnapshot {
|
||||
canTrade: boolean;
|
||||
canDeposit: boolean;
|
||||
canWithdraw: boolean;
|
||||
@@ -85,8 +288,8 @@ export interface AccountSnapshot {
|
||||
totalCrossUnPnl?: string;
|
||||
availableBalance?: string;
|
||||
maxWithdrawAmount?: string;
|
||||
positions: AccountPosition[];
|
||||
assets: AccountAsset[];
|
||||
positions: AsterAccountPosition[];
|
||||
assets: AsterAccountAsset[];
|
||||
marketType?: "perp" | "spot";
|
||||
baseAsset?: string;
|
||||
quoteAsset?: string;
|
||||
@@ -94,22 +297,22 @@ export interface AccountSnapshot {
|
||||
quoteAssetId?: number;
|
||||
}
|
||||
|
||||
export interface DepthLevel extends Array<string> {
|
||||
export interface AsterDepthLevel extends Array<string> {
|
||||
0: string; // price
|
||||
1: string; // quantity
|
||||
}
|
||||
|
||||
export interface Depth {
|
||||
export interface AsterDepth {
|
||||
lastUpdateId: number;
|
||||
bids: DepthLevel[];
|
||||
asks: DepthLevel[];
|
||||
bids: AsterDepthLevel[];
|
||||
asks: AsterDepthLevel[];
|
||||
eventTime?: number;
|
||||
eventType?: string;
|
||||
tradeTime?: number;
|
||||
symbol?: string;
|
||||
}
|
||||
|
||||
export interface Ticker {
|
||||
export interface AsterTicker {
|
||||
symbol: string;
|
||||
lastPrice: string;
|
||||
openPrice: string;
|
||||
@@ -133,7 +336,254 @@ export interface Ticker {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface Kline {
|
||||
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: 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;
|
||||
eventTime?: number;
|
||||
symbol?: string;
|
||||
@@ -154,7 +604,7 @@ export interface Kline {
|
||||
isClosed?: boolean;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
export interface AsterOrder {
|
||||
orderId: number | string;
|
||||
clientOrderId: string;
|
||||
symbol: string;
|
||||
|
||||
@@ -426,10 +426,6 @@ const translations: Record<string, TranslationEntry> = {
|
||||
zh: "StandX 需要配置 STANDX_TOKEN",
|
||||
en: "StandX requires STANDX_TOKEN",
|
||||
},
|
||||
"env.missingOndoperps": {
|
||||
zh: "Ondo Perps 需要配置 ONDOPERPS_API_KEY_ID 与 ONDOPERPS_API_SECRET(兼容旧 ONDOPERP_ 前缀)",
|
||||
en: "Ondo Perps requires ONDOPERPS_API_KEY_ID and ONDOPERPS_API_SECRET (legacy ONDOPERP_ prefix is supported)",
|
||||
},
|
||||
"log.subscribe.accountFail": {
|
||||
zh: "订阅账户失败: {error}",
|
||||
en: "Failed to subscribe account: {error}",
|
||||
|
||||
+22
-56
@@ -4,64 +4,30 @@ import { App } from "./ui/App";
|
||||
import { setupGlobalErrorHandlers } from "./runtime-errors";
|
||||
import { parseCliArgs, printCliHelp } from "./cli/args";
|
||||
import { startStrategy } from "./cli/strategy-runner";
|
||||
import { CommandParseError, parseCommandArgv, printCommandHelp } from "./cli/command-parser";
|
||||
import { executeCliCommand, renderCommandPayload } from "./cli/command-executor";
|
||||
import { resolveExchangeId } from "./exchanges/create-adapter";
|
||||
|
||||
setupGlobalErrorHandlers();
|
||||
void run();
|
||||
const options = parseCliArgs();
|
||||
// If user specifies --exchange, override environment-based resolution for this process
|
||||
if (options.exchange) {
|
||||
// Ensure downstream calls to resolveExchangeId() pick the CLI value.
|
||||
// We set both common env keys respected by resolveExchangeId.
|
||||
process.env.EXCHANGE = options.exchange;
|
||||
process.env.TRADE_EXCHANGE = options.exchange;
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const rawArgv = process.argv.slice(2);
|
||||
if (options.help) {
|
||||
printCliHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedCommand = parseCommandArgv(rawArgv);
|
||||
if (parsedCommand) {
|
||||
if (parsedCommand.kind === "help") {
|
||||
printCommandHelp(parsedCommand.topic);
|
||||
process.exit(0);
|
||||
}
|
||||
const result = await executeCliCommand(parsedCommand);
|
||||
const output = renderCommandPayload(result.payload, parsedCommand.json);
|
||||
if (result.payload.success) {
|
||||
console.log(output);
|
||||
} else {
|
||||
console.error(output);
|
||||
}
|
||||
if (result.forceExit) {
|
||||
process.exit(result.exitCode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof CommandParseError) {
|
||||
console.error(`[CLI] ${error.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const options = parseCliArgs(rawArgv);
|
||||
// If user specifies --exchange, override environment-based resolution for this process
|
||||
if (options.exchange) {
|
||||
// Ensure downstream calls to resolveExchangeId() pick the CLI value.
|
||||
// We set both common env keys respected by resolveExchangeId.
|
||||
process.env.EXCHANGE = options.exchange;
|
||||
process.env.TRADE_EXCHANGE = options.exchange;
|
||||
}
|
||||
|
||||
if (options.help) {
|
||||
printCliHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (options.strategy) {
|
||||
startStrategy(options.strategy, { silent: options.silent })
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[Strategy] Failed to start: ${message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
render(<App />);
|
||||
}
|
||||
if (options.strategy) {
|
||||
startStrategy(options.strategy, { silent: options.silent })
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[Strategy] Failed to start: ${message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
render(<App />);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { BasisArbConfig } from "../config";
|
||||
import type { ExchangeAdapter, FundingRateSnapshot } from "../exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth } from "../exchanges/types";
|
||||
import type { AsterSpotBookTicker } from "../exchanges/aster/types";
|
||||
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/gateway";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterSpotBookTicker } from "../exchanges/types";
|
||||
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/client";
|
||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||
import { StrategyEventEmitter } from "./common/event-emitter";
|
||||
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
||||
@@ -156,7 +155,7 @@ export class BasisArbEngine {
|
||||
private bootstrap(): void {
|
||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||
|
||||
safeSubscribe<Depth>(
|
||||
safeSubscribe<AsterDepth>(
|
||||
this.exchange.watchDepth.bind(this.exchange, this.config.futuresSymbol),
|
||||
(depth) => {
|
||||
this.applyFuturesDepth(depth);
|
||||
@@ -169,7 +168,7 @@ export class BasisArbEngine {
|
||||
);
|
||||
|
||||
if (this.exchange.id === "nado" || this.exchange.id === "standx" || this.exchange.id === "binance") {
|
||||
safeSubscribe<Depth>(
|
||||
safeSubscribe<AsterDepth>(
|
||||
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
|
||||
(depth) => {
|
||||
this.applySpotDepth(depth);
|
||||
@@ -197,7 +196,7 @@ export class BasisArbEngine {
|
||||
);
|
||||
}
|
||||
|
||||
safeSubscribe<AccountSnapshot>(
|
||||
safeSubscribe<AsterAccountSnapshot>(
|
||||
this.exchange.watchAccount.bind(this.exchange),
|
||||
(snapshot) => {
|
||||
this.applyAccountSnapshot(snapshot);
|
||||
@@ -211,7 +210,7 @@ export class BasisArbEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private applyFuturesDepth(depth: Depth): void {
|
||||
private applyFuturesDepth(depth: AsterDepth): void {
|
||||
if (!depth?.bids?.length || !depth?.asks?.length) {
|
||||
return;
|
||||
}
|
||||
@@ -361,7 +360,7 @@ export class BasisArbEngine {
|
||||
this.emitUpdate();
|
||||
}
|
||||
|
||||
private applySpotDepth(depth: Depth): void {
|
||||
private applySpotDepth(depth: AsterDepth): void {
|
||||
if (!depth?.bids?.length || !depth?.asks?.length) {
|
||||
return;
|
||||
}
|
||||
@@ -396,7 +395,7 @@ export class BasisArbEngine {
|
||||
this.emitUpdate();
|
||||
}
|
||||
|
||||
private applyAccountSnapshot(snapshot: AccountSnapshot): void {
|
||||
private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void {
|
||||
const assets = Array.isArray(snapshot.assets) ? snapshot.assets : [];
|
||||
|
||||
const spotBalances: SpotBalanceStateEntry[] = [];
|
||||
@@ -410,7 +409,7 @@ export class BasisArbEngine {
|
||||
if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue;
|
||||
if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue;
|
||||
|
||||
const isTaggedFuturesAsset = name.endsWith("0");
|
||||
const isTaggedFuturesAsset = /0$/.test(name);
|
||||
if (isTaggedFuturesAsset) {
|
||||
futuresBalances.push({ asset: name.replace(/0$/, ""), wallet, available });
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import NodeWebSocket from "ws";
|
||||
import type { DepthLevel } from "../../exchanges/types";
|
||||
import type { AsterDepthLevel } from "../../exchanges/types";
|
||||
import type { DepthImbalance } from "../../utils/depth";
|
||||
|
||||
const WebSocketCtor: typeof globalThis.WebSocket =
|
||||
@@ -56,14 +56,14 @@ export type BinanceConnectionListener = (state: BinanceConnectionState) => void;
|
||||
interface DepthUpdateEvent {
|
||||
U: number;
|
||||
u: number;
|
||||
bids: DepthLevel[];
|
||||
asks: DepthLevel[];
|
||||
bids: AsterDepthLevel[];
|
||||
asks: AsterDepthLevel[];
|
||||
}
|
||||
|
||||
interface DepthSnapshotResponse {
|
||||
lastUpdateId: number;
|
||||
bids: DepthLevel[];
|
||||
asks: DepthLevel[];
|
||||
bids: AsterDepthLevel[];
|
||||
asks: AsterDepthLevel[];
|
||||
}
|
||||
|
||||
export class BinanceDepthTracker {
|
||||
@@ -540,7 +540,7 @@ export class BinanceDepthTracker {
|
||||
return true;
|
||||
}
|
||||
|
||||
private applyLevels(book: Map<string, number>, levels: DepthLevel[]): void {
|
||||
private applyLevels(book: Map<string, number>, levels: AsterDepthLevel[]): void {
|
||||
for (const level of levels) {
|
||||
const priceRaw = level?.[0];
|
||||
const qtyRaw = level?.[1];
|
||||
@@ -657,8 +657,8 @@ export class BinanceDepthTracker {
|
||||
throw new Error("invalid lastUpdateId");
|
||||
}
|
||||
|
||||
const bids = Array.isArray(json.bids) ? (json.bids as DepthLevel[]) : [];
|
||||
const asks = Array.isArray(json.asks) ? (json.asks as DepthLevel[]) : [];
|
||||
const bids = Array.isArray(json.bids) ? (json.bids as AsterDepthLevel[]) : [];
|
||||
const asks = Array.isArray(json.asks) ? (json.asks as AsterDepthLevel[]) : [];
|
||||
this.lastRestSyncAt = Date.now();
|
||||
this.restConsecutiveFailures = 0;
|
||||
this.restLastError = null;
|
||||
@@ -697,8 +697,8 @@ export class BinanceDepthTracker {
|
||||
|
||||
const bidsRaw = Array.isArray(payload.b) ? payload.b : [];
|
||||
const asksRaw = Array.isArray(payload.a) ? payload.a : [];
|
||||
const bids = bidsRaw.filter((level): level is DepthLevel => Array.isArray(level)) as DepthLevel[];
|
||||
const asks = asksRaw.filter((level): level is DepthLevel => Array.isArray(level)) as DepthLevel[];
|
||||
const bids = bidsRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[];
|
||||
const asks = asksRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[];
|
||||
|
||||
return { U, u, bids, asks };
|
||||
} catch {
|
||||
|
||||
@@ -5,19 +5,6 @@ import type { GridDirection } from "../../config";
|
||||
const DATA_DIR = process.env.GRID_DATA_DIR?.trim() || path.resolve("data");
|
||||
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 {
|
||||
symbol: string;
|
||||
lowerPrice: number;
|
||||
@@ -26,8 +13,8 @@ export interface StoredGridState {
|
||||
orderSize: number;
|
||||
maxPositionSize: number;
|
||||
direction: GridDirection;
|
||||
/** Per-level state: key is level index string */
|
||||
levels: Record<string, StoredLevelInfo>;
|
||||
longExposure: Record<string, number>;
|
||||
shortExposure: Record<string, number>;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
|
||||
+511
-812
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import type { TradingConfig } from "../config";
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type { AccountSnapshot, Order, Ticker } from "../exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterOrder, AsterTicker } from "../exchanges/types";
|
||||
import {
|
||||
calcStopLossPrice,
|
||||
calcTrailingActivationPrice,
|
||||
@@ -30,11 +30,11 @@ export interface GuardianEngineSnapshot {
|
||||
unrealized: number;
|
||||
targetStopPrice: number | null;
|
||||
trailingActivationPrice: number | null;
|
||||
stopOrder: Order | null;
|
||||
trailingOrder: Order | null;
|
||||
stopOrder: AsterOrder | null;
|
||||
trailingOrder: AsterOrder | null;
|
||||
requiresStop: boolean;
|
||||
tradeLog: TradeLogEntry[];
|
||||
openOrders: Order[];
|
||||
openOrders: AsterOrder[];
|
||||
lastUpdated: number | null;
|
||||
guardStatus: "idle" | "protecting" | "pending";
|
||||
}
|
||||
@@ -43,9 +43,9 @@ type GuardianEngineEvent = "update";
|
||||
type GuardianEngineListener = (snapshot: GuardianEngineSnapshot) => void;
|
||||
|
||||
export class GuardianEngine {
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private openOrders: Order[] = [];
|
||||
private tickerSnapshot: Ticker | null = null;
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private openOrders: AsterOrder[] = [];
|
||||
private tickerSnapshot: AsterTicker | null = null;
|
||||
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
@@ -101,7 +101,7 @@ export class GuardianEngine {
|
||||
private bootstrap(): void {
|
||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||
|
||||
safeSubscribe<AccountSnapshot>(
|
||||
safeSubscribe<AsterAccountSnapshot>(
|
||||
this.exchange.watchAccount.bind(this.exchange),
|
||||
(snapshot) => {
|
||||
this.accountSnapshot = snapshot;
|
||||
@@ -114,7 +114,7 @@ export class GuardianEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Order[]>(
|
||||
safeSubscribe<AsterOrder[]>(
|
||||
this.exchange.watchOrders.bind(this.exchange),
|
||||
(orders) => {
|
||||
this.synchronizeLocks(orders);
|
||||
@@ -141,7 +141,7 @@ export class GuardianEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Ticker>(
|
||||
safeSubscribe<AsterTicker>(
|
||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||
(ticker) => {
|
||||
this.tickerSnapshot = ticker;
|
||||
@@ -155,7 +155,7 @@ export class GuardianEngine {
|
||||
);
|
||||
}
|
||||
|
||||
private synchronizeLocks(orders: Order[] | null | undefined): void {
|
||||
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
|
||||
const list = Array.isArray(orders) ? orders : [];
|
||||
Object.keys(this.pending).forEach((type) => {
|
||||
const pendingId = this.pending[type];
|
||||
@@ -261,8 +261,8 @@ export class GuardianEngine {
|
||||
price: number;
|
||||
stopPrice: number;
|
||||
activationPrice: number;
|
||||
currentStop?: Order;
|
||||
currentTrailing?: Order;
|
||||
currentStop?: AsterOrder;
|
||||
currentTrailing?: AsterOrder;
|
||||
}): Promise<void> {
|
||||
const { position, direction, stopSide, price, stopPrice, activationPrice, currentStop, currentTrailing } = params;
|
||||
|
||||
@@ -416,7 +416,7 @@ export class GuardianEngine {
|
||||
|
||||
private async tryReplaceStop(
|
||||
side: "BUY" | "SELL",
|
||||
currentOrder: Order,
|
||||
currentOrder: AsterOrder,
|
||||
nextStopPrice: number,
|
||||
lastPrice: number
|
||||
): Promise<void> {
|
||||
@@ -559,7 +559,7 @@ export class GuardianEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private isProtectiveOrder(order: Order): boolean {
|
||||
private isProtectiveOrder(order: AsterOrder): boolean {
|
||||
if (order.symbol !== this.config.symbol) {
|
||||
return false;
|
||||
}
|
||||
@@ -571,14 +571,14 @@ export class GuardianEngine {
|
||||
return type === "STOP_MARKET" || hasStopPrice;
|
||||
}
|
||||
|
||||
private findStopOrder(side: "BUY" | "SELL"): Order | undefined {
|
||||
private findStopOrder(side: "BUY" | "SELL"): AsterOrder | undefined {
|
||||
return this.openOrders.find((order) => {
|
||||
const hasStopPrice = Number.isFinite(Number(order.stopPrice)) && Number(order.stopPrice) > 0;
|
||||
return order.side === side && (order.type === "STOP_MARKET" || hasStopPrice);
|
||||
});
|
||||
}
|
||||
|
||||
private findTrailingOrder(side: "BUY" | "SELL"): Order | undefined {
|
||||
private findTrailingOrder(side: "BUY" | "SELL"): AsterOrder | undefined {
|
||||
return this.openOrders.find((order) => order.type === "TRAILING_STOP_MARKET" && order.side === side);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { LiquidityMakerConfig } from "../config";
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
} from "../exchanges/types";
|
||||
import { formatPriceToString } from "../utils/math";
|
||||
import { createTradeLog } from "../logging/trade-log";
|
||||
@@ -65,12 +65,12 @@ type MakerListener = (snapshot: LiquidityMakerEngineSnapshot) => void;
|
||||
const EPS = 1e-5;
|
||||
|
||||
export class LiquidityMakerEngine {
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private depthSnapshot: Depth | null = null;
|
||||
private tickerSnapshot: Ticker | null = null;
|
||||
private lastKline: Kline | null = null;
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private depthSnapshot: AsterDepth | null = null;
|
||||
private tickerSnapshot: AsterTicker | null = null;
|
||||
private lastKline: AsterKline | null = null;
|
||||
private liveCandle: { startMs: number; open: number; close: number } | null = null;
|
||||
private openOrders: Order[] = [];
|
||||
private openOrders: AsterOrder[] = [];
|
||||
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
@@ -180,7 +180,7 @@ export class LiquidityMakerEngine {
|
||||
private bootstrap(): void {
|
||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||
|
||||
safeSubscribe<AccountSnapshot>(
|
||||
safeSubscribe<AsterAccountSnapshot>(
|
||||
this.exchange.watchAccount.bind(this.exchange),
|
||||
(snapshot) => {
|
||||
this.accountSnapshot = snapshot;
|
||||
@@ -224,7 +224,7 @@ export class LiquidityMakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Order[]>(
|
||||
safeSubscribe<AsterOrder[]>(
|
||||
this.exchange.watchOrders.bind(this.exchange),
|
||||
(orders) => {
|
||||
this.syncLocksWithOrders(orders);
|
||||
@@ -232,7 +232,7 @@ export class LiquidityMakerEngine {
|
||||
|
||||
// 检测成交:对比上一轮的订单ID
|
||||
const currentIds = new Set<string>();
|
||||
const activeOrders: Order[] = [];
|
||||
const activeOrders: AsterOrder[] = [];
|
||||
|
||||
if (Array.isArray(orders)) {
|
||||
for (const order of orders) {
|
||||
@@ -268,7 +268,7 @@ export class LiquidityMakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Depth>(
|
||||
safeSubscribe<AsterDepth>(
|
||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||
(depth) => {
|
||||
this.depthSnapshot = depth;
|
||||
@@ -282,7 +282,7 @@ export class LiquidityMakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Ticker>(
|
||||
safeSubscribe<AsterTicker>(
|
||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||
(ticker) => {
|
||||
this.tickerSnapshot = ticker;
|
||||
@@ -296,7 +296,7 @@ export class LiquidityMakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Kline[]>(
|
||||
safeSubscribe<AsterKline[]>(
|
||||
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
|
||||
(klines) => {
|
||||
if (!Array.isArray(klines) || !klines.length) return;
|
||||
@@ -318,7 +318,7 @@ export class LiquidityMakerEngine {
|
||||
}
|
||||
|
||||
/** 检测成交订单 */
|
||||
private detectFills(orders: Order[] | null | undefined): void {
|
||||
private detectFills(orders: AsterOrder[] | null | undefined): void {
|
||||
if (!Array.isArray(orders)) return;
|
||||
|
||||
// 查找已成交或部分成交的订单
|
||||
@@ -357,7 +357,7 @@ export class LiquidityMakerEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
|
||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
||||
const list = Array.isArray(orders) ? orders : [];
|
||||
Object.keys(this.pending).forEach((type) => {
|
||||
const pendingId = this.pending[type];
|
||||
@@ -759,7 +759,7 @@ export class LiquidityMakerEngine {
|
||||
* 更敏感的偏移判断:当一侧深度超出另一侧 depthImbalanceRatio 倍时,
|
||||
* 取消订单簿较薄一端的订单
|
||||
*/
|
||||
private evaluateDepth(depth: Depth): {
|
||||
private evaluateDepth(depth: AsterDepth): {
|
||||
buySum: number;
|
||||
sellSum: number;
|
||||
skipBuySide: boolean;
|
||||
@@ -1186,7 +1186,7 @@ export class LiquidityMakerEngine {
|
||||
return position;
|
||||
}
|
||||
|
||||
private getSpotBalances(snapshot: AccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
|
||||
private getSpotBalances(snapshot: AsterAccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
|
||||
const assets = snapshot?.assets ?? [];
|
||||
if (!assets.length) return null;
|
||||
const parsed = parseSymbolParts(this.config.symbol);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { MakerConfig } from "../config";
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
} from "../exchanges/types";
|
||||
import { formatPriceToString } from "../utils/math";
|
||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||
@@ -47,7 +48,7 @@ export interface MakerEngineSnapshot {
|
||||
pnl: number;
|
||||
accountUnrealized: number;
|
||||
sessionVolume: number;
|
||||
openOrders: Order[];
|
||||
openOrders: AsterOrder[];
|
||||
desiredOrders: DesiredOrder[];
|
||||
tradeLog: TradeLogEntry[];
|
||||
lastUpdated: number | null;
|
||||
@@ -66,10 +67,10 @@ const EPS = 1e-5;
|
||||
const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000;
|
||||
|
||||
export class MakerEngine {
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private depthSnapshot: Depth | null = null;
|
||||
private tickerSnapshot: Ticker | null = null;
|
||||
private openOrders: Order[] = [];
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private depthSnapshot: AsterDepth | null = null;
|
||||
private tickerSnapshot: AsterTicker | null = null;
|
||||
private openOrders: AsterOrder[] = [];
|
||||
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
@@ -154,7 +155,7 @@ export class MakerEngine {
|
||||
private bootstrap(): void {
|
||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||
|
||||
safeSubscribe<AccountSnapshot>(
|
||||
safeSubscribe<AsterAccountSnapshot>(
|
||||
this.exchange.watchAccount.bind(this.exchange),
|
||||
(snapshot) => {
|
||||
this.accountSnapshot = snapshot;
|
||||
@@ -178,7 +179,7 @@ export class MakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Order[]>(
|
||||
safeSubscribe<AsterOrder[]>(
|
||||
this.exchange.watchOrders.bind(this.exchange),
|
||||
(orders) => {
|
||||
this.syncLocksWithOrders(orders);
|
||||
@@ -211,7 +212,7 @@ export class MakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Depth>(
|
||||
safeSubscribe<AsterDepth>(
|
||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||
(depth) => {
|
||||
this.depthSnapshot = depth;
|
||||
@@ -229,7 +230,7 @@ export class MakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Ticker>(
|
||||
safeSubscribe<AsterTicker>(
|
||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||
(ticker) => {
|
||||
this.tickerSnapshot = ticker;
|
||||
@@ -250,7 +251,7 @@ export class MakerEngine {
|
||||
// Maker strategy does not require realtime klines.
|
||||
}
|
||||
|
||||
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
|
||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
||||
const list = Array.isArray(orders) ? orders : [];
|
||||
Object.keys(this.pending).forEach((type) => {
|
||||
const pendingId = this.pending[type];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MakerPointsConfig } from "../config";
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
} from "../exchanges/types";
|
||||
import { formatPriceToString } from "../utils/math";
|
||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||
@@ -59,7 +59,7 @@ export interface MakerPointsSnapshot {
|
||||
pnl: number;
|
||||
accountUnrealized: number;
|
||||
sessionVolume: number;
|
||||
openOrders: Order[];
|
||||
openOrders: AsterOrder[];
|
||||
desiredOrders: DesiredOrder[];
|
||||
tradeLog: TradeLogEntry[];
|
||||
lastUpdated: number | null;
|
||||
@@ -102,10 +102,10 @@ const STANDX_MARGIN_MODE_MAX_ATTEMPTS = 10;
|
||||
const ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS = 5_000;
|
||||
|
||||
export class MakerPointsEngine {
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private depthSnapshot: Depth | null = null;
|
||||
private tickerSnapshot: Ticker | null = null;
|
||||
private openOrders: Order[] = [];
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private depthSnapshot: AsterDepth | null = null;
|
||||
private tickerSnapshot: AsterTicker | null = null;
|
||||
private openOrders: AsterOrder[] = [];
|
||||
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
@@ -298,7 +298,7 @@ export class MakerPointsEngine {
|
||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||
this.setupRestHealthProtection();
|
||||
|
||||
safeSubscribe<AccountSnapshot>(
|
||||
safeSubscribe<AsterAccountSnapshot>(
|
||||
this.exchange.watchAccount.bind(this.exchange),
|
||||
(snapshot) => {
|
||||
this.applyAccountSnapshot(snapshot);
|
||||
@@ -310,7 +310,7 @@ export class MakerPointsEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Order[]>(
|
||||
safeSubscribe<AsterOrder[]>(
|
||||
this.exchange.watchOrders.bind(this.exchange),
|
||||
(orders) => {
|
||||
this.syncLocksWithOrders(orders);
|
||||
@@ -339,7 +339,7 @@ export class MakerPointsEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Depth>(
|
||||
safeSubscribe<AsterDepth>(
|
||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||
(depth) => {
|
||||
this.depthSnapshot = depth;
|
||||
@@ -358,7 +358,7 @@ export class MakerPointsEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Ticker>(
|
||||
safeSubscribe<AsterTicker>(
|
||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||
(ticker) => {
|
||||
this.tickerSnapshot = ticker;
|
||||
@@ -376,7 +376,7 @@ export class MakerPointsEngine {
|
||||
this.setupConnectionProtection();
|
||||
}
|
||||
|
||||
private applyAccountSnapshot(snapshot: AccountSnapshot): void {
|
||||
private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void {
|
||||
this.accountSnapshot = snapshot;
|
||||
// StandX: WS 推送使用本地接收时间戳;REST 快照使用响应里的 time 字段映射到 snapshot.updateTime
|
||||
this.lastStandxAccountTime =
|
||||
@@ -545,7 +545,7 @@ export class MakerPointsEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
|
||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
||||
const list = Array.isArray(orders) ? orders : [];
|
||||
Object.keys(this.pending).forEach((type) => {
|
||||
const pendingId = this.pending[type];
|
||||
@@ -746,7 +746,7 @@ export class MakerPointsEngine {
|
||||
ask1: number;
|
||||
skipBuy: boolean;
|
||||
skipSell: boolean;
|
||||
depth: Depth | null;
|
||||
depth: AsterDepth | null;
|
||||
}): DesiredOrder[] {
|
||||
const { bid1, ask1, skipBuy, skipSell, depth } = params;
|
||||
|
||||
@@ -837,7 +837,7 @@ export class MakerPointsEngine {
|
||||
* 当深度从足够变为不足,或从不足变为足够时,需要触发重新计算
|
||||
*/
|
||||
private checkDepthStatusChanged(
|
||||
depth: Depth | null,
|
||||
depth: AsterDepth | null,
|
||||
bid1: number,
|
||||
ask1: number
|
||||
): boolean {
|
||||
@@ -879,7 +879,7 @@ export class MakerPointsEngine {
|
||||
/**
|
||||
* 当深度从“满足阈值”切换到“不满足阈值”时,立即触发一次主循环,优先撤销不再安全的挂单。
|
||||
*/
|
||||
private shouldTriggerImmediateDepthProtection(depth: Depth | null): boolean {
|
||||
private shouldTriggerImmediateDepthProtection(depth: AsterDepth | null): boolean {
|
||||
if (!depth) return false;
|
||||
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
||||
|
||||
@@ -917,7 +917,7 @@ export class MakerPointsEngine {
|
||||
/**
|
||||
* 当盘口相对上次报价偏移超过 minRepriceBps 时,立即触发一次主循环,优先撤销旧报价。
|
||||
*/
|
||||
private shouldTriggerImmediateReprice(depth: Depth | null): boolean {
|
||||
private shouldTriggerImmediateReprice(depth: AsterDepth | null): boolean {
|
||||
if (!depth) return false;
|
||||
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
||||
|
||||
@@ -1993,7 +1993,7 @@ export class MakerPointsEngine {
|
||||
void poll();
|
||||
}
|
||||
|
||||
private getStandxMarginMode(snapshot: AccountSnapshot | null): string | null {
|
||||
private getStandxMarginMode(snapshot: AsterAccountSnapshot | null): string | null {
|
||||
if (this.exchange.id !== "standx") return null;
|
||||
const positions = snapshot?.positions ?? [];
|
||||
const match = positions.find((pos) => pos.symbol === this.config.symbol);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MakerConfig } from "../config";
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
} from "../exchanges/types";
|
||||
import { formatPriceToString } from "../utils/math";
|
||||
import { createTradeLog } from "../logging/trade-log";
|
||||
@@ -56,12 +56,12 @@ type MakerListener = (snapshot: OffsetMakerEngineSnapshot) => void;
|
||||
const EPS = 1e-5;
|
||||
|
||||
export class OffsetMakerEngine {
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private depthSnapshot: Depth | null = null;
|
||||
private tickerSnapshot: Ticker | null = null;
|
||||
private lastKline: Kline | null = null;
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private depthSnapshot: AsterDepth | null = null;
|
||||
private tickerSnapshot: AsterTicker | null = null;
|
||||
private lastKline: AsterKline | null = null;
|
||||
private liveCandle: { startMs: number; open: number; close: number } | null = null;
|
||||
private openOrders: Order[] = [];
|
||||
private openOrders: AsterOrder[] = [];
|
||||
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
@@ -163,7 +163,7 @@ export class OffsetMakerEngine {
|
||||
private bootstrap(): void {
|
||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||
|
||||
safeSubscribe<AccountSnapshot>(
|
||||
safeSubscribe<AsterAccountSnapshot>(
|
||||
this.exchange.watchAccount.bind(this.exchange),
|
||||
(snapshot) => {
|
||||
this.accountSnapshot = snapshot;
|
||||
@@ -207,7 +207,7 @@ export class OffsetMakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Order[]>(
|
||||
safeSubscribe<AsterOrder[]>(
|
||||
this.exchange.watchOrders.bind(this.exchange),
|
||||
(orders) => {
|
||||
this.syncLocksWithOrders(orders);
|
||||
@@ -236,7 +236,7 @@ export class OffsetMakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Depth>(
|
||||
safeSubscribe<AsterDepth>(
|
||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||
(depth) => {
|
||||
this.depthSnapshot = depth;
|
||||
@@ -250,7 +250,7 @@ export class OffsetMakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Ticker>(
|
||||
safeSubscribe<AsterTicker>(
|
||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||
(ticker) => {
|
||||
this.tickerSnapshot = ticker;
|
||||
@@ -264,7 +264,7 @@ export class OffsetMakerEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Kline[]>(
|
||||
safeSubscribe<AsterKline[]>(
|
||||
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
|
||||
(klines) => {
|
||||
if (!Array.isArray(klines) || !klines.length) return;
|
||||
@@ -284,7 +284,7 @@ export class OffsetMakerEngine {
|
||||
);
|
||||
}
|
||||
|
||||
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
|
||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
||||
const list = Array.isArray(orders) ? orders : [];
|
||||
Object.keys(this.pending).forEach((type) => {
|
||||
const pendingId = this.pending[type];
|
||||
@@ -600,7 +600,7 @@ export class OffsetMakerEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private evaluateDepth(depth: Depth): {
|
||||
private evaluateDepth(depth: AsterDepth): {
|
||||
buySum: number;
|
||||
sellSum: number;
|
||||
skipBuySide: boolean;
|
||||
@@ -1038,7 +1038,7 @@ export class OffsetMakerEngine {
|
||||
return position;
|
||||
}
|
||||
|
||||
private getSpotBalances(snapshot: AccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
|
||||
private getSpotBalances(snapshot: AsterAccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
|
||||
const assets = snapshot?.assets ?? [];
|
||||
if (!assets.length) return null;
|
||||
const parsed = parseSymbolParts(this.config.symbol);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Order, Ticker } from "../exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterOrder, AsterTicker } from "../exchanges/types";
|
||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||
import { marketClose, placeMarketOrder, placeStopLossOrder, unlockOperating } from "../core/order-coordinator";
|
||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
||||
@@ -55,9 +55,9 @@ export interface SwingEngineSnapshot {
|
||||
stopLossTarget: number | null;
|
||||
stopLossKillSwitch: boolean;
|
||||
|
||||
openOrders: Order[];
|
||||
depth: Depth | null;
|
||||
ticker: Ticker | null;
|
||||
openOrders: AsterOrder[];
|
||||
depth: AsterDepth | null;
|
||||
ticker: AsterTicker | null;
|
||||
|
||||
tradeLog: TradeLogEntry[];
|
||||
lastUpdated: number | null;
|
||||
@@ -70,10 +70,10 @@ type SwingListener = (snapshot: SwingEngineSnapshot) => void;
|
||||
const EPS = 1e-5;
|
||||
|
||||
export class SwingEngine {
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private openOrders: Order[] = [];
|
||||
private depthSnapshot: Depth | null = null;
|
||||
private tickerSnapshot: Ticker | null = null;
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private openOrders: AsterOrder[] = [];
|
||||
private depthSnapshot: AsterDepth | null = null;
|
||||
private tickerSnapshot: AsterTicker | null = null;
|
||||
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
@@ -163,7 +163,7 @@ export class SwingEngine {
|
||||
private bootstrap(): void {
|
||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||
|
||||
safeSubscribe<AccountSnapshot>(
|
||||
safeSubscribe<AsterAccountSnapshot>(
|
||||
this.exchange.watchAccount.bind(this.exchange),
|
||||
(snapshot) => {
|
||||
this.accountSnapshot = snapshot;
|
||||
@@ -192,7 +192,7 @@ export class SwingEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Order[]>(
|
||||
safeSubscribe<AsterOrder[]>(
|
||||
this.exchange.watchOrders.bind(this.exchange),
|
||||
(orders) => {
|
||||
this.synchronizeLocks(orders);
|
||||
@@ -214,7 +214,7 @@ export class SwingEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Depth>(
|
||||
safeSubscribe<AsterDepth>(
|
||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||
(depth) => {
|
||||
this.depthSnapshot = depth;
|
||||
@@ -227,7 +227,7 @@ export class SwingEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Ticker>(
|
||||
safeSubscribe<AsterTicker>(
|
||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||
(ticker) => {
|
||||
this.tickerSnapshot = ticker;
|
||||
@@ -241,7 +241,7 @@ export class SwingEngine {
|
||||
);
|
||||
}
|
||||
|
||||
private synchronizeLocks(orders: Order[] | null | undefined): void {
|
||||
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
|
||||
const list = Array.isArray(orders) ? orders : [];
|
||||
Object.keys(this.pending).forEach((type) => {
|
||||
const pendingId = this.pending[type];
|
||||
@@ -422,7 +422,7 @@ export class SwingEngine {
|
||||
: position.entryPrice * (1 + Math.max(0, this.config.stopLossPct));
|
||||
|
||||
const tick = Math.max(1e-9, this.config.priceTick);
|
||||
const lastPrice = referencePrice;
|
||||
const lastPrice = referencePrice ?? Number(this.tickerSnapshot?.lastPrice) ?? null;
|
||||
|
||||
// Kill-switch (always-on).
|
||||
const triggerKill =
|
||||
@@ -621,3 +621,4 @@ export class SwingEngine {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import crypto from "crypto";
|
||||
import type { TradingConfig } from "../config";
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Order,
|
||||
Ticker,
|
||||
Depth,
|
||||
Kline,
|
||||
AsterAccountSnapshot,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
} from "../exchanges/types";
|
||||
import {
|
||||
calcStopLossPrice,
|
||||
@@ -51,9 +51,9 @@ export interface TrendEngineSnapshot {
|
||||
totalTrades: number;
|
||||
sessionVolume: number;
|
||||
tradeLog: TradeLogEntry[];
|
||||
openOrders: Order[];
|
||||
depth: Depth | null;
|
||||
ticker: Ticker | null;
|
||||
openOrders: AsterOrder[];
|
||||
depth: AsterDepth | null;
|
||||
ticker: AsterTicker | null;
|
||||
lastUpdated: number | null;
|
||||
lastOpenSignal: OpenOrderPlan;
|
||||
}
|
||||
@@ -68,11 +68,11 @@ type TrendEngineEvent = "update";
|
||||
type TrendEngineListener = (snapshot: TrendEngineSnapshot) => void;
|
||||
|
||||
export class TrendEngine {
|
||||
private accountSnapshot: AccountSnapshot | null = null;
|
||||
private openOrders: Order[] = [];
|
||||
private depthSnapshot: Depth | null = null;
|
||||
private tickerSnapshot: Ticker | null = null;
|
||||
private klineSnapshot: Kline[] = [];
|
||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
private openOrders: AsterOrder[] = [];
|
||||
private depthSnapshot: AsterDepth | null = null;
|
||||
private tickerSnapshot: AsterTicker | null = null;
|
||||
private klineSnapshot: AsterKline[] = [];
|
||||
|
||||
private readonly locks: OrderLockMap = {};
|
||||
private readonly timers: OrderTimerMap = {};
|
||||
@@ -164,7 +164,7 @@ export class TrendEngine {
|
||||
private bootstrap(): void {
|
||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||
|
||||
safeSubscribe<AccountSnapshot>(
|
||||
safeSubscribe<AsterAccountSnapshot>(
|
||||
this.exchange.watchAccount.bind(this.exchange),
|
||||
(snapshot) => {
|
||||
this.accountSnapshot = snapshot;
|
||||
@@ -181,7 +181,7 @@ export class TrendEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Order[]>(
|
||||
safeSubscribe<AsterOrder[]>(
|
||||
this.exchange.watchOrders.bind(this.exchange),
|
||||
(orders) => {
|
||||
this.synchronizeLocks(orders);
|
||||
@@ -215,7 +215,7 @@ export class TrendEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Depth>(
|
||||
safeSubscribe<AsterDepth>(
|
||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||
(depth) => {
|
||||
this.depthSnapshot = depth;
|
||||
@@ -228,7 +228,7 @@ export class TrendEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Ticker>(
|
||||
safeSubscribe<AsterTicker>(
|
||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||
(ticker) => {
|
||||
this.tickerSnapshot = ticker;
|
||||
@@ -241,7 +241,7 @@ export class TrendEngine {
|
||||
}
|
||||
);
|
||||
|
||||
safeSubscribe<Kline[]>(
|
||||
safeSubscribe<AsterKline[]>(
|
||||
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, this.config.klineInterval),
|
||||
(klines) => {
|
||||
this.klineSnapshot = Array.isArray(klines) ? klines : [];
|
||||
@@ -258,7 +258,7 @@ export class TrendEngine {
|
||||
);
|
||||
}
|
||||
|
||||
private synchronizeLocks(orders: Order[] | null | undefined): void {
|
||||
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
|
||||
const list = Array.isArray(orders) ? orders : [];
|
||||
Object.keys(this.pending).forEach((type) => {
|
||||
const pendingId = this.pending[type];
|
||||
@@ -386,7 +386,7 @@ export class TrendEngine {
|
||||
private async enforceRateLimitStop(): Promise<void> {
|
||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||
if (Math.abs(position.positionAmt) < 1e-5) return;
|
||||
const price = this.getReferencePrice();
|
||||
const price = this.getReferencePrice() ?? Number(this.tickerSnapshot?.lastPrice) ?? this.lastPrice;
|
||||
if (!Number.isFinite(price) || price == null) return;
|
||||
const result = await this.handlePositionManagement(position, Number(price));
|
||||
if (result.closed) {
|
||||
@@ -834,7 +834,7 @@ export class TrendEngine {
|
||||
|
||||
private async tryReplaceStop(
|
||||
side: "BUY" | "SELL",
|
||||
currentOrder: Order,
|
||||
currentOrder: AsterOrder,
|
||||
nextStopPrice: number,
|
||||
lastPrice: number
|
||||
): Promise<void> {
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import type { Depth } from "../exchanges/types";
|
||||
import type { AsterDepth } from "../exchanges/types";
|
||||
|
||||
export type DepthImbalance = "balanced" | "buy_dominant" | "sell_dominant";
|
||||
|
||||
export function computeDepthStats(
|
||||
depth: Depth,
|
||||
depth: AsterDepth,
|
||||
levels = 10,
|
||||
ratio = 3
|
||||
): {
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
import type { Depth, Ticker } from "../exchanges/types";
|
||||
import type { AsterDepth, AsterTicker } from "../exchanges/types";
|
||||
|
||||
export function getTopPrices(depth?: Depth | null): { topBid: number | null; topAsk: number | null } {
|
||||
export function getTopPrices(depth?: AsterDepth | null): { topBid: number | null; topAsk: number | null } {
|
||||
const bid = Number(depth?.bids?.[0]?.[0]);
|
||||
const ask = Number(depth?.asks?.[0]?.[0]);
|
||||
return {
|
||||
@@ -16,7 +16,7 @@ export function getTopPrices(depth?: Depth | null): { topBid: number | null; top
|
||||
* @returns 指定档位的买卖价格,如果该档位不存在则回退到最近的有效档位
|
||||
*/
|
||||
export function getPricesAtLevel(
|
||||
depth?: Depth | null,
|
||||
depth?: AsterDepth | null,
|
||||
level: number = 1
|
||||
): { bidAtLevel: number | null; askAtLevel: number | null } {
|
||||
const index = Math.max(0, level - 1);
|
||||
@@ -49,7 +49,7 @@ export function getPricesAtLevel(
|
||||
return { bidAtLevel, askAtLevel };
|
||||
}
|
||||
|
||||
export function getMidOrLast(depth?: Depth | null, ticker?: Ticker | null): number | null {
|
||||
export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | null): number | null {
|
||||
const { topBid, topAsk } = getTopPrices(depth);
|
||||
if (topBid != null && topAsk != null) return (topBid + topAsk) / 2;
|
||||
const last = Number(ticker?.lastPrice);
|
||||
@@ -64,7 +64,7 @@ export function getMidOrLast(depth?: Depth | null, ticker?: Ticker | null): numb
|
||||
* @returns 从一档到目标价格之间的挂单总量 (不包含目标价格本身)
|
||||
*/
|
||||
export function getDepthBetweenPrices(
|
||||
depth: Depth | null | undefined,
|
||||
depth: AsterDepth | null | undefined,
|
||||
side: "BUY" | "SELL",
|
||||
targetPrice: number
|
||||
): number {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AccountAsset, AccountSnapshot, Kline } from "../exchanges/types";
|
||||
import type { AsterAccountAsset, AsterAccountSnapshot, AsterKline } from "../exchanges/types";
|
||||
|
||||
export interface PositionSnapshot {
|
||||
positionAmt: number;
|
||||
@@ -7,7 +7,7 @@ export interface PositionSnapshot {
|
||||
markPrice: number | null;
|
||||
}
|
||||
|
||||
export function getPosition(snapshot: AccountSnapshot | null, symbol: string): PositionSnapshot {
|
||||
export function getPosition(snapshot: AsterAccountSnapshot | null, symbol: string): PositionSnapshot {
|
||||
if (!snapshot) {
|
||||
return { positionAmt: 0, entryPrice: 0, unrealizedProfit: 0, markPrice: null };
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export function getPosition(snapshot: AccountSnapshot | null, symbol: string): P
|
||||
}
|
||||
|
||||
export function validateAccountSnapshotForSymbol(
|
||||
snapshot: AccountSnapshot | null,
|
||||
snapshot: AsterAccountSnapshot | null,
|
||||
symbol: string
|
||||
): { ok: true } | { ok: false; issues: string[] } {
|
||||
if (!snapshot) return { ok: true };
|
||||
@@ -88,7 +88,7 @@ export function validateAccountSnapshotForSymbol(
|
||||
return { ok: false, issues: Array.from(new Set(issues)) };
|
||||
}
|
||||
|
||||
export function getSMA(values: Kline[], length: number): number | null {
|
||||
export function getSMA(values: AsterKline[], length: number): number | null {
|
||||
if (!Array.isArray(values) || values.length < length) return null;
|
||||
const window = values.slice(-length);
|
||||
const closes = window.map((kline) => Number(kline.close));
|
||||
@@ -118,7 +118,7 @@ export function calcTrailingActivationPrice(entryPrice: number, qty: number, sid
|
||||
}
|
||||
|
||||
export function computeBollingerBandwidth(
|
||||
values: Kline[],
|
||||
values: AsterKline[],
|
||||
length: number,
|
||||
stdMultiplier: number
|
||||
): number | null {
|
||||
@@ -190,7 +190,7 @@ function normalizeBaseSymbol(symbol: string | undefined): string | undefined {
|
||||
return parseSymbolParts(symbol).base;
|
||||
}
|
||||
|
||||
function selectAsset(assets: AccountAsset[], baseSymbol: string, baseAssetId?: number): AccountAsset | undefined {
|
||||
function selectAsset(assets: AsterAccountAsset[], baseSymbol: string, baseAssetId?: number): AsterAccountAsset | undefined {
|
||||
const normalized = baseSymbol.toUpperCase();
|
||||
const targetId = Number(baseAssetId);
|
||||
return assets.find((asset) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AccountSnapshot } from "../src/exchanges/types";
|
||||
import type { AsterAccountSnapshot } from "../src/exchanges/types";
|
||||
import { validateAccountSnapshotForSymbol } from "../src/utils/strategy";
|
||||
|
||||
function baseSnapshot(positions: AccountSnapshot["positions"]): AccountSnapshot {
|
||||
function baseSnapshot(positions: AsterAccountSnapshot["positions"]): AsterAccountSnapshot {
|
||||
return {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AsterSpotRestClient } from "../src/exchanges/aster/gateway";
|
||||
import { AsterSpotRestClient } from "../src/exchanges/aster/client";
|
||||
|
||||
describe("AsterSpotRestClient", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
} from "../src/exchanges/types";
|
||||
import { BasisArbEngine } from "../src/strategy/basis-arb-engine";
|
||||
|
||||
class StubAdapter implements ExchangeAdapter {
|
||||
id = "aster";
|
||||
private depthHandler: ((depth: Depth) => void) | null = null;
|
||||
private depthHandler: ((depth: AsterDepth) => void) | null = null;
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {
|
||||
// not required for this test
|
||||
}
|
||||
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {
|
||||
// not required for this test
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
||||
this.depthHandler = cb;
|
||||
}
|
||||
|
||||
emitDepth(depth: Depth): void {
|
||||
emitDepth(depth: AsterDepth): void {
|
||||
this.depthHandler?.(depth);
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {
|
||||
// not required for this test
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {
|
||||
// not required for this test
|
||||
}
|
||||
|
||||
createOrder(): Promise<Order> {
|
||||
createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
|
||||
const ORIGINAL_FETCH = globalThis.fetch;
|
||||
@@ -12,13 +12,13 @@ class StubAdapter implements ExchangeAdapter {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
async createOrder(): Promise<Order> {
|
||||
async createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class StubAdapter implements ExchangeAdapter {
|
||||
async cancelOrders(): Promise<void> {}
|
||||
async cancelAllOrders(): Promise<void> {}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
return {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { executeCliCommand } from "../src/cli/command-executor";
|
||||
import type { ParsedCliCommand } from "../src/cli/command-types";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
CreateOrderParams,
|
||||
} from "../src/exchanges/types";
|
||||
|
||||
class FakeAdapter implements ExchangeAdapter {
|
||||
readonly id = "aster";
|
||||
createOrderCalls = 0;
|
||||
cancelOrderCalls = 0;
|
||||
cancelOrdersCalls = 0;
|
||||
cancelAllOrdersCalls = 0;
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
watchAccount(cb: (snapshot: AccountSnapshot) => void): void {
|
||||
cb({
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
canWithdraw: true,
|
||||
updateTime: Date.now(),
|
||||
totalWalletBalance: "100",
|
||||
totalUnrealizedProfit: "0",
|
||||
positions: [],
|
||||
assets: [],
|
||||
});
|
||||
}
|
||||
|
||||
watchOrders(cb: (orders: Order[]) => void): void {
|
||||
cb([]);
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||
cb({
|
||||
lastUpdateId: 1,
|
||||
bids: [["100", "1"]],
|
||||
asks: [["101", "1"]],
|
||||
});
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: (ticker: Ticker) => void): void {
|
||||
cb({
|
||||
symbol,
|
||||
lastPrice: "100",
|
||||
openPrice: "99",
|
||||
highPrice: "102",
|
||||
lowPrice: "98",
|
||||
volume: "10",
|
||||
quoteVolume: "1000",
|
||||
eventTime: Date.now(),
|
||||
} as Ticker);
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, _interval: string, cb: (klines: Kline[]) => void): void {
|
||||
cb([
|
||||
{
|
||||
openTime: 1,
|
||||
open: "100",
|
||||
high: "101",
|
||||
low: "99",
|
||||
close: "100",
|
||||
volume: "1",
|
||||
closeTime: 2,
|
||||
numberOfTrades: 1,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
this.createOrderCalls += 1;
|
||||
return {
|
||||
orderId: "1",
|
||||
clientOrderId: "1",
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
type: params.type,
|
||||
status: "NEW",
|
||||
price: String(params.price ?? 0),
|
||||
origQty: String(params.quantity ?? 0),
|
||||
executedQty: "0",
|
||||
stopPrice: String(params.stopPrice ?? 0),
|
||||
time: Date.now(),
|
||||
updateTime: Date.now(),
|
||||
reduceOnly: params.reduceOnly === "true",
|
||||
closePosition: params.closePosition === "true",
|
||||
};
|
||||
}
|
||||
|
||||
async cancelOrder(): Promise<void> {
|
||||
this.cancelOrderCalls += 1;
|
||||
}
|
||||
|
||||
async cancelOrders(): Promise<void> {
|
||||
this.cancelOrdersCalls += 1;
|
||||
}
|
||||
|
||||
async cancelAllOrders(): Promise<void> {
|
||||
this.cancelAllOrdersCalls += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function common(overrides: Partial<ParsedCliCommand> = {}): any {
|
||||
return {
|
||||
json: true,
|
||||
dryRun: false,
|
||||
timeoutMs: 1000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("command executor", () => {
|
||||
it("executes market ticker command", async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
const command: ParsedCliCommand = {
|
||||
kind: "market-ticker",
|
||||
...common({
|
||||
exchange: "aster",
|
||||
symbol: "BTCUSDT",
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await executeCliCommand(command, {
|
||||
buildAdapterFromEnvFn: () => adapter,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.payload.success).toBe(true);
|
||||
if (result.payload.success) {
|
||||
expect((result.payload.data as any).ticker.symbol).toBe("BTCUSDT");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses dry-run wrapper for order create", async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
const command: ParsedCliCommand = {
|
||||
kind: "order-create",
|
||||
...common({
|
||||
exchange: "aster",
|
||||
symbol: "BTCUSDT",
|
||||
dryRun: true,
|
||||
}),
|
||||
payload: {
|
||||
side: "BUY",
|
||||
type: "limit",
|
||||
quantity: 0.01,
|
||||
price: 100000,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await executeCliCommand(command, {
|
||||
buildAdapterFromEnvFn: () => adapter,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(adapter.createOrderCalls).toBe(0);
|
||||
expect(result.payload.success).toBe(true);
|
||||
if (result.payload.success) {
|
||||
const data = result.payload.data as any;
|
||||
expect(Array.isArray(data.dryRunActions)).toBe(true);
|
||||
expect(data.dryRunActions.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses dry-run wrapper for order cancel", async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
const command: ParsedCliCommand = {
|
||||
kind: "order-cancel",
|
||||
...common({
|
||||
exchange: "aster",
|
||||
symbol: "BTCUSDT",
|
||||
dryRun: true,
|
||||
}),
|
||||
orderId: "abc",
|
||||
};
|
||||
|
||||
const result = await executeCliCommand(command, {
|
||||
buildAdapterFromEnvFn: () => adapter,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(adapter.cancelOrderCalls).toBe(0);
|
||||
expect(result.payload.success).toBe(true);
|
||||
});
|
||||
|
||||
it("returns unsupported for order-open when queryOpenOrders is unavailable", async () => {
|
||||
const adapter = new FakeAdapter();
|
||||
const command: ParsedCliCommand = {
|
||||
kind: "order-open",
|
||||
...common({
|
||||
exchange: "aster",
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await executeCliCommand(command, {
|
||||
buildAdapterFromEnvFn: () => adapter,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(5);
|
||||
expect(result.payload.success).toBe(false);
|
||||
if (!result.payload.success) {
|
||||
expect(result.payload.error.code).toBe("UNSUPPORTED");
|
||||
}
|
||||
});
|
||||
|
||||
it("passes dryRun to strategy runner", async () => {
|
||||
const startStrategyFn = vi.fn(async () => undefined);
|
||||
const command: ParsedCliCommand = {
|
||||
kind: "strategy-run",
|
||||
...common({
|
||||
exchange: "aster",
|
||||
dryRun: true,
|
||||
}),
|
||||
strategy: "trend",
|
||||
silent: true,
|
||||
};
|
||||
|
||||
const result = await executeCliCommand(command, {
|
||||
startStrategyFn,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(startStrategyFn).toHaveBeenCalledWith("trend", { silent: true, dryRun: true });
|
||||
});
|
||||
|
||||
it("falls back to static capabilities when adapter creation fails", async () => {
|
||||
const command: ParsedCliCommand = {
|
||||
kind: "exchange-capabilities",
|
||||
...common({
|
||||
exchange: "binance",
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await executeCliCommand(command, {
|
||||
buildAdapterFromEnvFn: () => {
|
||||
throw new Error("Missing BINANCE_API_KEY environment variable");
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.payload.success).toBe(true);
|
||||
if (result.payload.success) {
|
||||
expect((result.payload.data as any).source).toBe("static");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CommandParseError, parseCommandArgv } from "../src/cli/command-parser";
|
||||
|
||||
describe("command parser", () => {
|
||||
it("returns null for legacy flag-only argv", () => {
|
||||
expect(parseCommandArgv(["--strategy", "trend"])).toBeNull();
|
||||
});
|
||||
|
||||
it("parses doctor command with global options", () => {
|
||||
const command = parseCommandArgv(["doctor", "--json", "--dry-run", "--timeout", "1000"]);
|
||||
expect(command).toMatchObject({
|
||||
kind: "doctor",
|
||||
json: true,
|
||||
dryRun: true,
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses market kline command", () => {
|
||||
const command = parseCommandArgv([
|
||||
"market",
|
||||
"kline",
|
||||
"--exchange",
|
||||
"binance",
|
||||
"--symbol",
|
||||
"BTCUSDT_PERP",
|
||||
"--interval",
|
||||
"1m",
|
||||
"--limit",
|
||||
"10",
|
||||
]);
|
||||
expect(command).toMatchObject({
|
||||
kind: "market-kline",
|
||||
exchange: "binance",
|
||||
symbol: "BTCUSDT_PERP",
|
||||
interval: "1m",
|
||||
limit: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses order create trailing-stop command", () => {
|
||||
const command = parseCommandArgv([
|
||||
"order",
|
||||
"create",
|
||||
"--exchange",
|
||||
"grv",
|
||||
"--symbol",
|
||||
"BTCUSDT",
|
||||
"--side",
|
||||
"sell",
|
||||
"--type",
|
||||
"trailing-stop",
|
||||
"--qty",
|
||||
"0.01",
|
||||
"--activation-price",
|
||||
"101000",
|
||||
"--callback-rate",
|
||||
"0.2",
|
||||
"--dry-run",
|
||||
]);
|
||||
expect(command).toMatchObject({
|
||||
kind: "order-create",
|
||||
exchange: "grvt",
|
||||
symbol: "BTCUSDT",
|
||||
dryRun: true,
|
||||
payload: {
|
||||
side: "SELL",
|
||||
type: "trailing-stop",
|
||||
quantity: 0.01,
|
||||
activationPrice: 101000,
|
||||
callbackRate: 0.2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("parses strategy run command with alias strategy", () => {
|
||||
const command = parseCommandArgv(["strategy", "run", "--strategy", "offset"]);
|
||||
expect(command).toMatchObject({
|
||||
kind: "strategy-run",
|
||||
strategy: "offset-maker",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses the Ondo Perps exchange alias", () => {
|
||||
for (const exchange of ["ondoperps", "ondoperp"]) {
|
||||
const command = parseCommandArgv(["exchange", "capabilities", "--exchange", exchange]);
|
||||
expect(command).toMatchObject({
|
||||
kind: "exchange-capabilities",
|
||||
exchange: "ondoperps",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("throws for unsupported option", () => {
|
||||
expect(() => parseCommandArgv(["doctor", "--unknown"])).toThrow(CommandParseError);
|
||||
});
|
||||
});
|
||||
@@ -56,19 +56,4 @@ describe("resolveSymbolFromEnv", () => {
|
||||
|
||||
expect(resolveSymbolFromEnv("binance")).toBe("ETHUSDT");
|
||||
});
|
||||
|
||||
it("supports Ondo Perps symbol defaults when explicit exchange id is provided", () => {
|
||||
delete process.env.EXCHANGE;
|
||||
process.env.ONDOPERPS_SYMBOL = "NVDA-USD.P";
|
||||
|
||||
expect(resolveSymbolFromEnv("ondoperps")).toBe("NVDA-USD.P");
|
||||
});
|
||||
|
||||
it("supports the legacy Ondo Perps exchange id and symbol prefix", () => {
|
||||
delete process.env.EXCHANGE;
|
||||
delete process.env.ONDOPERPS_SYMBOL;
|
||||
process.env.ONDOPERP_SYMBOL = "ETH-USD.P";
|
||||
|
||||
expect(resolveSymbolFromEnv("ondoperp")).toBe("ETH-USD.P");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DryRunExchangeAdapter } from "../src/exchanges/dry-run-adapter";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
CreateOrderParams,
|
||||
} from "../src/exchanges/types";
|
||||
|
||||
class BaseAdapter implements ExchangeAdapter {
|
||||
readonly id = "aster";
|
||||
createCalls = 0;
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
|
||||
async createOrder(_params: CreateOrderParams): Promise<Order> {
|
||||
this.createCalls += 1;
|
||||
throw new Error("should not be called in dry-run");
|
||||
}
|
||||
|
||||
async cancelOrder(): Promise<void> {}
|
||||
async cancelOrders(): Promise<void> {}
|
||||
async cancelAllOrders(): Promise<void> {}
|
||||
}
|
||||
|
||||
describe("DryRunExchangeAdapter", () => {
|
||||
it("simulates create order and records actions", async () => {
|
||||
const base = new BaseAdapter();
|
||||
const dry = new DryRunExchangeAdapter(base);
|
||||
|
||||
const order = await dry.createOrder({
|
||||
symbol: "BTCUSDT",
|
||||
side: "BUY",
|
||||
type: "LIMIT",
|
||||
quantity: 0.01,
|
||||
price: 100000,
|
||||
});
|
||||
|
||||
expect(base.createCalls).toBe(0);
|
||||
expect(order.orderId).toMatch(/^dry-run-/);
|
||||
expect(dry.actions.length).toBe(1);
|
||||
expect(dry.actions[0]?.method).toBe("createOrder");
|
||||
});
|
||||
});
|
||||
@@ -18,11 +18,11 @@ import {
|
||||
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Kline,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterKline,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
} from "../src/exchanges/types";
|
||||
|
||||
@@ -42,7 +42,7 @@ const REQUIRED_ENV_BY_EXCHANGE: Record<SupportedExchangeId, Record<string, strin
|
||||
},
|
||||
lighter: {
|
||||
LIGHTER_ACCOUNT_INDEX: "1",
|
||||
LIGHTER_API_PRIVATE_KEY: "0xed636277f3753b6c0275f7a28c2678a7f3a95655e09deaebec15179b50c5da7f903152e50f594f7b",
|
||||
LIGHTER_API_PRIVATE_KEY: "lighter-private-key",
|
||||
LIGHTER_API_KEY_INDEX: "0",
|
||||
},
|
||||
backpack: {
|
||||
@@ -64,10 +64,6 @@ const REQUIRED_ENV_BY_EXCHANGE: Record<SupportedExchangeId, Record<string, strin
|
||||
BINANCE_API_KEY: "binance-key",
|
||||
BINANCE_API_SECRET: "binance-secret",
|
||||
},
|
||||
ondoperps: {
|
||||
ONDOPERPS_API_KEY_ID: "ondoKeyId_test",
|
||||
ONDOPERPS_API_SECRET: "ondoApiSecret_test",
|
||||
},
|
||||
};
|
||||
|
||||
class RecorderAdapter implements ExchangeAdapter {
|
||||
@@ -82,17 +78,17 @@ class RecorderAdapter implements ExchangeAdapter {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
|
||||
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
||||
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
this.lastCreateOrderParams = params;
|
||||
return {
|
||||
orderId: 1,
|
||||
@@ -155,7 +151,6 @@ describe("exchange contract suite", () => {
|
||||
for (const id of SUPPORTED_EXCHANGE_IDS) {
|
||||
expect(output).toContain(id);
|
||||
}
|
||||
expect(parseCliArgs(["--exchange", "ondoperp"]).exchange).toBe("ondoperps");
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -263,21 +258,4 @@ describe("exchange contract suite", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("routes orders from adapters that still report the legacy ondoperp id", async () => {
|
||||
delete process.env.EXCHANGE;
|
||||
delete process.env.TRADE_EXCHANGE;
|
||||
const adapter = new RecorderAdapter("ondoperps");
|
||||
(adapter as unknown as { id: string }).id = "ondoperp";
|
||||
|
||||
await routeLimitOrder({
|
||||
adapter,
|
||||
symbol: "BTC-USD.P",
|
||||
side: "BUY",
|
||||
quantity: 0.01,
|
||||
price: 100_000,
|
||||
});
|
||||
|
||||
expect(adapter.lastCreateOrderParams?.type).toBe("LIMIT");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
import { createExchangeAdapter, resolveExchangeId } from "../src/exchanges/create-adapter";
|
||||
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
|
||||
import { AsterExchangeAdapter } from "../src/exchanges/aster/adapter";
|
||||
import { AsterExchangeAdapter } from "../src/exchanges/aster-adapter";
|
||||
import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
|
||||
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
|
||||
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
|
||||
import { StandxExchangeAdapter } from "../src/exchanges/standx/adapter";
|
||||
import { BinanceExchangeAdapter } from "../src/exchanges/binance/adapter";
|
||||
import { OndoperpsExchangeAdapter } from "../src/exchanges/ondoperps/adapter";
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env };
|
||||
|
||||
@@ -36,8 +34,6 @@ describe("exchange factory", () => {
|
||||
expect(resolveExchangeId("PaRaDeX")).toBe("paradex");
|
||||
expect(resolveExchangeId("StandX")).toBe("standx");
|
||||
expect(resolveExchangeId("BiNaNcE")).toBe("binance");
|
||||
expect(resolveExchangeId("OndoPerps")).toBe("ondoperps");
|
||||
expect(resolveExchangeId("OndoPerp")).toBe("ondoperps");
|
||||
});
|
||||
|
||||
it("creates grvt adapter when EXCHANGE=grvt", () => {
|
||||
@@ -96,41 +92,4 @@ describe("exchange factory", () => {
|
||||
expect(adapter).toBeInstanceOf(BinanceExchangeAdapter);
|
||||
expect(adapter.id).toBe("binance");
|
||||
});
|
||||
|
||||
it("creates ondoperps adapter when EXCHANGE=ondoperps", () => {
|
||||
process.env.EXCHANGE = "ondoperps";
|
||||
process.env.ONDOPERPS_API_KEY_ID = "ondoKeyId_test";
|
||||
process.env.ONDOPERPS_API_SECRET = "ondoApiSecret_test";
|
||||
process.env.ONDOPERPS_SYMBOL = "XAU-USD.P";
|
||||
|
||||
const adapter = createExchangeAdapter({ symbol: "XAU-USD.P" });
|
||||
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
|
||||
expect(adapter.id).toBe("ondoperps");
|
||||
});
|
||||
|
||||
it("accepts the legacy ondoperp id and environment prefix", () => {
|
||||
delete process.env.ONDOPERPS_API_KEY_ID;
|
||||
delete process.env.ONDOPERPS_API_SECRET;
|
||||
delete process.env.ONDOPERPS_SYMBOL;
|
||||
process.env.ONDOPERP_API_KEY_ID = "ondoKeyId_legacy";
|
||||
process.env.ONDOPERP_API_SECRET = "ondoApiSecret_legacy";
|
||||
process.env.ONDOPERP_SYMBOL = "ETH-USD.P";
|
||||
|
||||
const adapter = buildAdapterFromEnv({ exchangeId: "ondoperp", symbol: "BTC-USD.P" });
|
||||
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
|
||||
expect(adapter.id).toBe("ondoperps");
|
||||
});
|
||||
|
||||
it("accepts the legacy ondoperp factory option", () => {
|
||||
const adapter = createExchangeAdapter({
|
||||
exchange: "ondoperp",
|
||||
symbol: "BTC-USD.P",
|
||||
ondoperp: {
|
||||
apiKeyId: "ondoKeyId_legacy",
|
||||
apiSecret: "ondoApiSecret_legacy",
|
||||
},
|
||||
});
|
||||
expect(adapter).toBeInstanceOf(OndoperpsExchangeAdapter);
|
||||
expect(adapter.id).toBe("ondoperps");
|
||||
});
|
||||
});
|
||||
|
||||
+38
-385
@@ -1,25 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type {
|
||||
AccountSnapshot,
|
||||
Depth,
|
||||
Order,
|
||||
Ticker,
|
||||
AsterAccountSnapshot,
|
||||
AsterDepth,
|
||||
AsterOrder,
|
||||
AsterTicker,
|
||||
CreateOrderParams,
|
||||
} from "../src/exchanges/types";
|
||||
import type { GridConfig } from "../src/config";
|
||||
import { GridEngine } from "../src/strategy/grid-engine";
|
||||
|
||||
let orderCounter = 0;
|
||||
|
||||
class StubAdapter implements ExchangeAdapter {
|
||||
id = "aster";
|
||||
|
||||
private accountHandler: ((snapshot: AccountSnapshot) => void) | null = null;
|
||||
private orderHandler: ((orders: Order[]) => void) | null = null;
|
||||
private depthHandler: ((depth: Depth) => void) | null = null;
|
||||
private tickerHandler: ((ticker: Ticker) => void) | null = null;
|
||||
private currentOrders: Order[] = [];
|
||||
private accountHandler: ((snapshot: AsterAccountSnapshot) => void) | null = null;
|
||||
private orderHandler: ((orders: AsterOrder[]) => void) | null = null;
|
||||
private depthHandler: ((depth: AsterDepth) => void) | null = null;
|
||||
private tickerHandler: ((ticker: AsterTicker) => void) | null = null;
|
||||
private currentOrders: AsterOrder[] = [];
|
||||
|
||||
public createdOrders: CreateOrderParams[] = [];
|
||||
public marketOrders: CreateOrderParams[] = [];
|
||||
@@ -30,19 +28,19 @@ class StubAdapter implements ExchangeAdapter {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(cb: (snapshot: AccountSnapshot) => void): void {
|
||||
watchAccount(cb: (snapshot: AsterAccountSnapshot) => void): void {
|
||||
this.accountHandler = cb;
|
||||
}
|
||||
|
||||
watchOrders(cb: (orders: Order[]) => void): void {
|
||||
watchOrders(cb: (orders: AsterOrder[]) => void): void {
|
||||
this.orderHandler = cb;
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
||||
this.depthHandler = cb;
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, cb: (ticker: Ticker) => void): void {
|
||||
watchTicker(_symbol: string, cb: (ticker: AsterTicker) => void): void {
|
||||
this.tickerHandler = cb;
|
||||
}
|
||||
|
||||
@@ -50,28 +48,26 @@ class StubAdapter implements ExchangeAdapter {
|
||||
// not used in tests
|
||||
}
|
||||
|
||||
emitAccount(snapshot: AccountSnapshot): void {
|
||||
emitAccount(snapshot: AsterAccountSnapshot): void {
|
||||
this.accountHandler?.(snapshot);
|
||||
}
|
||||
|
||||
emitOrders(orders: Order[]): void {
|
||||
emitOrders(orders: AsterOrder[]): void {
|
||||
this.orderHandler?.(orders);
|
||||
}
|
||||
|
||||
emitDepth(depth: Depth): void {
|
||||
emitDepth(depth: AsterDepth): void {
|
||||
this.depthHandler?.(depth);
|
||||
}
|
||||
|
||||
emitTicker(ticker: Ticker): void {
|
||||
emitTicker(ticker: AsterTicker): void {
|
||||
this.tickerHandler?.(ticker);
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
orderCounter++;
|
||||
const orderId = params.clientOrderId ?? `stub-${orderCounter}`;
|
||||
const order: Order = {
|
||||
orderId,
|
||||
clientOrderId: params.clientOrderId ?? orderId,
|
||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
||||
const order: AsterOrder = {
|
||||
orderId: `${Date.now()}-${Math.random()}`,
|
||||
clientOrderId: "test",
|
||||
symbol: params.symbol,
|
||||
side: params.side,
|
||||
type: params.type,
|
||||
@@ -90,21 +86,18 @@ class StubAdapter implements ExchangeAdapter {
|
||||
this.marketOrders.push(params);
|
||||
this.orderHandler?.([]);
|
||||
} else {
|
||||
this.currentOrders.push(order);
|
||||
this.orderHandler?.([...this.currentOrders]);
|
||||
this.currentOrders = [order];
|
||||
this.orderHandler?.(this.currentOrders);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
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> {
|
||||
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> {
|
||||
@@ -112,17 +105,9 @@ class StubAdapter implements ExchangeAdapter {
|
||||
this.currentOrders = [];
|
||||
this.orderHandler?.([]);
|
||||
}
|
||||
|
||||
clearCurrentOrders(): void {
|
||||
this.currentOrders = [];
|
||||
}
|
||||
|
||||
getCurrentOrders(): Order[] {
|
||||
return [...this.currentOrders];
|
||||
}
|
||||
}
|
||||
|
||||
function createAccountSnapshot(symbol: string, positionAmt: number): AccountSnapshot {
|
||||
function createAccountSnapshot(symbol: string, positionAmt: number): AsterAccountSnapshot {
|
||||
return {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
@@ -141,7 +126,7 @@ function createAccountSnapshot(symbol: string, positionAmt: number): AccountSnap
|
||||
},
|
||||
],
|
||||
assets: [],
|
||||
} as unknown as AccountSnapshot;
|
||||
} as unknown as AsterAccountSnapshot;
|
||||
}
|
||||
|
||||
describe("GridEngine", () => {
|
||||
@@ -166,7 +151,7 @@ describe("GridEngine", () => {
|
||||
|
||||
it("creates geometric desired orders when running in both directions", async () => {
|
||||
const adapter = new StubAdapter();
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
||||
|
||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||
adapter.emitOrders([]);
|
||||
@@ -196,7 +181,7 @@ describe("GridEngine", () => {
|
||||
|
||||
it("limits sell orders for long-only direction when no position is available", () => {
|
||||
const adapter = new StubAdapter();
|
||||
const engine = new GridEngine({ ...baseConfig, direction: "long" }, adapter, { now: () => 0, skipPersistence: true });
|
||||
const engine = new GridEngine({ ...baseConfig, direction: "long" }, adapter, { now: () => 0 });
|
||||
|
||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||
adapter.emitOrders([]);
|
||||
@@ -213,7 +198,7 @@ describe("GridEngine", () => {
|
||||
|
||||
it("does not repopulate the same buy level until exposure is released", () => {
|
||||
const adapter = new StubAdapter();
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
||||
|
||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||
adapter.emitOrders([]);
|
||||
@@ -238,7 +223,7 @@ describe("GridEngine", () => {
|
||||
|
||||
it("keeps level side assignments stable regardless of price", () => {
|
||||
const adapter = new StubAdapter();
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
||||
|
||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||
adapter.emitOrders([]);
|
||||
@@ -260,7 +245,7 @@ describe("GridEngine", () => {
|
||||
|
||||
it("limits active sell orders by remaining short headroom", () => {
|
||||
const adapter = new StubAdapter();
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
||||
|
||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||
adapter.emitOrders([]);
|
||||
@@ -270,7 +255,7 @@ describe("GridEngine", () => {
|
||||
expect(sellCountFull).toBeGreaterThan(0);
|
||||
|
||||
const limitedHeadroomConfig = { ...baseConfig, maxPositionSize: baseConfig.orderSize * 2 };
|
||||
const limitedEngine = new GridEngine(limitedHeadroomConfig, adapter as any, { now: () => 0, skipPersistence: true });
|
||||
const limitedEngine = new GridEngine(limitedHeadroomConfig, adapter as any, { now: () => 0 });
|
||||
(limitedEngine as any).shortExposure.set(12, baseConfig.orderSize * 2);
|
||||
|
||||
const desiredLimited = (limitedEngine as any).computeDesiredOrders(2.1) as Array<{ level: number; side: string }>;
|
||||
@@ -283,7 +268,7 @@ describe("GridEngine", () => {
|
||||
|
||||
it("places reduce-only orders to close existing exposures", () => {
|
||||
const adapter = new StubAdapter();
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
||||
|
||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
|
||||
adapter.emitOrders([]);
|
||||
@@ -307,11 +292,11 @@ describe("GridEngine", () => {
|
||||
|
||||
it("restores exposures from existing reduce-only orders on restart", async () => {
|
||||
const adapter = new StubAdapter();
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
||||
|
||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize * 2));
|
||||
|
||||
const reduceOrder: Order = {
|
||||
const reduceOrder: AsterOrder = {
|
||||
orderId: "existing-reduce",
|
||||
clientOrderId: "existing-reduce",
|
||||
symbol: baseConfig.symbol,
|
||||
@@ -356,15 +341,14 @@ describe("GridEngine", () => {
|
||||
expect(reduceDesired).toBeTruthy();
|
||||
expect(reduceDesired!.amount).toBeCloseTo(baseConfig.orderSize * 2, 6);
|
||||
expect(Number(reduceDesired!.price)).toBeCloseTo(baseConfig.upperPrice, 6);
|
||||
// New engine cancels unrecognized orders (no grid- prefix) during recovery;
|
||||
// legacy syncGrid still picks up exposure from position regardless.
|
||||
expect(adapter.cancelledOrders).toHaveLength(0);
|
||||
|
||||
engine.stop();
|
||||
});
|
||||
|
||||
it("halts the grid and closes positions when stop loss triggers", async () => {
|
||||
const adapter = new StubAdapter();
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
||||
|
||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0.2));
|
||||
adapter.emitOrders([]);
|
||||
@@ -381,341 +365,10 @@ describe("GridEngine", () => {
|
||||
(engine as any).stopReason = "test stop";
|
||||
await (engine as any).haltGrid(90);
|
||||
|
||||
expect(adapter.cancelAllCount).toBeGreaterThanOrEqual(1);
|
||||
expect(adapter.cancelAllCount).toBe(1);
|
||||
expect(adapter.marketOrders).toHaveLength(1);
|
||||
expect(engine.getSnapshot().running).toBe(false);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LighterSigner } from "../../src/exchanges/lighter/signer";
|
||||
import { LIGHTER_ORDER_TYPE, LIGHTER_TIME_IN_FORCE } from "../../src/exchanges/lighter/constants";
|
||||
|
||||
describe("LighterSigner", () => {
|
||||
it("produces deterministic create order signature", async () => {
|
||||
it("produces deterministic create order signature", () => {
|
||||
const signer = new LighterSigner({
|
||||
accountIndex: 65,
|
||||
chainId: 300,
|
||||
@@ -12,7 +12,7 @@ describe("LighterSigner", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const signed = await signer.signCreateOrder({
|
||||
const signed = signer.signCreateOrder({
|
||||
marketIndex: 0,
|
||||
clientOrderIndex: 123n,
|
||||
baseAmount: 1000n,
|
||||
@@ -31,7 +31,7 @@ describe("LighterSigner", () => {
|
||||
const payload = JSON.parse(signed.txInfo);
|
||||
expect(payload.AccountIndex).toBe(65);
|
||||
expect(payload.ApiKeyIndex).toBe(3);
|
||||
expect(payload).toMatchObject({
|
||||
expect(payload.OrderInfo).toMatchObject({
|
||||
MarketIndex: 0,
|
||||
ClientOrderIndex: 123,
|
||||
BaseAmount: 1000,
|
||||
@@ -45,10 +45,7 @@ describe("LighterSigner", () => {
|
||||
});
|
||||
expect(typeof payload.Sig).toBe("string");
|
||||
expect(payload.Sig.length).toBeGreaterThan(0);
|
||||
if (signed.txHash) {
|
||||
expect(typeof signed.txHash).toBe("string");
|
||||
expect(signed.txHash.length).toBeGreaterThan(0);
|
||||
}
|
||||
expect(signed.txHash).toBe("3ef41bc5fdb2e2146b5f5df046fb1ad801dc2aa5c47703665bfd3eb67a21e67048957f7187b12035");
|
||||
expect(typeof signed.signature).toBe("string");
|
||||
expect(signed.signature.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||
import { t } from "../src/i18n";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
|
||||
@@ -11,13 +11,13 @@ class StubAdapter implements ExchangeAdapter {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
async createOrder(): Promise<Order> {
|
||||
async createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
|
||||
class StubAdapter implements ExchangeAdapter {
|
||||
id = "standx";
|
||||
accountSnapshot: AccountSnapshot | null = null;
|
||||
accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
async createOrder(): Promise<Order> {
|
||||
async createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class StubAdapter implements ExchangeAdapter {
|
||||
async cancelOrders(): Promise<void> {}
|
||||
async cancelAllOrders(): Promise<void> {}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
return this.accountSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
|
||||
class StubAdapter implements ExchangeAdapter {
|
||||
id = "standx";
|
||||
cancelAllCount = 0;
|
||||
openOrders: Order[] | Error = [];
|
||||
accountSnapshot: AccountSnapshot | null = null;
|
||||
openOrders: AsterOrder[] | Error = [];
|
||||
accountSnapshot: AsterAccountSnapshot | null = null;
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
async createOrder(): Promise<Order> {
|
||||
async createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
@@ -31,12 +31,12 @@ class StubAdapter implements ExchangeAdapter {
|
||||
this.openOrders = [];
|
||||
}
|
||||
|
||||
async queryOpenOrders(): Promise<Order[]> {
|
||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
||||
if (this.openOrders instanceof Error) throw this.openOrders;
|
||||
return this.openOrders;
|
||||
}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
return this.accountSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
|
||||
class StubAdapter implements ExchangeAdapter {
|
||||
id = "standx";
|
||||
|
||||
private depthListeners: Array<(depth: Depth) => void> = [];
|
||||
private depthListeners: Array<(depth: AsterDepth) => void> = [];
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
||||
this.depthListeners.push(cb);
|
||||
}
|
||||
|
||||
emitDepth(depth: Depth): void {
|
||||
emitDepth(depth: AsterDepth): void {
|
||||
for (const listener of this.depthListeners) {
|
||||
listener(depth);
|
||||
}
|
||||
}
|
||||
|
||||
async createOrder(): Promise<Order> {
|
||||
async createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class StubAdapter implements ExchangeAdapter {
|
||||
async cancelOrders(): Promise<void> {}
|
||||
async cancelAllOrders(): Promise<void> {}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
|
||||
class StubAdapter implements ExchangeAdapter {
|
||||
id = "standx";
|
||||
|
||||
private depthListeners: Array<(depth: Depth) => void> = [];
|
||||
private depthListeners: Array<(depth: AsterDepth) => void> = [];
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
||||
this.depthListeners.push(cb);
|
||||
}
|
||||
|
||||
emitDepth(depth: Depth): void {
|
||||
emitDepth(depth: AsterDepth): void {
|
||||
for (const listener of this.depthListeners) {
|
||||
listener(depth);
|
||||
}
|
||||
}
|
||||
|
||||
async createOrder(): Promise<Order> {
|
||||
async createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class StubAdapter implements ExchangeAdapter {
|
||||
async cancelOrders(): Promise<void> {}
|
||||
async cancelAllOrders(): Promise<void> {}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||
|
||||
class StandxStubAdapter implements ExchangeAdapter {
|
||||
@@ -12,20 +12,20 @@ class StandxStubAdapter implements ExchangeAdapter {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
||||
|
||||
async createOrder(): Promise<Order> {
|
||||
async createOrder(): Promise<AsterOrder> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
async cancelOrder(): Promise<void> {}
|
||||
async cancelOrders(): Promise<void> {}
|
||||
async cancelAllOrders(): Promise<void> {}
|
||||
|
||||
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
||||
return {
|
||||
canTrade: true,
|
||||
canDeposit: true,
|
||||
@@ -98,7 +98,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
|
||||
asks: [["101", "1"]],
|
||||
eventTime: Date.now(),
|
||||
symbol: "BTC-USD",
|
||||
} as Depth;
|
||||
} as AsterDepth;
|
||||
(engine as any).tickerSnapshot = {
|
||||
symbol: "BTC-USD",
|
||||
lastPrice: "100",
|
||||
@@ -108,7 +108,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
|
||||
volume: "0",
|
||||
quoteVolume: "0",
|
||||
eventTime: Date.now(),
|
||||
} as Ticker;
|
||||
} as AsterTicker;
|
||||
|
||||
const syncSpy = vi.fn().mockResolvedValue(undefined);
|
||||
(engine as any).syncOrders = syncSpy;
|
||||
@@ -162,7 +162,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
|
||||
asks: [["101", "1"]],
|
||||
eventTime: Date.now(),
|
||||
symbol: "BTC-USD",
|
||||
} as Depth;
|
||||
} as AsterDepth;
|
||||
(engine as any).tickerSnapshot = {
|
||||
symbol: "BTC-USD",
|
||||
lastPrice: "100",
|
||||
@@ -172,7 +172,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
|
||||
volume: "0",
|
||||
quoteVolume: "0",
|
||||
eventTime: Date.now(),
|
||||
} as Ticker;
|
||||
} as AsterTicker;
|
||||
|
||||
const syncSpy = vi.fn().mockResolvedValue(undefined);
|
||||
(engine as any).syncOrders = syncSpy;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user