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

These types are the platform-agnostic contract used by all 8 exchanges,
not Aster-specific. Renamed across 63 files.
2026-04-06 18:11:57 +08:00
discountry 230c0d5e18 Add .factory to .gitignore and ensure proper newline at end of file 2026-04-06 17:58:57 +08:00
discountry eb70bab8c5 Add Oxlint configuration and integrate linting commands
- Introduced a new `.oxlintrc.json` file to configure Oxlint for code quality checks.
- Updated `package.json` to include linting scripts (`lint` and `lint:fix`) for easier code maintenance.
- Enhanced documentation in `README` files to guide users on running Oxlint checks and applying fixes.
2026-03-12 22:40:18 +08:00
discountry 055bb445a9 fix denpendence 2026-03-01 23:47:52 +08:00
discountry 74d0a0a98b Update package.json and README files to include repository information and enhance CLI command mode documentation
- Added repository details to package.json for better project visibility.
- Revised README files in both English and Chinese to improve the clarity and accessibility of the `ritmex-bot` CLI command mode instructions.
- Included installation instructions for adding the project as a skill, enhancing user onboarding experience.
2026-02-27 12:55:28 +08:00
discountry 674a6fe0da Add CLI user guides and .npmignore file
- Introduced English and Chinese user guides for the `ritmex-bot` CLI, detailing command usage and options.
- Created a new `.npmignore` file to exclude documentation directories from npm package distribution.
- Updated README files to link to the new user guides directly, ensuring easy access for users.
2026-02-27 12:47:32 +08:00
DisneyandGitHub 4014a86223 Enhance CLI command mode for ritmex-bot (#23)
- Introduced a new command mode for `ritmex-bot`, allowing agent-friendly structured trading operations without entering the Ink interactive menu.
- Updated `package.json` to include versioning and set the project as public with a new CLI entry point.
- Enhanced documentation in both English and Chinese to provide comprehensive usage instructions for the new command mode.
- Added a new executable script for `ritmex-bot` to facilitate command execution.
- Improved error handling and command parsing for better user experience and clarity in command execution.
2026-02-27 12:42:20 +08:00
DisneyandGitHub d6399b92aa Feat/support binance (#22)
* add docs

* Add Binance exchange support

- Updated the environment configuration to include Binance as a selectable exchange option.
- Enhanced the README documentation to reflect the addition of Binance.
- Implemented the Binance exchange adapter and integrated it into the existing exchange framework.
- Modified the basis arbitrage strategy to support Binance alongside existing exchanges.
- Added tests to ensure proper functionality and integration of Binance within the trading system.

* Enhance README with detailed Binance exchange configuration

- Added comprehensive instructions for setting up Binance as an exchange option.
- Included environment variable specifications for API keys, market types, and trading symbols.
- Provided examples for both perpetual and spot trading strategies.
- Clarified the use of WebSocket and REST for the Binance adapter.

* Enhance exchange support and testing framework

- Added a new test suite for exchange contracts to ensure consistency and functionality across supported exchanges.
- Refactored exchange ID handling to utilize a centralized list of supported exchanges, improving maintainability.
- Updated CLI argument parsing and help documentation to reflect the new exchange structure.
- Introduced utility functions for validating supported exchanges and their display names.
- Enhanced the BasisApp and strategy runner to leverage the new exchange validation logic.
- Added a new test command for running exchange-related tests.

* Refactor exchange contract tests and update CLI commands

- Removed the trailing supported exchanges set and simplified the logic for trailing stop support in the exchange contract tests.
- Updated the test command for exchange contracts to exclude unnecessary tests, streamlining the testing process.
- Enhanced test descriptions for clarity and improved understanding of the functionality being tested.
2026-02-27 11:37:44 +08:00
127 changed files with 13294 additions and 4316 deletions
+8
View File
@@ -87,6 +87,14 @@ GRID_STOP_LOSS_PCT=0.01 # Stop loss trigger percentage beyond bo
GRID_RESTART_TRIGGER_PCT=0.01 # Restart buffer percentage inside bounds
GRID_AUTO_RESTART_ENABLED=true # Automatically resume grid when price re-enters range
GRID_MAX_CLOSE_SLIPPAGE_PCT=0.05 # Close-order slippage guard relative to mark price
GRID_SHIFT_ENABLED=false # Smart-follow grid: shift the whole grid when price drifts from anchor
GRID_SHIFT_TRIGGER_PCT=0.05 # Shift trigger: |price/anchor - 1| threshold (0.05 => 5%)
GRID_SHIFT_RANGE_PCT=0.05 # New grid half-range around the new anchor after a shift
GRID_SHIFT_CONFIRM_MS=3000 # Deviation must persist this long before shifting (anti-wick)
GRID_USE_REDUCE_ONLY=false # Attach reduceOnly to EXIT orders (some venues reject it alongside entries)
GRID_EXCHANGE_STOP_ENABLED=true # Keep an exchange-side STOP_MARKET backstop (aster/binance/grvt/ondoperps)
GRID_RECONCILE_INTERVAL_MS=30000 # Periodic REST reconcile cadence when the venue supports order queries
GRID_UNCOVERED_GRACE_MS=5000 # Grace before the coverage audit acts on uncovered position
# GRID_PRICE_TICK=0.1 # Optional override for grid price tick (falls back to PRICE_TICK)
# GRID_QTY_STEP=0.001 # Optional override for grid quantity step (falls back to QTY_STEP)
+3 -1
View File
@@ -35,4 +35,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.DS_Store
.tmp
.tmp/*
.tmp/*
.factory
+3
View File
@@ -0,0 +1,3 @@
docs/
.claude/
.cursor/
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [
"coverage/**",
"data/**",
"dist/**",
"docs/**",
"node_modules/**",
"out/**",
".tmp/**"
]
}
+132 -25
View File
@@ -1,35 +1,142 @@
# Repository Guidelines
## 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/`:
## Package Manager
- `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.
**必须使用 Bun** — 本项目使用 Bun 作为包管理器和运行时。所有命令都必须用 bun 执行,**不要使用 npm、yarn 或 npx**。
## 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.
## Project Structure
Strategy-specific scripts still execute through the CLI; there is no separate `legacy/` workspace.
入口 `index.ts` 在仓库根目录,用于 CLI 启动。所有生产代码在 `src/` 下:
## Coding Style & Naming Conventions
Use modern TypeScript with ES modules, two-space indentation, and sorted imports (external → internal). Favor `camelCase` for variables/functions and `PascalCase` for classes/enums. Place new strategies under `src/strategy/`, shared utilities under `src/utils/` or `src/strategy/common/` when they only apply to strategies. Keep comments focused on non-obvious trading logic.
```
src/
├── config.ts # 运行时配置(GridConfig、MakerPointsConfig 等)
├── runtime-errors.ts # 全局错误处理
├── index.tsx # Ink 渲染入口
├── cli/ # CLI 参数解析与策略启动
│ ├── args.ts
│ ├── command-executor.ts
│ ├── command-parser.ts
│ ├── command-types.ts
│ └── strategy-runner.ts
├── core/ # 订单协调 + 共享库
│ ├── order-coordinator.ts # 限价单/市价单统一下单(锁、去重、速率控制)
│ └── lib/
│ ├── order-plan.ts
│ ├── orders.ts
│ └── rate-limit.ts
├── exchanges/ # 交易所适配器(每个子目录含 adapter/gateway/order
│ ├── adapter.ts # ExchangeAdapter 接口
│ ├── adapter-utils.ts # 适配器工具函数
│ ├── types.ts # CreateOrderParams、Order、AccountSnapshot 等公共类型
│ ├── order-schema.ts # BaseOrderIntent、LimitOrderIntent 等
│ ├── order-handlers.ts # 通用 order handler 工厂
│ ├── order-router.ts # 按交易所分发订单处理
│ ├── create-adapter.ts # 工厂函数
│ ├── resolve-from-env.ts
│ ├── dry-run-adapter.ts # 模拟适配器
│ ├── aster/ # Aster 交易所
│ ├── backpack/ # Backpack 交易所
│ ├── binance/ # Binance 交易所(CCXT
│ ├── grvt/ # GRVT 交易所
│ ├── lighter/ # Lighter 交易所(含签名/nonce/字节处理)
│ ├── nado/ # Nado 交易所
│ ├── paradex/ # Paradex 交易所
│ └── standx/ # StandX 交易所
├── strategy/ # 所有策略引擎
│ ├── maker-engine.ts # Maker 做市策略
│ ├── maker-points-engine.ts # MakerPoints 积分做市策略
│ ├── maker-points-logic.ts # MakerPoints 纯逻辑(可单独测试)
│ ├── offset-maker-engine.ts # 偏移做市策略
│ ├── trend-engine.ts # 趋势跟踪策略
│ ├── grid-engine.ts # 网格交易策略(LevelState + clientOrderId 恢复)
│ ├── basis-arb-engine.ts # 基差套利策略
│ ├── swing-engine.ts # 波段交易策略
│ ├── swing-logic.ts # 波段纯逻辑
│ ├── guardian-engine.ts # 监控守护策略
│ ├── liquidity-maker-engine.ts # 流动性做市策略
│ └── common/ # 策略共享辅助
│ ├── binance-depth.ts # Binance 深度分析
│ ├── binance-rsi.ts # Binance RSI 指标
│ ├── event-emitter.ts # 策略事件发射器
│ ├── grid-storage.ts # 网格状态磁盘持久化
│ ├── session-volume.ts # 会话成交量统计
│ └── subscriptions.ts # WebSocket 安全订阅
├── ui/ # Ink 仪表盘(每个策略独立 App)
│ ├── App.tsx
│ ├── GridApp.tsx
│ ├── MakerApp.tsx
│ ├── MakerPointsApp.tsx
│ ├── OffsetMakerApp.tsx
│ ├── TrendApp.tsx
│ ├── BasisApp.tsx
│ ├── SwingApp.tsx
│ ├── GuardianApp.tsx
│ ├── LiquidityMakerApp.tsx
│ └── components/
├── logging/ # 交易日志
│ └── trade-log.ts
├── notifications/ # Telegram 通知
│ ├── index.ts
│ ├── telegram.ts
│ └── types.ts
├── i18n/ # 国际化
│ └── index.ts
└── utils/ # 跨领域工具
├── math.ts, format.ts, price.ts, depth.ts
├── errors.ts, risk.ts, pnl.ts
├── strategy.ts, order-status.ts, security.ts
├── copyright.ts
└── standx-token-expiry.ts
```
## Testing Guidelines
Vitest powers unit/integration tests. Co-locate new tests next to their subject using `<feature>.test.ts`. Strategies should have coverage for order lifecycle, risk guards, and websocket edge cases. Run `bun x vitest --watch` during development for fast feedback.
辅助目录:
- `tests/` — Vitest 测试套件(大部分测试在此,少量与逻辑共处于 `src/`
- `docs/` — 交易所 API 参考文档
- `scripts/` — 一次性脚本(密钥派生等)
- `skills/` — AI 助手技能配置
## Commit & Pull Request Guidelines
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.
## 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:`),每次提交作用域限单个模块或策略。
## Environment & Secrets
Duplicate `.env.example` to `.env` and populate API keys (Aster/GRVT) before running strategies. Do not commit secrets—use local `.env` or deployment secret managers. Rotate keys if they leak into logs or backups.
复制 `.env.example``.env` 填入 API 密钥。**不要读取、打印或提交 `.env` 和任何密钥。** 泄露立即轮换。
## Safety
- 永远不要读取、运行、打印或访问 `.env` 或任何 secrets
- 代码中遇到项目依赖时,先查版本再查文档,避免过时知识导致错误
-12
View File
@@ -1,12 +0,0 @@
# RitMEX Bot - Claude Instructions
## Package Manager
**必须使用 Bun** - 这个项目使用 Bun 作为包管理器和运行时。所有能用 bun 执行的命令都必须使用 bun:
- 安装依赖: `bun install`
- 运行脚本: `bun run <script>`
- 执行测试: `bun test`
- 类型检查: `bun run typecheck`
**不要使用 npm、yarn 或 npx**
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+78 -106
View File
@@ -11,19 +11,40 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
如果您希望获取优惠并支持本项目,请考虑使用以下注册链接:
* [Lighter 手续费优惠注册链接](https://app.lighter.xyz/?referral=111909FA)
* [Hyperliquid 邀请注册链接](https://app.hyperliquid.xyz/join/RITMEX)
* [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)
* [GRVT 手续费优惠注册链接](https://grvt.io/exchange/sign-up?ref=sea)
* [Nado 手续费优惠注册链接](https://app.nado.xyz?join=LKbIUs5)
* [Backpack 手续费优惠注册链接](https://backpack.exchange/join/ritmex)
* [edgex 手续费优惠注册链接](https://pro.edgex.exchange/referral/BULL)
* [Paradex 手续费优惠注册链接](https://paradex.io/ref/xingxingjun)
* [Apex 手续费优惠注册链接](https://join.omni.apex.exchange/SEA)
* [GRVT 手续费优惠注册链接](https://grvt.io/exchange/sign-up?ref=sea)
## CLI 命令模式(ritmex-bot
`ritmex-bot` 支持 Agent 友好的结构化命令调用,覆盖交易所能力查询、行情、账户、仓位、下单、撤单与策略启动。
- 保持现有环境变量体系,不新增也不改名,只读取当前执行环境中的变量。
- `--symbol` 原样透传,不对交易对做统一改写。
- 支持 `--dry-run` 模拟执行与 `--json` 结构化输出,便于自动化系统集成。
### 安装当前项目 Skillskills add
```bash
bunx 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 自动同步账户、挂单与仓位,断线后自动恢复。
@@ -33,15 +54,18 @@ A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend en
- **模块化架构**:策略引擎、交易所适配器与 Ink CLI 相互解耦,新增交易所或策略更容易。
## 支持的交易所
| 交易所 | 合约类型 | 必填环境变量 | 备注 |
| 交易所 | 市场类型 | 标准配置必填项 | 备注 |
| --- | --- | --- | --- |
| Aster | USDT 永续 | `ASTER_API_KEY`, `ASTER_API_SECRET` | 默认交易所;兼容脚本引导
| StandX | USD 永续 | `STANDX_TOKEN` | 使用 JWT Token 登录,优先走 WebSocket 推送
| GRVT | USDT 永续 | `GRVT_API_KEY`, `GRVT_API_SECRET`, `GRVT_SUB_ACCOUNT_ID` | `GRVT_ENV` 可切换 `prod`/`testnet`
| Lighter | zkLighter 永续 | `LIGHTER_ACCOUNT_INDEX`, `LIGHTER_API_PRIVATE_KEY` | 默认 `LIGHTER_ENV=testnet`
| 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`
| Aster | USDT 永续 | `ASTER_API_KEY`, `ASTER_API_SECRET` | 生产环境;默认交易所 |
| Binance | 现货 + USDⓈ-M 永续 | `BINANCE_API_KEY`, `BINANCE_API_SECRET` | `BINANCE_MARKET_TYPE` 选择市场 |
| StandX | USD 永续 | `STANDX_TOKEN`, `STANDX_REQUEST_PRIVATE_KEY` | JWT 认证 + Ed25519 交易签名 |
| GRVT | USDT 永续 | `GRVT_API_KEY`, `GRVT_API_SECRET`, `GRVT_SUB_ACCOUNT_ID`, `GRVT_INSTRUMENT` | `GRVT_ENV` 支持 `prod`/`testnet` |
| Lighter | 永续 + 部分现货 | `LIGHTER_ACCOUNT_INDEX`, `LIGHTER_API_KEY_INDEX`, `LIGHTER_API_PRIVATE_KEY` | 默认 `LIGHTER_ENV=testnet` |
| Backpack | 现货 + USDC 永续 | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET` | 永续应显式使用 `*_PERP` 符号 |
| Paradex | USD 永续 | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | `PARADEX_SANDBOX=true` 使用测试网 |
| Nado | USDC 永续 | `NADO_SIGNER_PRIVATE_KEY`, `NADO_SUBACCOUNT_OWNER` | `NADO_ENV` 支持 `inkMainnet`/`inkTestnet` |
| Ondo Perps | 加密资产/股票/商品永续 | `ONDOPERPS_API_KEY_ID`, `ONDOPERPS_API_SECRET` | HMAC 鉴权;支持生产与沙盒环境 |
## 系统要求
- Bun ≥ 1.2(需同时包含 `bun``bunx` 命令)
@@ -86,7 +110,7 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
| 变量 | 说明 |
| --- | --- |
| `EXCHANGE` | 选择交易所(`aster`/`binance`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado` |
| `EXCHANGE` | 选择交易所(`aster`/`binance`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado`/`ondoperps` |
| `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT` |
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
@@ -105,113 +129,59 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
> ```
## 交易所配置指南
### Aster
1. 将 `EXCHANGE` 保持为 `aster`(默认值)。
2. 填写 `ASTER_API_KEY` 与 `ASTER_API_SECRET`。
3. 根据交易对调整 `TRADE_SYMBOL`、`PRICE_TICK`、`QTY_STEP` 等精度参数。
4. 一键脚本会自动写入这些变量,手动部署时需自行维护。
### Binance
1. 设置 `EXCHANGE=binance`。
2. 填写 `BINANCE_API_KEY`、`BINANCE_API_SECRET`。
3. 设置市场模式 `BINANCE_MARKET_TYPE`
- `perp`:永续(默认)
- `spot`:现货
- `auto`:按符号自动匹配(同名现货/永续并存时建议不要使用)
4. 设置交易符号 `BINANCE_SYMBOL`(或使用 `TRADE_SYMBOL`):
- 永续建议写 `BTCUSDT_PERP`(或直接 `BTCUSDT` + `BINANCE_MARKET_TYPE=perp`
- 现货建议写 `BTCUSDT`(也支持 `BTCUSDT_SPOT`
5. 需要做期现套利(Basis)时,建议显式拆分:
- `BASIS_FUTURES_SYMBOL=BTCUSDT_PERP`
- `BASIS_SPOT_SYMBOL=BTCUSDT`
6. 可选测试网/自定义端点:
- `BINANCE_SANDBOX=true`
- `BINANCE_SPOT_REST_URL` / `BINANCE_FUTURES_REST_URL`
- `BINANCE_SPOT_WS_URL` / `BINANCE_FUTURES_WS_URL`
每个受支持交易所均提供独立的中文与英文配置教程。教程覆盖凭证获取、必填变量、网络选择、符号格式、只读验证和安全要求。
> Binance 适配器默认优先使用 WebSocket(盘口、ticker、kline、账户/订单用户流),仅在必要时使用 REST 补偿与兜底。
>
> 若使用现货模式,部分“仅合约可用”的保护单能力会受交易所限制,策略会按交易所能力自动降级。
**示例(永续做市)**
```bash
EXCHANGE=binance BINANCE_MARKET_TYPE=perp BINANCE_SYMBOL=BTCUSDT_PERP bun run index.ts --strategy maker
```
**示例(现货网格)**
```bash
EXCHANGE=binance BINANCE_MARKET_TYPE=spot BINANCE_SYMBOL=BTCUSDT bun run index.ts --strategy grid
```
### StandX
* [StandX 做市策略教程](docs/standx/maker-points-guide.md)
策略需要 StandX 的 API Token 和签名私钥才能下单。
**获取方式(使用 StandX 官方 API 生成功能):**
1. 打开 StandX 官方 API 创建页面:https://standx.com/user/session
2. 连接钱包并登录
3. 点击 **"Generate API Token"** 按钮
4. 页面会显示以下信息:
- **Token**(以 `eyJ` 开头的 JWT 字符串)→ 填入 `STANDX_TOKEN`
- **Ed25519 Private Key**Base58 格式私钥,类似 `HdsyJD7oWgT...`)→ 填入 `STANDX_REQUEST_PRIVATE_KEY`
- **创建日期** 和 **有效期天数** → 用于配置 Token 过期提醒
> Ed25519 Private Key 是系统自动生成的签名私钥,仅用于交易请求签名,你的资产仍在主钱包中,非常安全。
请妥善保存这些凭证,不要分享给他人。
**配置步骤:**
1. 设置 `EXCHANGE=standx`。
2. 填写 `STANDX_TOKEN`Perps API 的 JWT Token)。
3. 填写 `STANDX_REQUEST_PRIVATE_KEY`Ed25519 签名私钥,Base58 格式)。
4. 设置 `STANDX_SYMBOL`(默认 `BTC-USD`),并校准 `PRICE_TICK` / `QTY_STEP`。
5. 推荐配置 Token 过期时间:
- `STANDX_TOKEN_CREATE_DATE`(创建日期,格式 `YYYY-MM-DD`
- `STANDX_TOKEN_VALIDITY_DAYS`(有效期天数)
6. 可选:`STANDX_BASE_URL`、`STANDX_WS_URL`、`STANDX_SESSION_ID` 用于自定义环境。
### GRVT
1. 在 `.env` 中设置 `EXCHANGE=grvt`。
2. 填写 `GRVT_API_KEY`、`GRVT_API_SECRET`、`GRVT_SUB_ACCOUNT_ID`。
3. 若使用测试网,可将 `GRVT_ENV=testnet` 并调整 `GRVT_INSTRUMENT`/`GRVT_SYMBOL`。
4. 可选:提供 `GRVT_COOKIE` 或自定义 `GRVT_SIGNER_PATH` 以复用已有登录态。
### Lighter
1. 设置 `EXCHANGE=lighter`。
2. 填写 `LIGHTER_ACCOUNT_INDEX` 与 `LIGHTER_API_PRIVATE_KEY`40 字节十六进制私钥),其中`LIGHTER_ACCOUNT_INDEX`是你的账户索引,需要你在官网按F12观察接口请求获取,`LIGHTER_API_PRIVATE_KEY`是你的API私钥。
3. 如需切换环境,将 `LIGHTER_ENV` 改为 `mainnet`/`staging`/`dev`;必要时指定 `LIGHTER_BASE_URL`。
4. 交易对默认为 `LIGHTER_SYMBOL=BTCUSDT`,也可按需重写价格与数量小数位。
### Backpack
1. 设置 `EXCHANGE=backpack`。
2. 填写 `BACKPACK_API_KEY`、`BACKPACK_API_SECRET`、`BACKPACK_PASSWORD`;如有分账户,补充 `BACKPACK_SUBACCOUNT`,默认填写主账户ID。
3. 使用测试环境时将 `BACKPACK_SANDBOX=true`,并确认 `BACKPACK_SYMBOL` 与实际符号一致(默认 `BTC_USD_PERP`)。
4. 可通过 `BACKPACK_DEBUG=true` 观察适配器详细日志。
### Paradex
1. 设置 `EXCHANGE=paradex`。
2. 提供 `PARADEX_PRIVATE_KEY`EVM 私钥)与 `PARADEX_WALLET_ADDRESS` 注意这是你EVM钱包的地址和私钥,建议创建全新钱包,不要放置无关资产。
3. 默认连接主网,若需测试网,将 `PARADEX_SANDBOX=true` 并根据需要调整 `PARADEX_SYMBOL`。
4. 复杂环境可额外设置 `PARADEX_USE_PRO`、`PARADEX_RECONNECT_DELAY_MS` 或调试开关。
### Nado
1. 设置 `EXCHANGE=nado`。
2. 在 Nado 官网(交易界面)打开开发者工具(F12)→ 切换到 `Application` → `Local Storage`,找到 `nado.userSettings`,在其内容中取出 `privateKey` 字段并填入 `.env` 的 `NADO_SIGNER_PRIVATE_KEY`。
3. 提供 `NADO_SUBACCOUNT_OWNER`(或 `NADO_EVM_ADDRESS`)。
4. 选择网络 `NADO_ENV=inkMainnet`(主网)或 `inkTestnet`(测试网)。
5. 设置交易品种 `NADO_SYMBOL`(交易对格式类似 `BTC-PERP`;也支持输入 `BTCUSDT0`,会自动映射为 `BTC-PERP`)。
| 交易所 | 中文教程 | English Guide |
| --- | --- | --- |
| Aster | [配置教程](docs/exchanges/aster.md) | [Configuration Guide](docs/exchanges/aster.en.md) |
| Binance | [配置教程](docs/exchanges/binance.md) | [Configuration Guide](docs/exchanges/binance.en.md) |
| StandX | [配置教程](docs/exchanges/standx.md) | [Configuration Guide](docs/exchanges/standx.en.md) |
| GRVT | [配置教程](docs/exchanges/grvt.md) | [Configuration Guide](docs/exchanges/grvt.en.md) |
| Lighter | [配置教程](docs/exchanges/lighter.md) | [Configuration Guide](docs/exchanges/lighter.en.md) |
| Backpack | [配置教程](docs/exchanges/backpack.md) | [Configuration Guide](docs/exchanges/backpack.en.md) |
| Paradex | [配置教程](docs/exchanges/paradex.md) | [Configuration Guide](docs/exchanges/paradex.en.md) |
| Nado | [配置教程](docs/exchanges/nado.md) | [Configuration Guide](docs/exchanges/nado.en.md) |
| Ondo Perps | [配置教程](docs/exchanges/ondoperps.md) | [Configuration Guide](docs/exchanges/ondoperps.en.md) |
## 命令速查
```bash
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
# 全局安装
bun add -g ritmex-bot
ritmex-bot doctor
# 不安装直接运行
bunx ritmex-bot doctor
```
### 全局参数
- `--exchange`:按现有逻辑选择交易所(不修改原有环境变量体系)
- `--symbol`:原样透传,不做统一或改写
- `--dry-run`:模拟执行,不发真实下单/撤单请求
- `--json`:输出结构化 JSON,便于 AI Agent 解析
- `--timeout`:命令超时毫秒数
## 静默启动与后台运行
### 直接静默启动
无需进入 Ink 菜单,可用命令行直接拉起指定策略:
@@ -246,6 +216,8 @@ bun run pm2:start:offset
## 测试
项目使用 Vitest
```bash
bun run lint
bun run lint:fix
bun run test
bun x vitest --watch
```
+78 -106
View File
@@ -7,19 +7,40 @@ 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)
* [Hyperliquid referral link](https://app.hyperliquid.xyz/join/RITMEX)
* [Ondo Perps referral 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)
* [GRVT referral link](https://grvt.io/exchange/sign-up?ref=sea)
* [Nado referral link](https://app.nado.xyz?join=LKbIUs5)
* [Backpack referral link](https://backpack.exchange/join/ritmex)
* [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)
* [GRVT referral link](https://grvt.io/exchange/sign-up?ref=sea)
## 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
bunx 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)
- [Bilingual Exchange Configuration Guides](#exchange-setup-guides)
- [Ondo Perps Integration Guide](docs/ondoperps/README.md)
## Highlights
- **Live data & risk sync** via websockets with REST fallbacks and full reconciliation on restart.
@@ -29,15 +50,18 @@ If you'd like to support this project and get fee discounts, please consider usi
- **Modular architecture** decoupling engines, exchange adapters, and the Ink CLI for easy venue or strategy extensions.
## Supported Exchanges
| Exchange | Contract Type | Required Environment Variables | Notes |
| Exchange | Market Type | Standard Required Settings | Notes |
| --- | --- | --- | --- |
| Aster | USDT perpetuals | `ASTER_API_KEY`, `ASTER_API_SECRET` | Default venue; works with the bootstrap script |
| StandX | USD perpetuals | `STANDX_TOKEN` | Uses JWT token auth; prefer websocket streams |
| GRVT | USDT perpetuals | `GRVT_API_KEY`, `GRVT_API_SECRET`, `GRVT_SUB_ACCOUNT_ID` | Switch `GRVT_ENV` between `prod` and `testnet` |
| Lighter | zkLighter perpetuals | `LIGHTER_ACCOUNT_INDEX`, `LIGHTER_API_PRIVATE_KEY` | Defaults to `LIGHTER_ENV=testnet` |
| 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` |
| Aster | USDT perpetuals | `ASTER_API_KEY`, `ASTER_API_SECRET` | Production; default exchange |
| Binance | Spot + USDⓈ-M perpetuals | `BINANCE_API_KEY`, `BINANCE_API_SECRET` | `BINANCE_MARKET_TYPE` selects the market |
| StandX | USD perpetuals | `STANDX_TOKEN`, `STANDX_REQUEST_PRIVATE_KEY` | JWT authentication + Ed25519 trade signing |
| GRVT | USDT perpetuals | `GRVT_API_KEY`, `GRVT_API_SECRET`, `GRVT_SUB_ACCOUNT_ID`, `GRVT_INSTRUMENT` | `GRVT_ENV` supports `prod`/`testnet` |
| Lighter | Perpetuals + selected Spot markets | `LIGHTER_ACCOUNT_INDEX`, `LIGHTER_API_KEY_INDEX`, `LIGHTER_API_PRIVATE_KEY` | Defaults to `LIGHTER_ENV=testnet` |
| Backpack | Spot + USDC perpetuals | `BACKPACK_API_KEY`, `BACKPACK_API_SECRET` | Use an explicit `*_PERP` symbol for perpetuals |
| Paradex | USD perpetuals | `PARADEX_PRIVATE_KEY`, `PARADEX_WALLET_ADDRESS` | `PARADEX_SANDBOX=true` selects testnet |
| Nado | USDC perpetuals | `NADO_SIGNER_PRIVATE_KEY`, `NADO_SUBACCOUNT_OWNER` | `NADO_ENV` supports `inkMainnet`/`inkTestnet` |
| Ondo Perps | Crypto/equity/commodity perpetuals | `ONDOPERPS_API_KEY_ID`, `ONDOPERPS_API_SECRET` | HMAC authentication with production and sandbox endpoints |
## Requirements
- Bun >= 1.2 (both `bun` and `bunx` on PATH)
@@ -82,7 +106,7 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
| Variable | Purpose |
| --- | --- |
| `EXCHANGE` | Choose the venue (`aster` / `binance` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) |
| `EXCHANGE` | Choose the venue (`aster` / `binance` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado` / `ondoperps`) |
| `TRADE_SYMBOL` | Contract symbol (defaults to `BTCUSDT`) |
| `TRADE_AMOUNT` | Order size in base asset units |
| `LOSS_LIMIT` | Max per-trade loss in USDT before forced close |
@@ -102,112 +126,58 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
## Exchange Setup Guides
### Aster
1. Keep `EXCHANGE=aster` (default value).
2. Supply `ASTER_API_KEY` and `ASTER_API_SECRET`.
3. Adjust `TRADE_SYMBOL`, `PRICE_TICK`, and `QTY_STEP` to match the requested market.
4. The bootstrap script auto-populates these variables; manual installs must maintain them.
Each supported exchange has a standalone Chinese and English configuration guide covering credential creation, required variables, environment selection, symbol formats, read-only verification, and security controls.
### Binance
1. Set `EXCHANGE=binance`.
2. Provide `BINANCE_API_KEY` and `BINANCE_API_SECRET`.
3. Set market mode via `BINANCE_MARKET_TYPE`:
- `perp`: perpetual futures (default)
- `spot`: spot market
- `auto`: resolve by symbol (not recommended when both spot/perp share the same symbol)
4. Set `BINANCE_SYMBOL` (or fallback to `TRADE_SYMBOL`):
- Perp recommended: `BTCUSDT_PERP` (or `BTCUSDT` with `BINANCE_MARKET_TYPE=perp`)
- Spot recommended: `BTCUSDT` (also accepts `BTCUSDT_SPOT`)
5. For Basis arbitrage on Binance, explicitly split legs:
- `BASIS_FUTURES_SYMBOL=BTCUSDT_PERP`
- `BASIS_SPOT_SYMBOL=BTCUSDT`
6. Optional sandbox/custom endpoints:
- `BINANCE_SANDBOX=true`
- `BINANCE_SPOT_REST_URL` / `BINANCE_FUTURES_REST_URL`
- `BINANCE_SPOT_WS_URL` / `BINANCE_FUTURES_WS_URL`
> The Binance adapter is WS-first by default (depth/ticker/kline/account/order streams), with REST used only for reconciliation and fallback.
>
> In spot mode, some derivatives-only protective order capabilities are exchange-limited, and the strategy will degrade gracefully based on venue capabilities.
**Example (perp maker)**
```bash
EXCHANGE=binance BINANCE_MARKET_TYPE=perp BINANCE_SYMBOL=BTCUSDT_PERP bun run index.ts --strategy maker
```
**Example (spot grid)**
```bash
EXCHANGE=binance BINANCE_MARKET_TYPE=spot BINANCE_SYMBOL=BTCUSDT bun run index.ts --strategy grid
```
### StandX
* [StandX Maker Points Strategy Guide](docs/standx/maker-points-guide.md)
The strategy requires a StandX API Token and signing private key to place orders.
**How to obtain (using StandX's official API generation feature):**
1. Open the StandX official API creation page: https://standx.com/user/session
2. Connect your wallet and log in
3. Click the **"Generate API Token"** button
4. The page will display the following information:
- **Token** (JWT string starting with `eyJ`) → Fill in `STANDX_TOKEN`
- **Ed25519 Private Key** (Base58 format, like `HdsyJD7oWgT...`) → Fill in `STANDX_REQUEST_PRIVATE_KEY`
- **Creation date** and **Validity days** → Used to configure token expiry reminders
> The Ed25519 Private Key is an auto-generated signing key used only for trade request signatures. Your assets remain in your main wallet and are completely safe.
Please keep these credentials safe and do not share them with anyone.
**Configuration steps:**
1. Set `EXCHANGE=standx`.
2. Provide `STANDX_TOKEN` (JWT token for perps API).
3. Provide `STANDX_REQUEST_PRIVATE_KEY` (Ed25519 signing private key, Base58 format).
4. Set `STANDX_SYMBOL` (defaults to `BTC-USD`) and align `PRICE_TICK` / `QTY_STEP`.
5. Recommended: configure token expiry settings:
- `STANDX_TOKEN_CREATE_DATE` (creation date, format `YYYY-MM-DD`)
- `STANDX_TOKEN_VALIDITY_DAYS` (validity days)
6. Optional: `STANDX_BASE_URL`, `STANDX_WS_URL`, or `STANDX_SESSION_ID` for custom endpoints.
### GRVT
1. Set `EXCHANGE=grvt` inside `.env`.
2. Fill `GRVT_API_KEY`, `GRVT_API_SECRET`, and `GRVT_SUB_ACCOUNT_ID`.
3. Use `GRVT_ENV=testnet` when targeting the test environment, and align `GRVT_INSTRUMENT` / `GRVT_SYMBOL`.
4. Optional: provide `GRVT_COOKIE` or a custom `GRVT_SIGNER_PATH` when reusing an existing session.
### Lighter
1. Set `EXCHANGE=lighter`.
2. Provide `LIGHTER_ACCOUNT_INDEX` and `LIGHTER_API_PRIVATE_KEY` (40-byte hex private key). `LIGHTER_ACCOUNT_INDEX` is your account index, which you can find by opening DevTools (F12) on the official website and observing API requests. `LIGHTER_API_PRIVATE_KEY` is your API private key.
3. Switch `LIGHTER_ENV` to `mainnet`, `staging`, or `dev` when necessary; override `LIGHTER_BASE_URL` if endpoints differ.
4. `LIGHTER_SYMBOL` defaults to `BTCUSDT`; override price/size decimals when markets differ.
### Backpack
1. Set `EXCHANGE=backpack`.
2. Populate `BACKPACK_API_KEY`, `BACKPACK_API_SECRET`, and `BACKPACK_PASSWORD`; add `BACKPACK_SUBACCOUNT` if you trade from a subaccount (defaults to main account ID).
3. Toggle `BACKPACK_SANDBOX=true` for the sandbox environment and verify `BACKPACK_SYMBOL` matches the contract (defaults to `BTC_USD_PERP`).
4. Enable `BACKPACK_DEBUG=true` for verbose adapter logging.
### Paradex
1. Set `EXCHANGE=paradex`.
2. Provide `PARADEX_PRIVATE_KEY` (EVM private key) and `PARADEX_WALLET_ADDRESS`. Note: These are your EVM wallet address and private key. It is recommended to create a brand new wallet and avoid storing unrelated assets in it.
3. The adapter connects to mainnet by default; enable `PARADEX_SANDBOX=true` and adjust `PARADEX_SYMBOL` for testnet usage.
4. Advanced tuning: use `PARADEX_USE_PRO`, `PARADEX_RECONNECT_DELAY_MS`, or debug flags as needed.
### Nado
1. Set `EXCHANGE=nado`.
2. On the Nado web app (trading interface), open DevTools (F12) -> switch to the `Application` tab -> `Local Storage`, locate `nado.userSettings`, then grab the `privateKey` field from its JSON value and paste it into `.env` as `NADO_SIGNER_PRIVATE_KEY`.
3. Provide `NADO_SUBACCOUNT_OWNER` (or `NADO_EVM_ADDRESS`).
4. Select network via `NADO_ENV=inkMainnet` (mainnet) or `inkTestnet` (testnet).
5. Set `NADO_SYMBOL` using Nado product symbols like `BTC-PERP` (it also accepts `BTCUSDT0` and maps it to `BTC-PERP`).
| Exchange | English Guide | 中文教程 |
| --- | --- | --- |
| Aster | [Configuration Guide](docs/exchanges/aster.en.md) | [配置教程](docs/exchanges/aster.md) |
| Binance | [Configuration Guide](docs/exchanges/binance.en.md) | [配置教程](docs/exchanges/binance.md) |
| StandX | [Configuration Guide](docs/exchanges/standx.en.md) | [配置教程](docs/exchanges/standx.md) |
| GRVT | [Configuration Guide](docs/exchanges/grvt.en.md) | [配置教程](docs/exchanges/grvt.md) |
| Lighter | [Configuration Guide](docs/exchanges/lighter.en.md) | [配置教程](docs/exchanges/lighter.md) |
| Backpack | [Configuration Guide](docs/exchanges/backpack.en.md) | [配置教程](docs/exchanges/backpack.md) |
| Paradex | [Configuration Guide](docs/exchanges/paradex.en.md) | [配置教程](docs/exchanges/paradex.md) |
| Nado | [Configuration Guide](docs/exchanges/nado.en.md) | [配置教程](docs/exchanges/nado.md) |
| Ondo Perps | [Configuration Guide](docs/exchanges/ondoperps.en.md) | [配置教程](docs/exchanges/ondoperps.md) |
## Command Cheatsheet
```bash
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
bun add -g ritmex-bot
ritmex-bot doctor
# No install
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:
@@ -242,6 +212,8 @@ 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
```
Executable
+19
View File
@@ -0,0 +1,19 @@
#!/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);
+106 -105
View File
@@ -5,26 +5,27 @@
"": {
"name": "ritmex-bot",
"dependencies": {
"@grvt/client": "^1.6.4",
"@nadohq/client": "^0.1.0-alpha.41",
"@grvt/client": "^1.6.27",
"@nadohq/client": "^0.1.0-alpha.51",
"@noble/ed25519": "^3.0.0",
"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",
"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",
"trading-signals": "^7.4.3",
"viem": "^2.43.1",
"ws": "^8.18.3",
"viem": "^2.46.3",
"ws": "^8.19.0",
},
"devDependencies": {
"@types/bun": "latest",
"vitest": "^3.2.4",
"@types/bun": "^1.3.9",
"oxlint": "^1.54.0",
"vitest": "^4.0.18",
},
"peerDependencies": {
"typescript": "^5",
"typescript": "^5.9.3",
},
},
},
@@ -85,27 +86,65 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
"@grvt/client": ["@grvt/client@1.6.25", "", { "dependencies": { "axios": "^1.13.2" } }, "sha512-LG3oZSJDq1Qz6mLiHDmg+v/Hp+Nd+yOp8GIvkBVyIdOcPVzuzC8UQD7IxvS+g5EfQQKLiKzXSjKLc39a11Uj1A=="],
"@grvt/client": ["@grvt/client@1.6.27", "", { "dependencies": { "axios": "^1.13.5" } }, "sha512-YUb7bsPPRNhZs/T+RGDBp/AKbegRHvjB8KXpTEAy5Dbf44RI00KoX9S/R12HzxLtS5rSblmcpD7hmCtDnlcN5Q=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@nadohq/client": ["@nadohq/client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/indexer-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "@nadohq/trigger-client": "^0.1.0-alpha.45", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-63S7eT5xSk3hxEXRLGDI94mPeu/zd1Nu4oUOZrY8wzhaT7Gl7PVP2snb8JH/PduVQzgs7+qblr4aTBI/j6QvLQ=="],
"@nadohq/client": ["@nadohq/client@0.1.0-alpha.51", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.51", "@nadohq/indexer-client": "0.1.0-alpha.51", "@nadohq/shared": "0.1.0-alpha.51", "@nadohq/trigger-client": "0.1.0-alpha.51", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-t94DGAbnGPjHu517NlYixtzhsBmavv+Hu4OgIKy4WEbOwSWT0NZJRLVqhzfmsZQ1fzg/z0PGss3Z59v9fVjfgg=="],
"@nadohq/engine-client": ["@nadohq/engine-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-GqaRrsB0Z2EgNY6wu4nG5OTc5hF+buNRMc0+jPLlOQonlPJAC4OiMCgCigm4nJecYwsdiQD05W3Z4YUqggxbyw=="],
"@nadohq/engine-client": ["@nadohq/engine-client@0.1.0-alpha.51", "", { "dependencies": { "@nadohq/shared": "0.1.0-alpha.51", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-gP0IHFOWq+IXMOkURVXA+bb9yh4qc+rCXy210mwmETAVT7h0kvcIij+fU6/k9nMl2G4EaGRzfocKpn3OTb6Hlw=="],
"@nadohq/indexer-client": ["@nadohq/indexer-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-8PzG9taYh380lsKT8i4UvGfhZiPpt+zPisx+ErVqMifyhcJAhgMeYNcfijwMQFNJdgTaBvtI0R5JK98l4Y0YQw=="],
"@nadohq/indexer-client": ["@nadohq/indexer-client@0.1.0-alpha.51", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.51", "@nadohq/shared": "0.1.0-alpha.51", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-4ZJTwUOxKpYKi8TGnMRscuX3tkzzOJR7Uaqs4Se+BrE48POinaMd8SnJWu7KEvWNc6JnVwb/p5LWHLEsPWtC7A=="],
"@nadohq/shared": ["@nadohq/shared@0.1.0-alpha.45", "", { "peerDependencies": { "bignumber.js": "^9.3.0", "viem": "*" } }, "sha512-4Ml8dO9mkwnIidflpunwVMLX2KJA79b53T9xTBkH1Yzy0iOnbEE27r25wvgJgyszU0aznbPBv2++FZ0d0B5Zhg=="],
"@nadohq/shared": ["@nadohq/shared@0.1.0-alpha.51", "", { "dependencies": { "abitype": "^1.2.3" }, "peerDependencies": { "bignumber.js": "^9.3.0", "viem": "*" } }, "sha512-889WGX2WcSgYpXfLx1TvStzyo/kkx19pcgn0ZebBcUHnYM+xqXJH+ty+BuFaheP+IkPfWfuS9S3Q757j04R4rg=="],
"@nadohq/trigger-client": ["@nadohq/trigger-client@0.1.0-alpha.45", "", { "dependencies": { "@nadohq/engine-client": "^0.1.0-alpha.45", "@nadohq/shared": "^0.1.0-alpha.45", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-Xt8W9zxOZhMvoXSRnx9kUR26wWuHc3EgTVTBbUXt7SlI9v3IP9I3idS6SU3j2f08flcvNqRmC2EbXyYDC2XeyQ=="],
"@nadohq/trigger-client": ["@nadohq/trigger-client@0.1.0-alpha.51", "", { "dependencies": { "@nadohq/engine-client": "0.1.0-alpha.51", "@nadohq/shared": "0.1.0-alpha.51", "axios": "*", "ts-mixer": "*" }, "peerDependencies": { "viem": "*" } }, "sha512-OmM6opQy6E2lvZqP4IDf/BF1r0KogJIEZezQ8zRJROtuKYxHnSOcItnlS1vihxp9FR7Y25l/tnQ1JxoWmZnwSw=="],
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="],
"@noble/curves": ["@noble/curves@1.9.0", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg=="],
"@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="],
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-khWlVXUa4CPvp4eXnj7/TUNeyiarwvTEmZggylPIPUCSWgPqUBFGElIBa9xKNCQt4pb+WSGArCNEAy22ekQQxg=="],
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-p3qEVSDVmyducpI9ORTJNbaMyXfICidDXGaf3WwyDyiXPExyZdfc6UsPepGPxImlfFJs5kxOCPqP4ut12Ed9pg=="],
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tUQ+vn/AL2P2Rz6cl3gsFA+4pMBFMRR0e6AVePoezV9iGTSMZUXIf0YWHq203bwNDXcY04vSXDqPB6E1b9WULA=="],
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-9tzH/OLGZCzVGEXW0D3I0OrQOOWciHYWV9mcxjqqQE6f1DMOXT23mWj5/OQtqhW3E1VfHf0uf2MzMbM2gboPlA=="],
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XelGa+x7vC9epF5JzXWHaQtdxqcL5R+H2WcEkr+JDkfmY3dAXVLa+51qW3+MWt7CZanakaOyhraLzdM32pWFfg=="],
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t5FubAU899vMF+6rBtxDewkwSAeFn8OUCKb/+jJ1XqPU0FTR+RffnTfXafVE8WXw39eN4pzUK3rkxUWlcsFuaw=="],
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Du6cwkoMRi9DAgITzQpzS1QaEqE+5BP3Tfgr1yPy3C034n6IHISCwf4KMB+wojg8FE/kYvrJDibT0ENdcR3jUg=="],
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4PjJ1lfghsVBuAegW79jkvUWbN8P4F28KaKbM72dW6ZWTYMg6M0pkTilXFZ+q/mb8f0MEyR1rO16useoa6KTA=="],
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WBNFe1foIFg6ipgzjzJfDLn7C5nZpLr/UHlnlT7GI3scCD2xpJiJTGMTTpJqA8p0Q1l2NsEnAOj9PtreF0Tkzg=="],
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-M+UAW76rZHHiYKKy4e7Te5MNikANiFhYWl0qYF1MTIhQczYIqWVDQ+SX0SzW8ipOB/oK3+enOvlvJuhMoA968Q=="],
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-sGassljr8TR5F1WCqOaacrp3mYi107MnjkqlSXOMHACps7U2bL4v7M3RY7P7NMcGkNhzRHF3VO5+XyPWhTVM6w=="],
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-mW5Z6XTO8QWtlUjlf6yjntarjnbrmGVSK/V6XYy2rjH8xUfv9pn9G1vO92DBBBi0eUwnVpcOY3hMkP851WZNWg=="],
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-nfPmEGW9BfWEUD0PVXGaYa2RS4wVblofNih1gsyUoLSWSyliNisIWd7F+QXrDSL5SJ78BPZjDW6FainV8qRiTg=="],
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gybxMQx4NN1T+pa8TLwgVS4u9H9Hxwm7Sl0qvnQJF2WRhNN1oTOCzkmmCBbW/29DYLeV99WU76zh7srCamK9yw=="],
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EAXMh2w3pzSj/aiB67kXzcHIoKxAywC1i1IgxA1sLY7iffEaZowDTOJgirY7WxeR/WiiKwak89gPeh9wUdLnIw=="],
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Ih7CfITkbw86+LjgV5gDwNmNQlvz7X+51gdeLLUDwuA/9lojDxCmRQEzeMl44KStQlwzCuNIcgGMae0MllV5ww=="],
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qGQ8KIEfdgwwJd09Nc0d5hzlHQAM0EMoshMzHG/nNlMAHXrY7RFyat6VbzjflifAFb7LPAw+Yw9AB8cfzR7L8g=="],
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-GXos7N5oZIcIE8hlXgtUkI+y3frIqwiztx9p4+ZrpNgASbnIsxxy8bgLJqXE2SGCAdmtjBO1mdKhGTtVrb1jbg=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DVxSELcI72lmcODuVpO8uXkwuYPiA0hKMhBwNa8fO4+sIVxFvxugf/z6qrL+4PM8RjGWjdl8O/QqzuRWj9b5bA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="],
@@ -151,72 +190,64 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="],
"@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="],
"@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="],
"@scure/bip32": ["@scure/bip32@1.4.0", "", { "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg=="],
"@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="],
"@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="],
"@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
"@types/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@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
"@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
"@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
"@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
"ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-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.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="],
"axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bignumber.js": ["bignumber.js@10.0.2", "", {}, "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"ccxt": ["ccxt@4.5.35", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-aFn1jq/vR9wD3Vxu/2UFoS8snqlvfYn/iYrNhyZZfzB3N+kAHhP5+sAgO6ZwNHkHiUT9IPPahC1ZSFdPtjpIYA=="],
"ccxt": ["ccxt@4.5.40", "", { "dependencies": { "ws": "^8.8.1" } }, "sha512-8nJhhNKft7+fELDNQuCV2AQZBxLJfYQaD2LpqxHrucSszosyVl0u6lCIFfcnDUbeUW925uhrsKevHf2fNbLjKw=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"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=="],
@@ -229,13 +260,9 @@
"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.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -261,7 +288,7 @@
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="],
"ethereum-cryptography": ["ethereum-cryptography@3.2.0", "", { "dependencies": { "@noble/ciphers": "1.3.0", "@noble/curves": "1.9.0", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0" } }, "sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg=="],
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
@@ -271,7 +298,7 @@
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -293,7 +320,7 @@
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"ink": ["ink@6.6.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ=="],
"ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
@@ -301,11 +328,7 @@
"isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="],
"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=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -315,20 +338,20 @@
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"ox": ["ox@0.11.3", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw=="],
"ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="],
"oxlint": ["oxlint@1.54.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.54.0", "@oxlint/binding-android-arm64": "1.54.0", "@oxlint/binding-darwin-arm64": "1.54.0", "@oxlint/binding-darwin-x64": "1.54.0", "@oxlint/binding-freebsd-x64": "1.54.0", "@oxlint/binding-linux-arm-gnueabihf": "1.54.0", "@oxlint/binding-linux-arm-musleabihf": "1.54.0", "@oxlint/binding-linux-arm64-gnu": "1.54.0", "@oxlint/binding-linux-arm64-musl": "1.54.0", "@oxlint/binding-linux-ppc64-gnu": "1.54.0", "@oxlint/binding-linux-riscv64-gnu": "1.54.0", "@oxlint/binding-linux-riscv64-musl": "1.54.0", "@oxlint/binding-linux-s390x-gnu": "1.54.0", "@oxlint/binding-linux-x64-gnu": "1.54.0", "@oxlint/binding-linux-x64-musl": "1.54.0", "@oxlint/binding-openharmony-arm64": "1.54.0", "@oxlint/binding-win32-arm64-msvc": "1.54.0", "@oxlint/binding-win32-ia32-msvc": "1.54.0", "@oxlint/binding-win32-x64-msvc": "1.54.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-ObSjVwf0ZYA5U5Cmelj0PsCuqCJXsm2TxZ40tgUSAY7Wu0lKAsNjor6cgXHXSys8jOwv1ICjtzouoWHdKGHbZg=="],
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
"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=="],
@@ -351,7 +374,7 @@
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -359,47 +382,43 @@
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"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=="],
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
"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@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
"viem": ["viem@2.45.1", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.11.3", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA=="],
"viem": ["viem@2.46.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.12.4", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg=="],
"vite": ["vite@7.1.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA=="],
"vite-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=="],
"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=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
@@ -407,34 +426,16 @@
"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
View File
@@ -0,0 +1,389 @@
# 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
View File
@@ -0,0 +1,388 @@
# 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` 命令执行路径
如果你已在使用现有环境变量与策略配置,可直接增量接入命令模式,无需迁移变量名。
+80
View File
@@ -0,0 +1,80 @@
# Aster Configuration Guide
中文版:[Aster 配置教程](aster.md)
## Scope
This guide configures the ritmex-bot Aster USDT perpetual adapter. The current adapter connects to the production endpoint at `https://fapi.asterdex.com`. Market symbols use compact uppercase values such as `BTCUSDT`.
## 1. Prepare the account and credentials
1. Connect a wallet, activate perpetual trading, and fund the account on [Aster](https://www.asterdex.com/en/referral/4665f3).
2. Create an API key from Aster's API management page and save the API key and API secret shown there.
3. Enable read and perpetual-trading permissions. Keep withdrawal permission disabled.
4. Add the bot server's fixed IP address to the key whitelist.
5. Select one-way position mode and configure leverage in the Aster interface.
Aster also publishes a [programmatic API-key registration flow](https://github.com/asterdex/api-docs/blob/master/demo/aster-api-key-registration.md) with trading scopes, expiry, and IP whitelist settings.
## 2. Minimal configuration
Add these values to the project-root `.env` file:
```dotenv
EXCHANGE=aster
ASTER_API_KEY=<your_aster_api_key>
ASTER_API_SECRET=<your_aster_api_secret>
ASTER_SYMBOL=BTCUSDT
```
`ASTER_SYMBOL` takes priority over the shared `TRADE_SYMBOL`. The default is `BTCUSDT`.
## 3. Precision and strategy values
Aster returns market precision metadata. Keep the strategy values aligned with the selected market:
```dotenv
PRICE_TICK=0.1
QTY_STEP=0.001
```
Confirm the price increment, quantity increment, and minimum notional in Aster's market rules. Configure `TRADE_AMOUNT`, stop-loss values, and maker settings after a small-account validation run.
## 4. Environment behavior
The current Aster adapter uses production REST and WebSocket endpoints. It exposes no environment switch or custom endpoint variables. Aster testnet credentials cannot authenticate against these production endpoints. Use a dedicated low-balance account and ritmex-bot `--dry-run` write simulation for strategy checks.
## 5. Verify the configuration
Run the local configuration check:
```bash
bun run index.ts doctor --exchange aster --symbol BTCUSDT --json
```
Then run a read-only market connection check:
```bash
bun run index.ts market ticker --exchange aster --symbol BTCUSDT --json
```
These commands create no orders. Run an order command with `--dry-run` before starting a live strategy.
## Troubleshooting
- `Missing ASTER_API_KEY`: fill both Aster credential variables.
- `Invalid signature`: synchronize system time and verify the secret, permissions, and IP whitelist.
- `Symbol not found`: use the native Aster perpetual symbol, such as `BTCUSDT`.
- Precision errors: update `PRICE_TICK`, `QTY_STEP`, and the order quantity from the market rules.
## Security
- Grant read and trading scopes only.
- Keep the API secret in the local runtime environment.
- Delete and replace a credential immediately after exposure.
## References
- [Aster Futures API](https://github.com/asterdex/api-docs)
- [Repository Aster API reference](../aster/v2-api.md)
+80
View File
@@ -0,0 +1,80 @@
# Aster 配置教程
English version: [Aster Configuration Guide](aster.en.md)
## 适用范围
本教程用于配置 ritmex-bot 的 Aster USDT 永续适配器。当前适配器连接 Aster 生产环境的 `https://fapi.asterdex.com`,交易对使用 `BTCUSDT` 这类连续大写格式。
## 1. 准备账户与 API 凭证
1. 在 [Aster](https://www.asterdex.com/zh-CN/referral/4665f3) 完成钱包连接、永续账户开通和入金。
2. 在 Aster API 管理页面创建 API Key,保存页面显示的 API Key 与 API Secret。
3. 开启读取和永续交易权限,关闭提现权限。
4. 为运行机器配置固定 IP 白名单。
5. 将账户持仓模式设置为单向持仓,并在交易所界面设置所需杠杆。
Aster 官方还提供[程序化 API Key 注册说明](https://github.com/asterdex/api-docs/blob/master/demo/aster-api-key-registration.md)。该流程支持交易权限、到期时间和 IP 白名单配置。
## 2. 最小配置
在项目根目录的 `.env` 中填写:
```dotenv
EXCHANGE=aster
ASTER_API_KEY=<your_aster_api_key>
ASTER_API_SECRET=<your_aster_api_secret>
ASTER_SYMBOL=BTCUSDT
```
`ASTER_SYMBOL` 优先于通用的 `TRADE_SYMBOL`。未设置时默认使用 `BTCUSDT`
## 3. 精度与策略参数
Aster 会返回市场精度,策略配置仍需与目标市场保持一致:
```dotenv
PRICE_TICK=0.1
QTY_STEP=0.001
```
通过 Aster 市场规则确认价格步长、数量步长和最小名义价值。`TRADE_AMOUNT`、止损和做市参数属于策略配置,应在小额验证后设置。
## 4. 环境说明
当前 Aster 适配器没有环境切换变量,也没有自定义 REST/WebSocket 端点变量。Aster 测试网凭证无法用于当前生产端点。测试策略时使用专用小额账户和 ritmex-bot 的 `--dry-run` 写操作模拟。
## 5. 验证配置
先执行本地配置检查:
```bash
bun run index.ts doctor --exchange aster --symbol BTCUSDT --json
```
再执行只读行情连接检查:
```bash
bun run index.ts market ticker --exchange aster --symbol BTCUSDT --json
```
以上命令不会创建订单。启动真实策略前,先用命令模式执行带 `--dry-run` 的订单路由检查。
## 常见问题
- `Missing ASTER_API_KEY`:确认两个 Aster 凭证变量均已填写。
- `Invalid signature`:同步系统时间,检查 API Secret、权限和 IP 白名单。
- `Symbol not found`:使用 Aster 永续市场的原始符号,例如 `BTCUSDT`
- 精度错误:从市场规则更新 `PRICE_TICK``QTY_STEP` 和下单数量。
## 安全要求
- API Key 仅授予读取和交易权限。
- API Secret 只保存在本机运行环境中。
- 凭证泄露后立即删除旧 Key 并创建新 Key。
## 参考资料
- [Aster Futures API](https://github.com/asterdex/api-docs)
- [仓库内 Aster API 参考](../aster/v2-api.md)
+73
View File
@@ -0,0 +1,73 @@
# Backpack Configuration Guide
中文版:[Backpack 配置教程](backpack.md)
## Scope
This guide configures the Backpack Spot and USDC perpetual adapter. Backpack API authentication uses an Ed25519 keypair: the API key is the verifying public key, and the API secret is the signing private key.
## 1. Create an API key
1. Complete account activation, trading access, and funding on [Backpack Exchange](https://backpack.exchange/join/ritmex).
2. Create a dedicated trading key from the API Keys section in account settings.
3. Save the API key and API secret immediately after creation.
4. Enable read and trading access. Keep withdrawal access disabled.
5. Configure the IP whitelist and target subaccount.
## 2. Minimal perpetual configuration
```dotenv
EXCHANGE=backpack
BACKPACK_API_KEY=<your_backpack_api_key>
BACKPACK_API_SECRET=<your_backpack_api_secret>
BACKPACK_SYMBOL=BTC_USDC_PERP
```
`BTC_USDC_PERP` is Backpack's native perpetual market ID. The adapter also matches the CCXT unified symbol `BTC/USDC:USDC`. Set the `_PERP` symbol explicitly so the matching Spot market is not selected.
## 3. Spot configuration
```dotenv
BACKPACK_SYMBOL=BTC_USDC
```
Spot mode also accepts the CCXT unified symbol `BTC/USDC`.
## 4. Optional settings
| Variable | Default | Purpose |
| --- | --- | --- |
| `BACKPACK_PASSWORD` | Empty | Set only when the credential flow supplies a passphrase |
| `BACKPACK_SUBACCOUNT` | Main account | Target subaccount ID |
| `BACKPACK_WS_WINDOW` | `5000` | Signed-request validity window in milliseconds; official maximum is 60000 |
| `BACKPACK_DEBUG` | `false` | Market-resolution and WebSocket debug output |
| `BACKPACK_SANDBOX` | `false` | Sandbox flag forwarded to CCXT |
The installed CCXT Backpack adapter exposes production API URLs only, and this repository has no Backpack endpoint override variables. `BACKPACK_SANDBOX=true` does not create a documented isolated test environment. Use `--dry-run` for write-operation simulation.
## 5. Verify the configuration
```bash
bun run index.ts doctor --exchange backpack --symbol BTC_USDC_PERP --json
bun run index.ts market ticker --exchange backpack --symbol BTC_USDC_PERP --json
```
These commands create no orders. The ticker check loads Backpack markets and confirms symbol resolution.
## Troubleshooting
- `BACKPACK_API_KEY and BACKPACK_API_SECRET ... required`: provide the complete Ed25519 credential pair.
- `Symbol ... not found in Backpack markets`: use `BTC_USDC_PERP` for perpetuals or `BTC_USDC` for Spot.
- Signature failures: verify that the API key and secret form one pair and synchronize the host clock.
- Empty subaccount balance: confirm that `BACKPACK_SUBACCOUNT` matches the account authorized for the key.
## Security
- The API secret is an Ed25519 private key. Protect it as a trading private key.
- Grant read and trading scopes only.
- Delete the key and create a new pair immediately after exposure.
## References
- [Backpack Exchange API](https://docs.backpack.exchange/)
- [Repository Backpack OpenAPI](../backpack/openapi.json)
+73
View File
@@ -0,0 +1,73 @@
# Backpack 配置教程
English version: [Backpack Configuration Guide](backpack.en.md)
## 适用范围
本教程用于 Backpack 现货和 USDC 永续适配器。Backpack API 使用 Ed25519 密钥对:API Key 是验证公钥,API Secret 是签名私钥。
## 1. 创建 API Key
1. 在 [Backpack Exchange](https://backpack.exchange/join/ritmex) 完成账户、交易权限和入金设置。
2. 在账户设置的 API Keys 页面创建专用交易 Key。
3. 创建后立即保存 API Key 和 API Secret。
4. 开启读取和交易权限,关闭提现权限。
5. 配置 IP 白名单和目标子账户。
## 2. 永续最小配置
```dotenv
EXCHANGE=backpack
BACKPACK_API_KEY=<your_backpack_api_key>
BACKPACK_API_SECRET=<your_backpack_api_secret>
BACKPACK_SYMBOL=BTC_USDC_PERP
```
`BTC_USDC_PERP` 是 Backpack 原始永续 market ID。适配器也能匹配 CCXT 统一符号 `BTC/USDC:USDC`。应显式设置 `_PERP` 符号,避免同名现货市场被选中。
## 3. 现货配置
```dotenv
BACKPACK_SYMBOL=BTC_USDC
```
现货也可使用 CCXT 统一符号 `BTC/USDC`
## 4. 可选配置
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `BACKPACK_PASSWORD` | 空 | 仅在凭证流程提供 passphrase 时填写 |
| `BACKPACK_SUBACCOUNT` | 主账户 | 目标子账户 ID |
| `BACKPACK_WS_WINDOW` | `5000` | 签名请求有效窗口,单位毫秒,官方最大值为 60000 |
| `BACKPACK_DEBUG` | `false` | 输出市场解析和 WebSocket 调试日志 |
| `BACKPACK_SANDBOX` | `false` | 传递给 CCXT 的 sandbox 标志 |
当前安装的 CCXT Backpack 适配器只公开生产 API 地址,仓库也没有 Backpack 自定义端点变量。`BACKPACK_SANDBOX=true` 不构成独立的已文档化测试环境。测试写操作时使用 `--dry-run`
## 5. 验证配置
```bash
bun run index.ts doctor --exchange backpack --symbol BTC_USDC_PERP --json
bun run index.ts market ticker --exchange backpack --symbol BTC_USDC_PERP --json
```
以上命令不会创建订单。行情检查会加载 Backpack 市场列表并确认符号解析。
## 常见问题
- `BACKPACK_API_KEY and BACKPACK_API_SECRET ... required`:填写完整 Ed25519 凭证对。
- `Symbol ... not found in Backpack markets`:永续使用 `BTC_USDC_PERP`,现货使用 `BTC_USDC`
- 签名失败:检查 API Key/Secret 是否属于同一密钥对,并同步系统时间。
- 子账户余额为空:确认 `BACKPACK_SUBACCOUNT` 与创建 Key 时授权的账户一致。
## 安全要求
- API Secret 是 Ed25519 私钥,应按交易私钥保护。
- Key 仅授予读取和交易权限。
- Secret 泄露后立即删除 Key 并创建新密钥对。
## 参考资料
- [Backpack Exchange API](https://docs.backpack.exchange/)
- [仓库内 Backpack OpenAPI](../backpack/openapi.json)
+105
View File
@@ -0,0 +1,105 @@
# Binance Configuration Guide
中文版:[Binance 配置教程](binance.md)
## Scope
The ritmex-bot Binance adapter supports Spot and USDⓈ-M perpetual markets. `BINANCE_MARKET_TYPE` selects the market class, and symbol suffixes can force Spot or perpetual resolution.
## 1. Create an API key
1. Create a dedicated key in [Binance API Management](https://www.binance.com/en/my/settings/api-management).
2. Enable read access.
3. Enable Futures permission for perpetual strategies or Spot Trading permission for Spot strategies.
4. Keep withdrawal permission disabled and add a fixed IP whitelist.
5. Use one-way position mode for USDⓈ-M Futures and configure leverage in the Binance interface.
Mainnet and testnet keys are separate. Follow the [official Spot Testnet guide](https://developers.binance.com/docs/binance-spot-api-docs/testnet) to create Spot credentials. Use Binance Futures Testnet credentials for USDⓈ-M testnet trading.
## 2. Minimal perpetual configuration
```dotenv
EXCHANGE=binance
BINANCE_API_KEY=<your_binance_api_key>
BINANCE_API_SECRET=<your_binance_api_secret>
BINANCE_MARKET_TYPE=perp
BINANCE_SYMBOL=BTCUSDT_PERP
```
`BTCUSDT_PERP` forces perpetual resolution. `BTCUSDT` with `BINANCE_MARKET_TYPE=perp` also selects the perpetual market.
## 3. Minimal Spot configuration
```dotenv
EXCHANGE=binance
BINANCE_API_KEY=<your_binance_api_key>
BINANCE_API_SECRET=<your_binance_api_secret>
BINANCE_MARKET_TYPE=spot
BINANCE_SYMBOL=BTCUSDT_SPOT
```
Spot mode also accepts `BINANCE_SYMBOL=BTCUSDT`. Spot markets have no perpetual positions, funding rates, or derivatives-only protective-order features.
## 4. Market modes and symbols
| Setting | Meaning |
| --- | --- |
| `BINANCE_MARKET_TYPE=perp` | Default mode; prefers USDⓈ-M perpetuals |
| `BINANCE_MARKET_TYPE=spot` | Spot mode |
| `BINANCE_MARKET_TYPE=auto` | Resolves by symbol and applies default selection to duplicate names |
| `BTCUSDT_PERP` | Forces the perpetual market |
| `BTCUSDT_SPOT` | Forces the Spot market |
Use explicit legs for basis arbitrage:
```dotenv
BASIS_FUTURES_SYMBOL=BTCUSDT_PERP
BASIS_SPOT_SYMBOL=BTCUSDT_SPOT
```
## 5. Testnet configuration
`BINANCE_SANDBOX=true` switches the CCXT REST clients. The adapter manages native WebSocket URLs separately, so a complete testnet setup defines both REST and WebSocket endpoints:
```dotenv
BINANCE_SANDBOX=true
BINANCE_SPOT_REST_URL=https://testnet.binance.vision
BINANCE_SPOT_WS_URL=wss://stream.testnet.binance.vision/ws
BINANCE_FUTURES_REST_URL=https://testnet.binancefuture.com
BINANCE_FUTURES_WS_URL=wss://fstream.binancefuture.com
```
Use API keys generated by the matching testnet.
## 6. Optional settings
| Variable | Default | Purpose |
| --- | --- | --- |
| `BINANCE_ACCOUNT_POLL_MS` | `5000` | Account REST reconciliation interval; minimum 1000 ms |
| `BINANCE_ORDERS_POLL_MS` | `3000` | Order REST reconciliation interval; minimum 1000 ms |
| `BINANCE_SPOT_REST_URL` | `https://api.binance.com` | Spot REST base URL |
| `BINANCE_FUTURES_REST_URL` | `https://fapi.binance.com` | Perpetual REST base URL |
| `BINANCE_SPOT_WS_URL` | `wss://stream.binance.com:9443/ws` | Spot WebSocket base URL |
| `BINANCE_FUTURES_WS_URL` | `wss://fstream.binance.com/ws` | Perpetual WebSocket base URL |
## 7. Verify the configuration
```bash
bun run index.ts doctor --exchange binance --symbol BTCUSDT_PERP --json
bun run index.ts market ticker --exchange binance --symbol BTCUSDT_PERP --json
```
These commands create no orders. Run an order command with `--dry-run` before starting a live strategy.
## Troubleshooting
- `Invalid API-key, IP, or permissions`: verify market permissions, IP restrictions, and the mainnet/testnet key source.
- `Binance symbol not found`: use `BTCUSDT_PERP` or `BTCUSDT_SPOT` to select the market explicitly.
- `Position side does not match`: switch the USDⓈ-M Futures account to one-way mode.
- Timestamp errors: synchronize the host clock.
## References
- [Binance Spot API](https://developers.binance.com/docs/binance-spot-api-docs)
- [Binance USDⓈ-M Futures API](https://developers.binance.com/docs/derivatives/usds-margined-futures)
- [Repository Binance API reference](../binance/binance-spot/README.md)
+105
View File
@@ -0,0 +1,105 @@
# Binance 配置教程
English version: [Binance Configuration Guide](binance.en.md)
## 适用范围
ritmex-bot 的 Binance 适配器支持现货和 USDⓈ-M 永续。`BINANCE_MARKET_TYPE` 决定市场类型,交易对后缀可以显式指定现货或永续。
## 1. 创建 API Key
1. 在 [Binance API Management](https://www.binance.com/en/my/settings/api-management) 创建专用 API Key。
2. 开启读取权限。
3. 永续策略开启 Futures 权限;现货策略开启 Spot Trading 权限。
4. 关闭提现权限并配置固定 IP 白名单。
5. 永续账户使用单向持仓模式,并在交易所界面设置杠杆。
主网 Key 与测试网 Key 相互独立。Spot Testnet 按[官方测试网指南](https://developers.binance.com/docs/binance-spot-api-docs/testnet)创建凭证;USDⓈ-M Futures Testnet 使用 Binance Futures Testnet 凭证。
## 2. 永续最小配置
```dotenv
EXCHANGE=binance
BINANCE_API_KEY=<your_binance_api_key>
BINANCE_API_SECRET=<your_binance_api_secret>
BINANCE_MARKET_TYPE=perp
BINANCE_SYMBOL=BTCUSDT_PERP
```
`BTCUSDT_PERP` 强制选择永续市场。`BTCUSDT` 配合 `BINANCE_MARKET_TYPE=perp` 也会选择永续。
## 3. 现货最小配置
```dotenv
EXCHANGE=binance
BINANCE_API_KEY=<your_binance_api_key>
BINANCE_API_SECRET=<your_binance_api_secret>
BINANCE_MARKET_TYPE=spot
BINANCE_SYMBOL=BTCUSDT_SPOT
```
现货也可使用 `BINANCE_SYMBOL=BTCUSDT`。现货模式不提供永续专属的持仓、资金费率和部分保护单能力。
## 4. 市场模式与符号
| 配置 | 含义 |
| --- | --- |
| `BINANCE_MARKET_TYPE=perp` | 默认模式,优先 USDⓈ-M 永续 |
| `BINANCE_MARKET_TYPE=spot` | 现货模式 |
| `BINANCE_MARKET_TYPE=auto` | 根据符号匹配市场,同名市场由默认逻辑选择 |
| `BTCUSDT_PERP` | 强制永续 |
| `BTCUSDT_SPOT` | 强制现货 |
期现套利使用显式拆分交易对:
```dotenv
BASIS_FUTURES_SYMBOL=BTCUSDT_PERP
BASIS_SPOT_SYMBOL=BTCUSDT_SPOT
```
## 5. 测试网配置
`BINANCE_SANDBOX=true` 会切换 CCXT REST 客户端。适配器的原生 WebSocket 地址由独立变量控制,完整测试网配置应同时设置 REST 与 WebSocket 地址:
```dotenv
BINANCE_SANDBOX=true
BINANCE_SPOT_REST_URL=https://testnet.binance.vision
BINANCE_SPOT_WS_URL=wss://stream.testnet.binance.vision/ws
BINANCE_FUTURES_REST_URL=https://testnet.binancefuture.com
BINANCE_FUTURES_WS_URL=wss://fstream.binancefuture.com
```
测试网配置必须使用对应测试网生成的 API Key。
## 6. 其他可选配置
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `BINANCE_ACCOUNT_POLL_MS` | `5000` | 账户 REST 校准间隔,最小 1000 ms |
| `BINANCE_ORDERS_POLL_MS` | `3000` | 订单 REST 校准间隔,最小 1000 ms |
| `BINANCE_SPOT_REST_URL` | `https://api.binance.com` | 现货 REST 地址 |
| `BINANCE_FUTURES_REST_URL` | `https://fapi.binance.com` | 永续 REST 地址 |
| `BINANCE_SPOT_WS_URL` | `wss://stream.binance.com:9443/ws` | 现货 WebSocket 地址 |
| `BINANCE_FUTURES_WS_URL` | `wss://fstream.binance.com/ws` | 永续 WebSocket 地址 |
## 7. 验证配置
```bash
bun run index.ts doctor --exchange binance --symbol BTCUSDT_PERP --json
bun run index.ts market ticker --exchange binance --symbol BTCUSDT_PERP --json
```
以上命令不会创建订单。真实策略启动前,先使用带 `--dry-run` 的订单命令检查下单参数。
## 常见问题
- `Invalid API-key, IP, or permissions`:检查市场权限、IP 白名单和主网/测试网 Key。
- `Binance symbol not found`:使用 `BTCUSDT_PERP``BTCUSDT_SPOT` 明确市场。
- `Position side does not match`:将 USDⓈ-M Futures 账户切换为单向持仓模式。
- 时间戳错误:同步运行机器时间。
## 参考资料
- [Binance Spot API](https://developers.binance.com/docs/binance-spot-api-docs)
- [Binance USDⓈ-M Futures API](https://developers.binance.com/docs/derivatives/usds-margined-futures)
- [仓库内 Binance API 参考](../binance/binance-spot/README.md)
+93
View File
@@ -0,0 +1,93 @@
# GRVT Configuration Guide
中文版:[GRVT 配置教程](grvt.md)
## Scope
This guide configures the GRVT perpetual adapter. The standard authentication path uses an API key to create a session and an API secret to sign orders with EIP-712. GRVT's API instrument and the ritmex-bot display symbol are separate settings.
## 1. Collect the account values and credentials
1. Activate an account and subaccount on [GRVT](https://grvt.io/exchange/sign-up?ref=sea).
2. Create trading credentials from the API Keys section in account settings.
3. Save the API key and its signing secret.
4. Record the numeric ID of the target subaccount.
5. Read the exact instrument name from the GRVT market list or the `all_instruments` endpoint.
The standard BTC perpetual instrument is `BTC_USDT_Perp`. Instrument names preserve GRVT's capitalization and separators.
## 2. Minimal configuration
```dotenv
EXCHANGE=grvt
GRVT_ENV=prod
GRVT_API_KEY=<your_grvt_api_key>
GRVT_API_SECRET=<your_grvt_signing_secret>
GRVT_SUB_ACCOUNT_ID=<your_sub_account_id>
GRVT_INSTRUMENT=BTC_USDT_Perp
GRVT_SYMBOL=BTCUSDT
```
`GRVT_INSTRUMENT` is required by the current adapter. `GRVT_SYMBOL` controls ritmex-bot display values and unified position mapping. When omitted, the adapter derives it by removing `_` and `-` from the instrument.
## 3. Select an environment
| `GRVT_ENV` | Purpose |
| --- | --- |
| `prod` | Production |
| `testnet` | Public test environment |
| `staging` | GRVT staging environment |
| `dev` | GRVT development environment |
`mainnet` and `production` resolve to `prod`. The API key, secret, and subaccount ID must belong to the same environment.
Testnet example:
```dotenv
GRVT_ENV=testnet
GRVT_INSTRUMENT=BTC_USDT_Perp
GRVT_SYMBOL=BTCUSDT
```
## 4. Reuse an existing session
Advanced deployments can provide:
```dotenv
GRVT_COOKIE=<existing_session_cookie>
GRVT_ACCOUNT_ID=<existing_account_id>
```
These two values bypass API-key login. Order signing still requires `GRVT_API_SECRET` or an external signer configured through `GRVT_SIGNER_PATH`.
`GRVT_SIGNER_PATH` is designed for deployments that maintain their own signing service. The module must export a signing function that returns the GRVT order-signature fields.
## 5. Verify the configuration
```bash
bun run index.ts doctor --exchange grvt --symbol BTCUSDT --json
bun run index.ts market ticker --exchange grvt --symbol BTCUSDT --json
```
These commands create no orders. The ticker check validates the environment, session login, and instrument metadata.
## Troubleshooting
- `Missing GRVT_INSTRUMENT`: provide the complete instrument, such as `BTC_USDT_Perp`.
- `Failed to authenticate with GRVT using API key`: verify the environment, API key, and subaccount ownership.
- `GRVT_API_SECRET is not configured for local signing`: provide the signing secret or configure an external signer.
- `Unable to load GRVT instrument metadata`: verify the instrument spelling and `GRVT_ENV`.
- Expired signatures: synchronize the host clock.
## Security
- Use dedicated trading credentials for the API key and signing secret.
- Protect the session cookie as an account-access credential.
- Restrict file permissions and callers for an external signer module.
## References
- [GRVT API Documentation](https://api-docs.grvt.io/)
- [GRVT Trading Streams and Authentication](https://api-docs.grvt.io/trading_streams)
- [Repository GRVT SDK reference](../grvt/sdk-readme.md)
+93
View File
@@ -0,0 +1,93 @@
# GRVT 配置教程
English version: [GRVT Configuration Guide](grvt.en.md)
## 适用范围
本教程用于 GRVT 永续适配器。标准认证路径使用 API Key 登录会话,并使用 API Secret 对订单执行 EIP-712 签名。GRVT 的 API instrument 与 ritmex-bot 展示符号是两个独立配置项。
## 1. 获取账户信息与凭证
1. 在 [GRVT](https://grvt.io/exchange/sign-up?ref=sea) 完成账户和子账户开通。
2. 在账户设置的 API Key 页面创建交易凭证。
3. 保存 API Key 和配套的签名 Secret。
4. 记录目标子账户的数字 ID。
5. 从 GRVT 市场或 `all_instruments` 接口确认完整 instrument 名称。
BTC 永续的标准 instrument 为 `BTC_USDT_Perp`。instrument 区分大小写和分隔符,应使用 GRVT 返回的原始值。
## 2. 最小配置
```dotenv
EXCHANGE=grvt
GRVT_ENV=prod
GRVT_API_KEY=<your_grvt_api_key>
GRVT_API_SECRET=<your_grvt_signing_secret>
GRVT_SUB_ACCOUNT_ID=<your_sub_account_id>
GRVT_INSTRUMENT=BTC_USDT_Perp
GRVT_SYMBOL=BTCUSDT
```
`GRVT_INSTRUMENT` 是当前适配器的必填项。`GRVT_SYMBOL` 用于 ritmex-bot 内部展示和统一仓位映射;省略时会从 instrument 去除 `_``-` 后生成。
## 3. 环境选择
| `GRVT_ENV` | 用途 |
| --- | --- |
| `prod` | 生产环境 |
| `testnet` | 公共测试环境 |
| `staging` | GRVT staging 环境 |
| `dev` | GRVT development 环境 |
`mainnet``production` 会解析为 `prod`。API Key、Secret 和子账户 ID 必须属于同一环境。
测试网示例:
```dotenv
GRVT_ENV=testnet
GRVT_INSTRUMENT=BTC_USDT_Perp
GRVT_SYMBOL=BTCUSDT
```
## 4. 复用现有会话
高级部署可以提供:
```dotenv
GRVT_COOKIE=<existing_session_cookie>
GRVT_ACCOUNT_ID=<existing_account_id>
```
这两个变量会跳过 API Key 登录。订单签名仍需要 `GRVT_API_SECRET``GRVT_SIGNER_PATH` 指向的外部签名模块。
`GRVT_SIGNER_PATH` 适用于自行维护签名服务的部署。模块需要导出签名函数,并返回 GRVT 订单签名字段。
## 5. 验证配置
```bash
bun run index.ts doctor --exchange grvt --symbol BTCUSDT --json
bun run index.ts market ticker --exchange grvt --symbol BTCUSDT --json
```
以上命令不会创建订单。行情检查会验证环境、登录会话和 instrument 元数据。
## 常见问题
- `Missing GRVT_INSTRUMENT`:填写完整 instrument,例如 `BTC_USDT_Perp`
- `Failed to authenticate with GRVT using API key`:检查环境、API Key 和子账户归属。
- `GRVT_API_SECRET is not configured for local signing`:填写签名 Secret 或配置外部 signer。
- `Unable to load GRVT instrument metadata`:检查 instrument 拼写和 `GRVT_ENV`
- 签名过期:同步运行机器时间。
## 安全要求
- API Key 和签名 Secret 应使用专用交易凭证。
- 会话 Cookie 具备账户访问能力,应按敏感凭证保护。
- 外部 signer 模块应限制文件权限和调用来源。
## 参考资料
- [GRVT API Documentation](https://api-docs.grvt.io/)
- [GRVT Trading Streams and Authentication](https://api-docs.grvt.io/trading_streams)
- [仓库内 GRVT SDK 参考](../grvt/sdk-readme.md)
+98
View File
@@ -0,0 +1,98 @@
# Lighter Configuration Guide
中文版:[Lighter 配置教程](lighter.md)
## Scope
This guide configures Lighter perpetuals and the integrated Spot markets. Lighter credentials consist of an account index, an API-key index, and an API private key. All three values must belong to the same network and account.
## 1. Select a network
| `LIGHTER_ENV` | REST URL | Chain ID |
| --- | --- | --- |
| `mainnet` | `https://mainnet.zklighter.elliot.ai` | `304` |
| `testnet` | `https://testnet.zklighter.elliot.ai` | `300` |
| `staging` | `https://staging.zklighter.elliot.ai` | `300` |
| `dev` | `https://dev.zklighter.elliot.ai` | `300` |
The current default is `testnet`. Set `LIGHTER_ENV=mainnet` explicitly for production trading.
## 2. Obtain the account index and API key
1. Create and fund an account on [Lighter](https://app.lighter.xyz/?referral=111909FA).
2. Follow the official [Get Started guide](https://apidocs.lighter.xyz/docs/get-started) to query `account_index` from the L1 address.
3. Follow the official [API Keys guide](https://apidocs.lighter.xyz/docs/api-keys) to create an API key.
4. Save the API private key returned by the creation flow and record its `api_key_index`.
User-created API-key indices range from `2` to `254`. Indices `0` and `1` are reserved for Web/mobile clients, and `255` queries all keys. ritmex-bot defaults to index `0`, so a user-created key needs an explicit matching index.
## 3. Minimal testnet configuration
```dotenv
EXCHANGE=lighter
LIGHTER_ENV=testnet
LIGHTER_ACCOUNT_INDEX=<your_account_index>
LIGHTER_API_KEY_INDEX=<your_api_key_index>
LIGHTER_API_PRIVATE_KEY=<your_api_private_key_hex>
LIGHTER_SYMBOL=BTC
```
Lighter perpetual markets use symbols such as `BTC`, `ETH`, and `SOL`. The adapter resolves the market ID and precision from metadata for the selected network.
## 4. Mainnet configuration
Replace the values with credentials generated for the mainnet account:
```dotenv
LIGHTER_ENV=mainnet
LIGHTER_ACCOUNT_INDEX=<your_mainnet_account_index>
LIGHTER_API_KEY_INDEX=<your_mainnet_api_key_index>
LIGHTER_API_PRIVATE_KEY=<your_mainnet_api_private_key_hex>
LIGHTER_SYMBOL=BTC
```
Testnet and mainnet credentials cannot be mixed.
## 5. Optional settings
| Variable | Purpose |
| --- | --- |
| `LIGHTER_BASE_URL` | Overrides the REST URL; known hostnames also determine the network |
| `LIGHTER_L1_ADDRESS` | L1 address associated with the account |
| `LIGHTER_MARKET_ID` | Forces a market ID when metadata resolution fails |
| `LIGHTER_MARKET_TYPE` | `perp` or `spot` |
| `LIGHTER_PRICE_DECIMALS` | Forces price decimals |
| `LIGHTER_SIZE_DECIMALS` | Forces size decimals |
| `LIGHTER_CHAIN_ID` | Overrides the signing chain ID |
| `LIGHTER_DEBUG` | Set to `1` or `true` for debug output |
Spot markets use symbols such as `ETH/USDC`. Explicit market IDs and decimal overrides must match order-book metadata for the selected network.
## 6. Verify the configuration
```bash
bun run index.ts doctor --exchange lighter --symbol BTC --json
bun run index.ts market ticker --exchange lighter --symbol BTC --json
```
The ticker check loads market metadata, validates the account/API-key pair, and opens WebSocket connections. It creates no orders.
## Troubleshooting
- `LIGHTER_ACCOUNT_INDEX must be an integer`: use the numeric index returned by the account API.
- `Invalid LIGHTER_API_KEY_INDEX`: use the non-negative integer recorded during key creation.
- `private key does not match the one on Lighter`: the account index, key index, private key, or network differs.
- `Configured market id ... not found`: verify `LIGHTER_ENV`, `LIGHTER_SYMBOL`, and any manual market ID.
- Signer loading failures: the repository ships macOS arm64 and Linux amd64 signer libraries. Other platforms require a compatible signer build or a supported WSL/Linux environment.
## Security
- The API private key can sign transactions. Use a dedicated key and restrictive file permissions.
- Keep public-key/index records separately and store the private key only in the runtime environment.
- Revoke the affected index and create a new key after exposure.
## References
- [Lighter Get Started](https://apidocs.lighter.xyz/docs/get-started)
- [Lighter API Keys](https://apidocs.lighter.xyz/docs/api-keys)
- [Repository Lighter introduction](../lighter/get-start.md)
+98
View File
@@ -0,0 +1,98 @@
# Lighter 配置教程
English version: [Lighter Configuration Guide](lighter.en.md)
## 适用范围
本教程用于 Lighter 永续和已接入现货市场。Lighter 凭证由账户索引、API Key 索引和 API 私钥组成,三者必须属于同一网络和同一账户。
## 1. 选择网络
| `LIGHTER_ENV` | REST 地址 | Chain ID |
| --- | --- | --- |
| `mainnet` | `https://mainnet.zklighter.elliot.ai` | `304` |
| `testnet` | `https://testnet.zklighter.elliot.ai` | `300` |
| `staging` | `https://staging.zklighter.elliot.ai` | `300` |
| `dev` | `https://dev.zklighter.elliot.ai` | `300` |
当前默认值为 `testnet`。生产交易应显式设置 `LIGHTER_ENV=mainnet`
## 2. 获取账户索引和 API Key
1. 在 [Lighter](https://app.lighter.xyz/?referral=111909FA) 创建并入金账户。
2. 按[官方 Get Started](https://apidocs.lighter.xyz/docs/get-started) 使用 L1 地址查询 `account_index`
3. 按[官方 API Keys 指南](https://apidocs.lighter.xyz/docs/api-keys) 创建 API Key。
4. 保存创建流程返回的 API 私钥,并记录对应的 `api_key_index`
用户创建的 API Key 索引范围为 `2``254``0``1` 由 Web/移动端保留,`255` 用于查询全部 Key。ritmex-bot 代码默认索引为 `0`,用户创建的 Key 应显式填写真实索引。
## 3. 最小测试网配置
```dotenv
EXCHANGE=lighter
LIGHTER_ENV=testnet
LIGHTER_ACCOUNT_INDEX=<your_account_index>
LIGHTER_API_KEY_INDEX=<your_api_key_index>
LIGHTER_API_PRIVATE_KEY=<your_api_private_key_hex>
LIGHTER_SYMBOL=BTC
```
Lighter 永续市场使用 `BTC``ETH``SOL` 这类市场符号。适配器会从目标网络的市场元数据自动解析 market ID 和精度。
## 4. 主网配置
将同一组变量替换为主网账户生成的值:
```dotenv
LIGHTER_ENV=mainnet
LIGHTER_ACCOUNT_INDEX=<your_mainnet_account_index>
LIGHTER_API_KEY_INDEX=<your_mainnet_api_key_index>
LIGHTER_API_PRIVATE_KEY=<your_mainnet_api_private_key_hex>
LIGHTER_SYMBOL=BTC
```
测试网和主网凭证不可混用。
## 5. 可选配置
| 变量 | 说明 |
| --- | --- |
| `LIGHTER_BASE_URL` | 覆盖 REST 地址;网络可从已知主机名推断 |
| `LIGHTER_L1_ADDRESS` | 账户关联的 L1 地址 |
| `LIGHTER_MARKET_ID` | 强制 market ID;仅在自动解析失败时设置 |
| `LIGHTER_MARKET_TYPE` | `perp``spot` |
| `LIGHTER_PRICE_DECIMALS` | 强制价格小数位 |
| `LIGHTER_SIZE_DECIMALS` | 强制数量小数位 |
| `LIGHTER_CHAIN_ID` | 覆盖签名 Chain ID |
| `LIGHTER_DEBUG` | 设置为 `1``true` 输出调试日志 |
现货市场使用 `ETH/USDC` 这类符号。显式 market ID、价格小数位和数量小数位必须与目标网络的 order book 元数据一致。
## 6. 验证配置
```bash
bun run index.ts doctor --exchange lighter --symbol BTC --json
bun run index.ts market ticker --exchange lighter --symbol BTC --json
```
行情检查会加载市场元数据、校验账户/API Key 对并建立 WebSocket,不会创建订单。
## 常见问题
- `LIGHTER_ACCOUNT_INDEX must be an integer`:填写账户接口返回的数字索引。
- `Invalid LIGHTER_API_KEY_INDEX`:使用创建 Key 时记录的非负整数索引。
- `private key does not match the one on Lighter`:账户索引、Key 索引、私钥或网络不匹配。
- `Configured market id ... not found`:检查 `LIGHTER_ENV``LIGHTER_SYMBOL` 和手动 market ID。
- signer 加载失败:仓库预置 macOS arm64 与 Linux amd64 签名库,其他平台需要构建兼容签名库或使用受支持的 WSL/Linux 环境。
## 安全要求
- API 私钥可以签署交易,应使用独立 Key 并限制文件权限。
- 保留 API 公钥和索引记录,私钥只存放在运行环境中。
- Key 泄露后在 Lighter 撤销对应索引并创建新 Key。
## 参考资料
- [Lighter Get Started](https://apidocs.lighter.xyz/docs/get-started)
- [Lighter API Keys](https://apidocs.lighter.xyz/docs/api-keys)
- [仓库内 Lighter 入门参考](../lighter/get-start.md)
+100
View File
@@ -0,0 +1,100 @@
# Nado Configuration Guide
中文版:[Nado 配置教程](nado.md)
## Scope
This guide configures the Nado perpetual adapter. Nado uses the Ink network, subaccounts, and linked signers. ritmex-bot requires the private key of an authorized linked signer, the subaccount owner address, and the subaccount name.
## 1. Prepare the Nado account
1. Connect an EVM wallet on [Nado](https://app.nado.xyz?join=LKbIUs5).
2. Fund the Ink network with ETH for gas and a supported collateral asset.
3. Deposit collateral and create the target subaccount.
4. Record the subaccount owner's EVM address and subaccount name. The common default name is `default`.
## 2. Obtain the linked-signer private key
The official Nado SDK provides `createStandardLinkedSigner` and `createDeterministicLinkedSignerPrivateKey` for creating and authorizing a subaccount linked signer.
The current Nado Web UI stores authorized signer information in browser local storage under the official domain. After creating the signer through the Web UI, inspect `Application``Local Storage``nado.userSettings` in browser developer tools and read the `privateKey` field. This value is a trading private key. Close developer tools and remove temporary records after retrieval.
`NADO_SIGNER_PRIVATE_KEY` must contain `0x` followed by 64 hexadecimal characters. It must be the linked-signer key. Do not place a wallet seed phrase or primary wallet private key in this variable.
## 3. Minimal mainnet configuration
```dotenv
EXCHANGE=nado
NADO_ENV=inkMainnet
NADO_SIGNER_PRIVATE_KEY=<0x_linked_signer_private_key>
NADO_SUBACCOUNT_OWNER=<0x_owner_address>
NADO_SUBACCOUNT_NAME=default
NADO_SYMBOL=BTC-PERP
```
`NADO_EVM_ADDRESS` is a compatible alias for `NADO_SUBACCOUNT_OWNER`.
## 4. Testnet configuration
```dotenv
NADO_ENV=inkTestnet
NADO_SIGNER_PRIVATE_KEY=<0x_testnet_linked_signer_private_key>
NADO_SUBACCOUNT_OWNER=<0x_testnet_owner_address>
NADO_SUBACCOUNT_NAME=default
NADO_SYMBOL=BTC-PERP
```
Linked-signer authorization is environment-specific. A mainnet signer cannot authorize a testnet subaccount.
## 5. Symbol format
Nado perpetual markets use product symbols such as `BTC-PERP` and `ETH-PERP`. The adapter also accepts:
- `BTCPERP``BTC-PERP`
- `BTCUSDT0``BTC-PERP`
- `BTC/PERP``BTC-PERP`
Prefer the native `*-PERP` symbol returned by the Nado API.
## 6. Optional settings
| Variable | Default | Purpose |
| --- | --- | --- |
| `NADO_GATEWAY_WS_URL` | Selected by environment | Trading gateway WebSocket |
| `NADO_SUBSCRIPTIONS_WS_URL` | Selected by environment | Market subscription WebSocket |
| `NADO_ARCHIVE_URL` | Production/test archive | History and indexer endpoint |
| `NADO_TRIGGER_URL` | Production/test trigger | Stop-loss and take-profit service |
| `NADO_MARKET_SLIPPAGE_PCT` | `0.01` | Market/trigger protection range; `0.01` means 1% |
| `NADO_STOP_TRIGGER_SOURCE` | `oracle` | `oracle`, `last`, or `mid` |
| `NADO_MIN_SIZE_POLICY` | `adjust` | `adjust` raises to minimum size; `reject` rejects the order |
| `NADO_DEBUG` | `false` | Detailed error logging |
## 7. Verify the configuration
```bash
bun run index.ts doctor --exchange nado --symbol BTC-PERP --json
bun run index.ts market ticker --exchange nado --symbol BTC-PERP --json
```
The ticker check validates the network, linked signer, subaccount, and product metadata. It creates no orders.
## Troubleshooting
- `Missing NADO_SIGNER_PRIVATE_KEY`: provide the 32-byte key of an authorized linked signer.
- Invalid private-key errors: confirm the value contains `0x` and 64 hexadecimal characters.
- `Missing NADO_SUBACCOUNT_OWNER`: provide the subaccount owner's 20-byte EVM address.
- Signature or permission errors: confirm that the signer is linked to the same owner, subaccount name, and network.
- Product lookup failures: use a native Nado symbol such as `BTC-PERP`.
## Security
- A linked signer can execute trades. Use a dedicated signer and rotate it regularly.
- Keep the primary wallet private key and seed phrase outside ritmex-bot configuration.
- Revoke authorization and create a new signer immediately after exposure.
## References
- [Nado Onboarding](../nado/onboarding-tutorial.md)
- [Nado TypeScript SDK](https://docs.nado.xyz/developer-resources/typescript-sdk)
- [Create a Nado Client](../nado/developer-resources/typescript-sdk/how-to/create-a-nado-client.md)
+99
View File
@@ -0,0 +1,99 @@
# Nado 配置教程
English version: [Nado Configuration Guide](nado.en.md)
## 适用范围
本教程用于 Nado 永续适配器。Nado 使用 Ink 网络、子账户和 linked signer。ritmex-bot 需要已授权 linked signer 的私钥、子账户 owner 地址和子账户名称。
## 1. 准备 Nado 账户
1. 在 [Nado](https://app.nado.xyz?join=LKbIUs5) 连接 EVM 钱包。
2. 为 Ink 网络准备 ETH gas 和受支持的保证金资产。
3. 完成入金并创建目标子账户。
4. 记录子账户 owner 的 EVM 地址和子账户名称,默认名称为 `default`
## 2. 获取 linked signer 私钥
Nado 官方 SDK 提供 `createStandardLinkedSigner``createDeterministicLinkedSignerPrivateKey`,用于创建并授权子账户 linked signer。
当前 Nado Web UI 会在官方域名的浏览器本地存储中保存已授权 signer 信息。使用 Web UI 创建 signer 后,可以在浏览器开发者工具的 `Application``Local Storage` 中检查 `nado.userSettings`,读取其中的 `privateKey` 字段。该值属于交易私钥,应在读取后关闭开发者工具并清理任何临时记录。
`NADO_SIGNER_PRIVATE_KEY` 必须是 `0x` 加 64 个十六进制字符。它应是 linked signer 私钥。钱包助记词和主钱包私钥不应填入该变量。
## 3. 主网最小配置
```dotenv
EXCHANGE=nado
NADO_ENV=inkMainnet
NADO_SIGNER_PRIVATE_KEY=<0x_linked_signer_private_key>
NADO_SUBACCOUNT_OWNER=<0x_owner_address>
NADO_SUBACCOUNT_NAME=default
NADO_SYMBOL=BTC-PERP
```
`NADO_SUBACCOUNT_OWNER` 可以使用兼容别名 `NADO_EVM_ADDRESS`
## 4. 测试网配置
```dotenv
NADO_ENV=inkTestnet
NADO_SIGNER_PRIVATE_KEY=<0x_testnet_linked_signer_private_key>
NADO_SUBACCOUNT_OWNER=<0x_testnet_owner_address>
NADO_SUBACCOUNT_NAME=default
NADO_SYMBOL=BTC-PERP
```
linked signer 的授权与环境绑定。主网 signer 无法用于测试网子账户。
## 5. 符号格式
Nado 永续市场使用 `BTC-PERP``ETH-PERP` 这类产品符号。适配器还接受:
- `BTCPERP``BTC-PERP`
- `BTCUSDT0``BTC-PERP`
- `BTC/PERP``BTC-PERP`
应优先使用 Nado API 返回的原始 `*-PERP` 符号。
## 6. 可选配置
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `NADO_GATEWAY_WS_URL` | 按环境选择 | 交易网关 WebSocket |
| `NADO_SUBSCRIPTIONS_WS_URL` | 按环境选择 | 行情订阅 WebSocket |
| `NADO_ARCHIVE_URL` | 生产/测试 archive | 历史和索引查询地址 |
| `NADO_TRIGGER_URL` | 生产/测试 trigger | 止损止盈服务地址 |
| `NADO_MARKET_SLIPPAGE_PCT` | `0.01` | 市价/触发单保护范围,`0.01` 表示 1% |
| `NADO_STOP_TRIGGER_SOURCE` | `oracle` | `oracle``last``mid` |
| `NADO_MIN_SIZE_POLICY` | `adjust` | `adjust` 自动上调到最小量;`reject` 直接拒绝 |
| `NADO_DEBUG` | `false` | 详细错误日志 |
## 7. 验证配置
```bash
bun run index.ts doctor --exchange nado --symbol BTC-PERP --json
bun run index.ts market ticker --exchange nado --symbol BTC-PERP --json
```
行情检查会验证网络、linked signer、子账户和产品元数据,不会创建订单。
## 常见问题
- `Missing NADO_SIGNER_PRIVATE_KEY`:填写已授权 linked signer 的 32 字节私钥。
- `Invalid NADO private key`:确认值包含 `0x` 和 64 个十六进制字符。
- `Missing NADO_SUBACCOUNT_OWNER`:填写子账户 owner 的 20 字节 EVM 地址。
- 签名/权限错误:确认 signer 已链接到相同 owner、子账户名称和网络。
- 产品未找到:使用 `BTC-PERP` 这类 Nado 原始符号。
## 安全要求
- linked signer 可以执行交易,应使用专用 signer 并定期轮换。
- 主钱包私钥和助记词不进入 ritmex-bot 配置。
- signer 泄露后立即在 Nado 撤销授权并创建新 signer。
## 参考资料
- [Nado Onboarding](../nado/onboarding-tutorial.md)
- [Nado TypeScript SDK](https://docs.nado.xyz/developer-resources/typescript-sdk)
- [创建 Nado Client](../nado/developer-resources/typescript-sdk/how-to/create-a-nado-client.md)
+97
View File
@@ -0,0 +1,97 @@
# Ondo Perps Configuration Guide
中文版:[Ondo Perps 配置教程](ondoperps.md)
## Scope
This guide configures the Ondo Perps crypto, equity, and commodity perpetual adapter. Authentication uses an API key ID and an API secret with HMAC-SHA256 signatures.
## 1. Create an API key
1. Activate and fund an account on [Ondo Perps](https://app.ondoperps.xyz/?ref=4A3ACQ).
2. Create an API key with read and trading permissions from the account API settings.
3. Save the API key ID and API secret.
4. Keep withdrawal access disabled and add a fixed IPv4 whitelist.
## 2. Minimal mainnet configuration
```dotenv
EXCHANGE=ondoperps
ONDOPERPS_API_KEY_ID=<your_ondo_key_id>
ONDOPERPS_API_SECRET=<your_ondo_api_secret>
ONDOPERPS_SYMBOL=BTC-USD.P
```
Market symbols use the `{TICKER}-USD.P` format, including:
- `BTC-USD.P`
- `ETH-USD.P`
- `XAU-USD.P`
- `NVDA-USD.P`
The adapter also normalizes `BTCUSDT`, `BTC/USD`, and `BTC-USD` to `BTC-USD.P`.
## 3. Sandbox configuration
```dotenv
EXCHANGE=ondoperps
ONDOPERPS_SANDBOX=true
ONDOPERPS_API_KEY_ID=<your_sandbox_key_id>
ONDOPERPS_API_SECRET=<your_sandbox_api_secret>
ONDOPERPS_SYMBOL=BTC-USD.P
```
Sandbox mode defaults to:
- REST: `https://api.ondoperps-sandbox.xyz`
- WebSocket: `wss://api.ondoperps-sandbox.xyz/ws`
The sandbox requires separately generated credentials.
## 4. Optional settings
| Variable | Default | Purpose |
| --- | --- | --- |
| `ONDOPERPS_BASE_URL` | `https://api.ondoperps.xyz` | REST API base URL |
| `ONDOPERPS_WS_URL` | `wss://api.ondoperps.xyz/ws` | WebSocket URL |
| `ONDOPERPS_BUILDER_CODE` | Empty | Builder code assigned by Ondo |
| `ONDOPERPS_BUILDER_FEE_RATE_BPS` | Empty | Positive integer builder fee, capped at 10 bps by the adapter |
Compatibility aliases:
- `EXCHANGE=ondoperp` or `EXCHANGE=ondo`
- Legacy `ONDOPERP_*` variable prefix
- `ONDO_KEY_ID` and `ONDO_API_SECRET`
Use `ondoperps` and `ONDOPERPS_*` for new deployments.
## 5. Verify the configuration
```bash
bun run index.ts doctor --exchange ondoperps --symbol BTC-USD.P --json
bun run index.ts market ticker --exchange ondoperps --symbol BTC-USD.P --json
```
The ticker check loads contract precision, opens WebSocket connections, and performs read-only market requests. It creates no orders.
## Troubleshooting
- `Missing ONDOPERPS_API_KEY_ID or ONDOPERPS_API_SECRET`: provide the complete credential pair.
- `401` or signature failures: synchronize system time and verify the key ID, secret, environment, and IP whitelist.
- Missing market: use a `{TICKER}-USD.P` symbol from the official Ondo market list.
- Invalid builder fee: use an integer from `1` to `10` bps.
- Empty production account in sandbox: use the dedicated sandbox account and credentials.
## Security
- Grant read and trading permissions only.
- Pass the API secret through the local runtime environment.
- Revoke and replace the key immediately after exposure.
## References
- [Ondo Perps API Authentication](https://docs.ondoperps.xyz/api-reference/api_key_authentication.md)
- [Ondo Perps REST Specification](https://docs.ondoperps.xyz/api-reference/rest-spec.json)
- [Ondo Perps WebSocket Specification](https://docs.ondoperps.xyz/api-reference/ws-spec.json)
- [Repository Ondo Perps integration reference](../ondoperps/README.md)
+97
View File
@@ -0,0 +1,97 @@
# Ondo Perps 配置教程
English version: [Ondo Perps Configuration Guide](ondoperps.en.md)
## 适用范围
本教程用于 Ondo Perps 加密资产、股票和商品永续适配器。认证使用 API Key ID 与 API Secret 的 HMAC-SHA256 签名。
## 1. 创建 API Key
1. 在 [Ondo Perps](https://app.ondoperps.xyz/?ref=4A3ACQ) 完成账户开通和入金。
2. 在账户 API 管理页面创建具备读取和交易权限的 API Key。
3. 保存 API Key ID 和 API Secret。
4. 关闭提现权限并配置固定 IPv4 白名单。
## 2. 主网最小配置
```dotenv
EXCHANGE=ondoperps
ONDOPERPS_API_KEY_ID=<your_ondo_key_id>
ONDOPERPS_API_SECRET=<your_ondo_api_secret>
ONDOPERPS_SYMBOL=BTC-USD.P
```
市场符号使用 `{TICKER}-USD.P` 格式,例如:
- `BTC-USD.P`
- `ETH-USD.P`
- `XAU-USD.P`
- `NVDA-USD.P`
适配器也会把 `BTCUSDT``BTC/USD``BTC-USD` 归一化为 `BTC-USD.P`
## 3. 沙盒配置
```dotenv
EXCHANGE=ondoperps
ONDOPERPS_SANDBOX=true
ONDOPERPS_API_KEY_ID=<your_sandbox_key_id>
ONDOPERPS_API_SECRET=<your_sandbox_api_secret>
ONDOPERPS_SYMBOL=BTC-USD.P
```
启用沙盒后默认使用:
- REST`https://api.ondoperps-sandbox.xyz`
- WebSocket`wss://api.ondoperps-sandbox.xyz/ws`
沙盒需要独立生成的凭证。
## 4. 可选配置
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `ONDOPERPS_BASE_URL` | `https://api.ondoperps.xyz` | REST API 地址 |
| `ONDOPERPS_WS_URL` | `wss://api.ondoperps.xyz/ws` | WebSocket 地址 |
| `ONDOPERPS_BUILDER_CODE` | 空 | Ondo 分配的 Builder Code |
| `ONDOPERPS_BUILDER_FEE_RATE_BPS` | 空 | 正整数 Builder 费率,适配器上限为 10 bps |
兼容别名:
- `EXCHANGE=ondoperp``EXCHANGE=ondo`
- `ONDOPERP_*` 旧变量前缀
- `ONDO_KEY_ID``ONDO_API_SECRET`
新部署应统一使用 `ondoperps``ONDOPERPS_*`
## 5. 验证配置
```bash
bun run index.ts doctor --exchange ondoperps --symbol BTC-USD.P --json
bun run index.ts market ticker --exchange ondoperps --symbol BTC-USD.P --json
```
行情检查会加载合约精度、连接 WebSocket 并执行只读市场请求,不会创建订单。
## 常见问题
- `Missing ONDOPERPS_API_KEY_ID or ONDOPERPS_API_SECRET`:填写完整凭证对。
- `401` 或签名失败:同步系统时间,检查 Key ID、Secret、环境和 IP 白名单。
- 市场不存在:使用 Ondo 官方市场列表中的 `{TICKER}-USD.P` 符号。
- Builder 费率异常:使用 `1``10` 的整数 bps 值。
- 沙盒连接生产账户为空:使用沙盒专用账户和凭证。
## 安全要求
- API Key 仅授予读取和交易权限。
- API Secret 只通过本机运行环境传入。
- 凭证泄露后立即撤销 Key 并创建新凭证。
## 参考资料
- [Ondo Perps API Authentication](https://docs.ondoperps.xyz/api-reference/api_key_authentication.md)
- [Ondo Perps REST Specification](https://docs.ondoperps.xyz/api-reference/rest-spec.json)
- [Ondo Perps WebSocket Specification](https://docs.ondoperps.xyz/api-reference/ws-spec.json)
- [仓库内 Ondo Perps 接入说明](../ondoperps/README.md)
+83
View File
@@ -0,0 +1,83 @@
# Paradex Configuration Guide
中文版:[Paradex 配置教程](paradex.md)
## Scope
This guide configures the Paradex perpetual adapter. The current implementation uses CCXT with an EVM wallet address and matching private key for Paradex account authentication. Production and testnet environments are supported.
## 1. Prepare a dedicated wallet and account
1. Create an EVM wallet dedicated to Paradex API trading.
2. Connect that wallet on [Paradex](https://paradex.io/ref/xingxingjun) and complete account onboarding.
3. Accept the trading terms and fund the target environment.
4. Record the wallet's `0x` address and matching 32-byte private key.
The adapter validates these formats:
- `PARADEX_PRIVATE_KEY` must contain `0x` followed by 64 hexadecimal characters.
- `PARADEX_WALLET_ADDRESS` must contain `0x` followed by 40 hexadecimal characters.
## 2. Minimal mainnet configuration
```dotenv
EXCHANGE=paradex
PARADEX_PRIVATE_KEY=<0x_private_key>
PARADEX_WALLET_ADDRESS=<0x_wallet_address>
PARADEX_SANDBOX=false
PARADEX_SYMBOL=BTC-USD-PERP
```
`BTC-USD-PERP` is the native Paradex market ID. The adapter resolves it to the CCXT unified market symbol. The native ID avoids unified-symbol collisions between options, dated futures, and perpetuals.
## 3. Testnet configuration
```dotenv
EXCHANGE=paradex
PARADEX_PRIVATE_KEY=<0x_testnet_private_key>
PARADEX_WALLET_ADDRESS=<0x_testnet_wallet_address>
PARADEX_SANDBOX=true
PARADEX_SYMBOL=BTC-USD-PERP
```
The Paradex testnet account requires separate onboarding. Mainnet and testnet account state, balances, and authentication contexts are independent.
## 4. Optional settings
| Variable | Default | Purpose |
| --- | --- | --- |
| `PARADEX_RECONNECT_DELAY_MS` | `2000` | WebSocket/polling reconnect delay |
| `PARADEX_USE_PRO` | Auto-detect | Allows `ccxt.pro` streaming APIs |
| `PARADEX_DEBUG` | `false` | Adapter debug output |
The project depends on `ccxt` and does not include `ccxt.pro`. A standard installation uses REST polling.
## 5. Verify the configuration
```bash
bun run index.ts doctor --exchange paradex --symbol BTC-USD-PERP --json
bun run index.ts market ticker --exchange paradex --symbol BTC-USD-PERP --json
```
The ticker check loads markets and verifies account access. It creates no orders. An account without onboarding returns a specific error.
## Troubleshooting
- `Invalid PARADEX_PRIVATE_KEY`: use a 32-byte `0x` hexadecimal private key.
- `Invalid PARADEX_WALLET_ADDRESS`: use the matching 20-byte EVM address.
- `Paradex account is not onboarded`: complete onboarding in the selected mainnet or testnet environment.
- `Symbol ... not found`: use the native market ID, such as `BTC-USD-PERP`.
- Balance query failures: verify that the address, private key, and `PARADEX_SANDBOX` setting belong to one environment.
## Security
- Use a dedicated API wallet and keep only strategy capital in it.
- The private key can sign wallet actions. Apply restrictive file permissions and limit host access.
- Move funds and replace the wallet immediately after key exposure.
## References
- [Paradex API Documentation](https://docs.paradex.trade/)
- [Paradex API Quick Start](https://docs.paradex.trade/api/general-information/api-quick-start)
- [Paradex API Authentication](https://docs.paradex.trade/api/general-information/authentication)
+83
View File
@@ -0,0 +1,83 @@
# Paradex 配置教程
English version: [Paradex Configuration Guide](paradex.en.md)
## 适用范围
本教程用于 Paradex 永续适配器。当前实现通过 CCXT 使用 EVM 钱包地址和对应私钥完成 Paradex 账户认证,并支持生产环境与测试网。
## 1. 准备专用钱包与账户
1. 创建一个专用于 Paradex API 交易的 EVM 钱包。
2. 在 [Paradex](https://paradex.io/ref/xingxingjun) 连接该钱包并完成账户 onboarding。
3. 接受交易条款并为目标环境准备保证金。
4. 记录钱包的 `0x` 地址和对应 32 字节私钥。
适配器会校验:
- `PARADEX_PRIVATE_KEY` 必须是 `0x` 加 64 个十六进制字符。
- `PARADEX_WALLET_ADDRESS` 必须是 `0x` 加 40 个十六进制字符。
## 2. 主网最小配置
```dotenv
EXCHANGE=paradex
PARADEX_PRIVATE_KEY=<0x_private_key>
PARADEX_WALLET_ADDRESS=<0x_wallet_address>
PARADEX_SANDBOX=false
PARADEX_SYMBOL=BTC-USD-PERP
```
`BTC-USD-PERP` 是 Paradex 原始 market ID。适配器会将其解析到 CCXT 统一市场符号。使用原始 market ID 可以避免期权、交割合约和永续市场的统一符号冲突。
## 3. 测试网配置
```dotenv
EXCHANGE=paradex
PARADEX_PRIVATE_KEY=<0x_testnet_private_key>
PARADEX_WALLET_ADDRESS=<0x_testnet_wallet_address>
PARADEX_SANDBOX=true
PARADEX_SYMBOL=BTC-USD-PERP
```
Paradex 测试网账户需要独立 onboarding。主网和测试网账户状态、余额与认证上下文相互独立。
## 4. 可选配置
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `PARADEX_RECONNECT_DELAY_MS` | `2000` | WebSocket/轮询重连延迟 |
| `PARADEX_USE_PRO` | 自动检测 | 允许使用 `ccxt.pro` 流式接口 |
| `PARADEX_DEBUG` | `false` | 输出适配器调试日志 |
项目依赖包含 `ccxt`,未包含 `ccxt.pro`。当前标准安装会使用 REST 轮询路径。
## 5. 验证配置
```bash
bun run index.ts doctor --exchange paradex --symbol BTC-USD-PERP --json
bun run index.ts market ticker --exchange paradex --symbol BTC-USD-PERP --json
```
行情检查会加载市场并验证账户访问,不会创建订单。未完成 onboarding 时会返回明确错误。
## 常见问题
- `Invalid PARADEX_PRIVATE_KEY`:私钥必须是 32 字节 `0x` 十六进制值。
- `Invalid PARADEX_WALLET_ADDRESS`:填写与私钥匹配的 20 字节 EVM 地址。
- `Paradex account is not onboarded`:在当前主网或测试网完成 onboarding。
- `Symbol ... not found`:使用原始市场 ID,例如 `BTC-USD-PERP`
- 余额查询失败:检查钱包地址、私钥和 `PARADEX_SANDBOX` 是否属于同一环境。
## 安全要求
- 使用专用 API 钱包,钱包中只保留策略所需资产。
- 私钥具备钱包签名能力,应使用严格文件权限并限制机器访问。
- 私钥泄露后立即转移资产并更换钱包。
## 参考资料
- [Paradex API Documentation](https://docs.paradex.trade/)
- [Paradex API Quick Start](https://docs.paradex.trade/api/general-information/api-quick-start)
- [Paradex API Onboarding](https://docs.paradex.trade/api/general-information/authentication)
+80
View File
@@ -0,0 +1,80 @@
# StandX Configuration Guide
中文版:[StandX 配置教程](standx.md)
## Scope
This guide configures the StandX perpetual adapter. A standard setup uses a JWT token and an Ed25519 request-signing private key. The JWT authenticates the session, and the Ed25519 key signs trading requests.
## 1. Obtain the token and signing key
1. Open [StandX API Session](https://standx.com/user/session).
2. Connect the wallet and sign in.
3. Generate an API token.
4. Save the JWT token, Ed25519 private key, creation date, and validity period shown by the page.
The official authentication flow obtains a JWT through wallet signing and uses an Ed25519 key for request-body signatures. The official key format is Base58. The adapter also accepts a 32-byte hexadecimal or Base64 representation.
## 2. Minimal configuration
```dotenv
EXCHANGE=standx
STANDX_TOKEN=<your_standx_jwt>
STANDX_REQUEST_PRIVATE_KEY=<your_ed25519_private_key>
STANDX_SYMBOL=BTC-USD
```
`STANDX_TOKEN` is required during adapter construction. `STANDX_REQUEST_PRIVATE_KEY` is required for signed actions such as creating orders, cancelling orders, and changing margin mode.
## 3. Token-expiry reminder
The official authentication flow uses a seven-day JWT lifetime by default. Configure the reminder from the values shown on the API page:
```dotenv
STANDX_TOKEN_CREATE_DATE=2026-07-12
STANDX_TOKEN_VALIDITY_DAYS=7
```
The legacy `STANDX_TOKEN_EXPIRY` setting accepts a seconds/milliseconds timestamp or an ISO date.
## 4. Optional settings
| Variable | Default | Purpose |
| --- | --- | --- |
| `STANDX_BASE_URL` | `https://perps.standx.com` | REST API base URL |
| `STANDX_WS_URL` | `wss://perps.standx.com/ws-stream/v1` | WebSocket URL |
| `STANDX_SESSION_ID` | Generated UUID | Session identifier for order-response streams |
| `STANDX_WS_DEBUG` | `false` | WebSocket debug logging |
| `STANDX_WS_DEBUG_RAW` | `false` | Raw WebSocket message logging |
Symbols use values such as `BTC-USD`. Confirm `PRICE_TICK`, `QTY_STEP`, and minimum quantity from the StandX market rules.
## 5. Verify the configuration
```bash
bun run index.ts doctor --exchange standx --symbol BTC-USD --json
bun run index.ts market ticker --exchange standx --symbol BTC-USD --json
```
These commands create no orders. Generate a new credential set after expiry and update both the token and signing key.
## Troubleshooting
- `Missing STANDX_TOKEN`: provide a valid JWT token.
- `Request signature skipped`: provide `STANDX_REQUEST_PRIVATE_KEY` and confirm its decoded length is 32 bytes.
- `401` or token expiry: generate a fresh token and update the expiry settings.
- Missing order-response events: keep `STANDX_SESSION_ID` stable and verify the WebSocket URL.
## Security
- The token and signing key together provide trading capability. Protect both as trading credentials.
- Use a dedicated API session and rotate it regularly.
- Revoke the session and generate new credentials immediately after exposure.
## References
- [StandX Authentication](https://docs.standx.com/standx-api/perps-auth)
- [StandX HTTP API](https://docs.standx.com/standx-api/perps-http)
- [Repository StandX authentication reference](../standx/auth.md)
- [StandX Maker Points strategy guide](../standx/maker-points-guide.md)
+80
View File
@@ -0,0 +1,80 @@
# StandX 配置教程
English version: [StandX Configuration Guide](standx.en.md)
## 适用范围
本教程用于 StandX 永续适配器。标准配置包含 JWT Token 和 Ed25519 请求签名私钥。JWT 负责身份认证,签名私钥负责交易类请求签名。
## 1. 获取 Token 与签名私钥
1. 打开 [StandX API Session](https://standx.com/user/session)。
2. 连接钱包并完成登录。
3. 生成 API Token。
4. 保存页面显示的 JWT Token、Ed25519 Private Key、创建日期和有效期。
官方认证流程使用钱包签名获取 JWT,并使用 Ed25519 密钥签署请求正文。官方密钥格式为 Base58,适配器也接受 32 字节十六进制或 Base64 表示。
## 2. 最小配置
```dotenv
EXCHANGE=standx
STANDX_TOKEN=<your_standx_jwt>
STANDX_REQUEST_PRIVATE_KEY=<your_ed25519_private_key>
STANDX_SYMBOL=BTC-USD
```
`STANDX_TOKEN` 是适配器初始化的必填项。`STANDX_REQUEST_PRIVATE_KEY` 是下单、撤单和修改保证金模式等签名请求的必填项。
## 3. Token 到期提醒
StandX 官方认证流程的默认 JWT 有效期为 7 天。按 API 页面显示的信息配置到期提醒:
```dotenv
STANDX_TOKEN_CREATE_DATE=2026-07-12
STANDX_TOKEN_VALIDITY_DAYS=7
```
旧配置也支持 `STANDX_TOKEN_EXPIRY`,其值可以是秒级/毫秒级时间戳或 ISO 日期。
## 4. 可选配置
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `STANDX_BASE_URL` | `https://perps.standx.com` | REST API 地址 |
| `STANDX_WS_URL` | `wss://perps.standx.com/ws-stream/v1` | WebSocket 地址 |
| `STANDX_SESSION_ID` | 自动生成 UUID | 订单响应流会话标识 |
| `STANDX_WS_DEBUG` | `false` | 输出 WebSocket 调试日志 |
| `STANDX_WS_DEBUG_RAW` | `false` | 输出原始 WebSocket 消息 |
交易对使用 `BTC-USD` 这类格式。通过市场规则确认 `PRICE_TICK``QTY_STEP` 和最小数量。
## 5. 验证配置
```bash
bun run index.ts doctor --exchange standx --symbol BTC-USD --json
bun run index.ts market ticker --exchange standx --symbol BTC-USD --json
```
以上命令不会创建订单。Token 到期后重新生成凭证,并同步更新 Token 与签名私钥。
## 常见问题
- `Missing STANDX_TOKEN`:填写有效 JWT Token。
- `Request signature skipped`:填写 `STANDX_REQUEST_PRIVATE_KEY`,确认 Base58 解码后为 32 字节。
- `401` 或 Token 过期:重新生成 Token,更新到期配置。
- 订单响应流缺失:保持 `STANDX_SESSION_ID` 稳定并检查 WebSocket 地址。
## 安全要求
- Token 与签名私钥组合具备交易能力,应按交易凭证级别保护。
- 使用独立 API 会话并定期轮换。
- 凭证泄露后立即在 StandX 撤销会话并生成新凭证。
## 参考资料
- [StandX Authentication](https://docs.standx.com/standx-api/perps-auth)
- [StandX HTTP API](https://docs.standx.com/standx-api/perps-http)
- [仓库内 StandX 认证参考](../standx/auth.md)
- [StandX 做市积分策略教程](../standx/maker-points-guide.md)
+58
View File
@@ -0,0 +1,58 @@
# Ondo Perps 接入说明
## 链接
- 邀请注册:https://app.ondoperps.xyz/?ref=4A3ACQ
- 官方文档索引:https://ondoperps.mintlify.app/llms.txt
- REST OpenAPIhttps://docs.ondoperps.xyz/api-reference/rest-spec.json
- WebSocket OpenAPIhttps://docs.ondoperps.xyz/api-reference/ws-spec.json
- API Key 鉴权:https://docs.ondoperps.xyz/api-reference/api_key_authentication.md
## 配置
```bash
EXCHANGE=ondoperps
ONDOPERPS_API_KEY_ID=ondoKeyId_xxx
ONDOPERPS_API_SECRET=ondoApiSecret_xxx
ONDOPERPS_SYMBOL=BTC-USD.P
```
Ondo Perps 市场使用 `{TICKER}-USD.P` 格式。默认值为 `BTC-USD.P`,其他示例包括 `ETH-USD.P``XAU-USD.P``NVDA-USD.P``AMD-USD.P`
兼容入口:`EXCHANGE=ondoperp` 会解析为 `ondoperps`,旧 `ONDOPERP_*` 环境变量会在对应 `ONDOPERPS_*` 变量缺失时使用。
可选配置:
| 变量 | 说明 |
| --- | --- |
| `ONDOPERPS_SANDBOX` | `true` 时连接官方沙盒 REST 与 WebSocket 地址 |
| `ONDOPERPS_BASE_URL` | 覆盖 REST API 地址 |
| `ONDOPERPS_WS_URL` | 覆盖 WebSocket 地址 |
| `ONDOPERPS_BUILDER_CODE` | Ondo 分配的 Builder Code |
| `ONDOPERPS_BUILDER_FEE_RATE_BPS` | Builder 订单费率,适配器限制在 110 bps |
## 已接入能力
- API Key HMAC-SHA256 REST 鉴权
- 限价单、Post Only 限价单、市价单、批量撤单与全部撤单
- 仓位级止损与止盈,映射到统一订单类型
- 账户余额、仓位、活动订单与市场精度查询
- 深度、标记价、K 线、资金费率、订单、仓位与余额 WebSocket 订阅
- REST 定时校准,覆盖 WebSocket 断线和私有频道鉴权异常场景
- 生产与沙盒端点切换
## 鉴权规则
REST 请求发送以下请求头:
- `ONDO-KEY-ID`
- `ONDO-TIMESTAMP`
- `ONDO-SIGN`
签名内容为 `timestamp + uppercaseMethod + requestPathWithQuery + body`,使用 API Secret 进行 HMAC-SHA256 并输出十六进制字符串。
WebSocket 登录消息使用 API Key ID、毫秒时间戳与 `time + "ondo_perps_ws_login"` 的 HMAC-SHA256 签名。连接空闲限制为 180 秒,适配器每 30 秒发送一次 ping。
## 安全
API Secret 只通过运行环境传入。仓库文件、日志与错误信息均不保存请求签名头或凭证内容。建议在 Ondo Perps 后台配置固定 IPv4 白名单,并为 API Key 只授予策略需要的权限。
+131 -54
View File
@@ -1,21 +1,30 @@
# 网格交易策略使用教程
本文介绍如何在 Ritmex Bot 中使用全新的网格交易策略。我们将以 ASTERUSDT 永续合约为例,演示从环境配置到运行监控的完整流程,并对关键参数、风控机制、常见问题做出说明
本文介绍如何在 Ritmex Bot 中使用网格交易策略。当前版本的网格引擎围绕「每线状态机 + 开/平仓意图自治 + 断点恢复」重新设计,支持三种交易模式、四层止损防护与智能跟随移格。本文覆盖从环境配置到运行监控的完整流程,并详细说明各项机制与参数
## 环境配置
## 核心概念
1. 复制 `.env.example``.env`
在阅读参数前,先了解四个核心概念:
- **网格线(Level**:策略在 `GRID_LOWER_PRICE` ~ `GRID_UPPER_PRICE` 之间按几何等比铺设 `GRID_LEVELS` 条价格线。每条线有独立的生命周期:`idle →(挂开仓单)entry_placed →(成交)holding →(挂平仓单)exit_placed →(平仓成交)idle`
- **开仓单(ENTRY)与平仓单(EXIT)**:策略自身通过订单登记表 + clientOrderId 编码(`grid-{网格版本}-E-…` / `grid-{网格版本}-X-…`)+ 价档匹配三级机制区分每笔挂单的意图,不依赖交易所回传的 reduceOnly 标志。
- **相邻线配对**:每条线的平仓目标固定为相邻线(多单在上一条线卖出、空单在下一条线买回),每格利润恒等于一格间距。
- **锚定价(Anchor**:中性模式启动时以首笔行情价为分界线,上半区挂空、下半区挂多。锚定价持久化到磁盘,重启后沿用,避免价格漂移导致半区与已有持仓错位。
## 快速开始
1. 复制 `.env.example``.env`
```bash
cp .env.example .env
```
2. 配置 Aster 交易所 API
2. 配置交易所 API(以 Aster 为例)
```env
EXCHANGE=aster
ASTER_API_KEY=你的API密钥
ASTER_API_SECRET=你的API密钥
TRADE_SYMBOL=ASTERUSDT
```
3. 设置基础精度与网格参数(示例使用 1.50 ~ 2.50 区间20 条网格单笔 5 手,最大仓位 50 手):
3. 设置精度与网格参数(示例1.50 ~ 2.50 区间20 条网格单笔 5 手、单侧最大 50 手):
```env
PRICE_TICK=0.0001
QTY_STEP=0.01
@@ -25,76 +34,144 @@
GRID_LEVELS=20
GRID_ORDER_SIZE=5
GRID_MAX_POSITION_SIZE=50
GRID_REFRESH_INTERVAL_MS=1000
GRID_MAX_LOG_ENTRIES=200
GRID_DIRECTION=both
GRID_STOP_LOSS_PCT=0.02
GRID_RESTART_TRIGGER_PCT=0.02
GRID_AUTO_RESTART_ENABLED=true
GRID_MAX_CLOSE_SLIPPAGE_PCT=0.05
```
```
4. 启动:
```bash
bun install
bun run index.ts --strategy grid --exchange aster
```
或运行 `bun start` 后在菜单选择「基础网格策略」。
- `GRID_ORDER_SIZE``GRID_MAX_POSITION_SIZE` 需遵循「最大仓位 ÷ 单笔数量 ≥ 网格数」的原则,这样策略才能补齐全部挂单。本例 50 ÷ 5 = 10,但网格数为 20,意味着策略只会在离现价最近的上下各 10 个位置挂单,与仓位上限保持一致。
建议先用 dry-run 模拟运行(连接真实行情但不真正下单,验证建格方向与参数):
```bash
ritmex-bot strategy run --strategy grid --dry-run
# 或未全局安装时:bunx ritmex-bot strategy run --strategy grid --dry-run
```
## 网格机制概览
## 参数总表
- **几何等比网格**:所有网格价格基于上下边界按等比方式分布。
- **基于现价的挂单排序**:重启或行情驱动时,会优先在现价附近补挂,避免远端挂单未成交。
- **双向模式**`GRID_DIRECTION=both` 表示买卖两侧都开仓;设置为 `long``short` 则只在对应方向发起新仓,反方向挂单会自动带上 `reduceOnly`
- **风控**
- 跌破下界 * (1 - STOP_LOSS_PCT) 或突破上界 * (1 + STOP_LOSS_PCT) 时,策略撤销所有限价单并用市价平仓。
- 若 `GRID_AUTO_RESTART_ENABLED=true`,当价格回到边界内 `RESTART_TRIGGER_PCT` 范围时会自动重启网格。
- **持仓限制**`GRID_MAX_POSITION_SIZE` 是总持仓上限,用于控制网格在极端走势中不会累积过量仓位。
### 基础参数
## 运行命令
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `GRID_LOWER_PRICE` / `GRID_UPPER_PRICE` | 必填 | 网格上下边界(计价货币) |
| `GRID_LEVELS` | 10 | 网格线数量(≥2),几何等比分布 |
| `GRID_ORDER_SIZE` | TRADE_AMOUNT | 每条线的下单数量(标的资产) |
| `GRID_MAX_POSITION_SIZE` | orderSize×(levels1) | 单方向最大持仓(中性模式下多、空两侧分别约束) |
| `GRID_DIRECTION` | both | `long`(只做多)/ `short`(只做空)/ `both`(中性双向) |
| `GRID_REFRESH_INTERVAL_MS` | 1000 | 引擎轮询间隔,每轮最多新挂 1 笔限价单 |
| `GRID_PRICE_TICK` / `GRID_QTY_STEP` | PRICE_TICK / QTY_STEP | 价格与数量精度;交易所支持时会自动同步为交易所精度 |
安装依赖后,使用 CLI 直接启动网格策略:
```bash
bun install
bun run index.ts --strategy grid --exchange aster
```
### 风控参数
若要在 Ink Dashboard 中运行并交互,直接执行:
```bash
bun start
```
然后在菜单中选择 “基础网格策略”。
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `GRID_STOP_LOSS_PCT` | 0.01 | 价格越过边界该比例后触发止损(层①),同时决定兜底止损单触发价 |
| `GRID_MAX_CLOSE_SLIPPAGE_PCT` | 0.05 | 所有市价平仓路径的滑点守卫:盘口价偏离标记价超过该比例时暂缓平仓、下轮重试 |
| `GRID_UNCOVERED_GRACE_MS` | 5000 | 覆盖审计(层②)的宽限期:持仓未被平仓单覆盖持续超过该时长才处置 |
| `GRID_EXCHANGE_STOP_ENABLED` | true | 在支持触发单的交易所(aster / binance / grvt / ondoperps)额外挂交易所侧 STOP_MARKET 兜底单(层④) |
| `GRID_AUTO_RESTART_ENABLED` | true | 止损停机后价格回到区间内自动重启网格 |
| `GRID_RESTART_TRIGGER_PCT` | 0.01 | 自动重启要求价格回到边界内该比例的缓冲区 |
## 监控与调优
### 智能移格参数
界面主要包括:
- 当前买一/卖一、开仓方向、挂单/持仓概况。
- 最近日志(订单状态、风控触发等)。
- 触发止损后会清空网格并记录原因。
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `GRID_SHIFT_ENABLED` | false | 开启后价格偏离锚定价超阈值时整体移格 |
| `GRID_SHIFT_TRIGGER_PCT` | 0.05 | 移格触发阈值:\|现价/锚定价 − 1\| |
| `GRID_SHIFT_CONFIRM_MS` | 3000 | 偏离需持续该时长才触发(防插针) |
| `GRID_SHIFT_RANGE_PCT` | 0.05 | 移格后新区间 = 新锚定价 × (1 ± 该比例) |
调参建议:
1. **缩短区间**:想拉高单格盈利,可缩小上下边界并减少网格数。
2. **更精细挂单**:适当提高 `GRID_LEVELS` 并降低 `GRID_ORDER_SIZE`,但同时记得调大 `GRID_MAX_POSITION_SIZE`
3. **调节平仓容忍度**`GRID_MAX_CLOSE_SLIPPAGE_PCT` 控制平仓单相对标记价的最大偏移,确保 reduce-only 订单不会被交易所拒绝。
4. **只做单边**:若只想高抛低吸不反手,可设 `GRID_DIRECTION=long`,卖单会变成 `reduceOnly`
### 高级参数
## 中断恢复行为
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `GRID_USE_REDUCE_ONLY` | false | 平仓单是否携带 reduceOnly。默认不带(部分交易所会拒绝与反向挂单共存的 reduce-only 限价单);策略靠意图登记自治区分开/平仓,无需此标志 |
| `GRID_RECONCILE_INTERVAL_MS` | 30000 | 支持 REST 查单的交易所的周期对账间隔 |
| `GRID_DATA_DIR` | ./data | 状态持久化目录(`grid-record.json` |
策略重启后会:
- 重新订阅账户、订单、深度、ticker;
- 基于当前持仓和开放订单重新计算网格,只补挂缺失部分;
- 在仓位额度允许的情况下持续追踪价位。
## 三种交易模式
因此就算进程断掉,只要交易所回放的账号/订单快照完整,网格会从中断前的状态继续运行。若停机前手动撤过单,新启动时系统会把不在网格计划中的挂单一并清理。
`GRID_DIRECTION` 决定每条线的角色:
| 模式 | 开仓线 | 开仓方向 | 平仓目标 |
|---|---|---|---|
| `long` | 除最顶线外全部 | BUY | 上一条线(SELL |
| `short` | 除最底线外全部 | SELL | 下一条线(BUY |
| `both`(中性) | 锚定价下方 BUY / 上方 SELL | 按半区 | BUY→上一条线 / SELL→下一条线 |
- **long**:只在现价下方挂买单,买入成交后在相邻上方线挂卖单止盈。价格上行时逐格落袋,下行时逐格接多。
- **short**:镜像逻辑,只在现价上方挂卖单,成交后在相邻下方线买回。
- **both(中性)**:以启动时的锚定价分界。价格向上穿越上半区某条线时,会同时发生「下方多单的止盈卖出」与「该线自身的空头开仓」——两笔同价卖单并存是中性网格的正常形态。
## 挂单与仓位规则
- **每线一单**:只有 `idle` 状态的线才允许挂开仓单。线在 `holding` / `exit_placed` 期间,价格反复穿越也不会重复开仓,直到平仓单成交释放该线。
- **就近优先**:开仓单按与现价的距离排序,每轮只补挂 1 笔,逐步铺满。
- **仓位上限**:每次开仓前计算 `剩余额度 = GRID_MAX_POSITION_SIZE |同方向净仓| − 同方向在途开仓挂单量`,额度不足时跳过该线。中性模式下多空两侧分别计算。
- **平仓优先**:每轮规划先补挂缺失的平仓单,再考虑开仓单。
## 持久化与中断恢复
策略状态实时落盘到 `data/grid-record.json`schema v2,旧版 v1 文件自动迁移),内容包括:网格版本、锚定价、区间边界、每条线的状态与持仓量、每笔挂单的意图登记、移格进度、兜底止损单。
- **下单前写前日志(write-ahead**:每笔限价单在发出前先落盘 inflight 槽位,交易所接单后立即登记订单号并再次落盘,消灭「交易所已接单、本地未记录」的崩溃窗口。
- **重启恢复**:启动时读取磁盘状态(要求配置指纹一致:方向 / 单笔数量 / 网格数 / 网格模式 / 交易对 / 交易所;**区间边界以磁盘为准**,移格后可能与 env 不同),然后执行三方对账:磁盘登记 ↔ 交易所挂单 ↔ 实际仓位。挂单按订单号 → clientOrderId → 价档三级匹配归位;无法归属的挂单中,平仓方向的收编为「孤儿平仓单」继续保护仓位,其余撤销;仓位差额归档到最近的线(每线不超过单笔数量),归不完的残余交给覆盖审计立即处置。
- **配置变更**:修改方向、网格数、单笔数量等指纹字段后重启会放弃旧状态、全新建格,并对现场执行孤儿扫描(撤掉旧挂单、按新网格归档仓位)。
- **断线重连**:支持连接事件的交易所(standx / ondoperps / binance)断连时冻结新下单,重连后用 REST 查单 + 查仓走同一套对账逻辑;支持 REST 查单的交易所另有周期对账兜底(`GRID_RECONCILE_INTERVAL_MS`)。其余交易所依赖网关自动重连 + 订单流差分判定,并有「下单后订单流长时间无反映则暂停新下单」的陈旧性守卫。
## 多重止损(四层防护)
1. **层① 价格越界**:现价 ≤ 下界×(1stopLossPct) 或 ≥ 上界×(1+stopLossPct) 时,撤销全部挂单 → 市价平掉全部持仓(受滑点守卫保护,被拦截时下轮重试)→ 清空状态停机。开启移格时越界优先走移格,层①兜移格禁用或移格中再次越界的场景。
2. **层② 持仓覆盖审计**:每轮核对 `未覆盖仓位 = |净仓| − 活跃平仓挂单量 − 待挂平仓的线上持仓`。未覆盖持续超过 `GRID_UNCOVERED_GRACE_MS` 时:价格仍在区间内且浮亏未超限 → 在最近的可盈利线补挂平仓单;价格已出区间或浮亏超过 stopLossPct → 未覆盖部分直接市价平掉。
3. **层③ 恢复期孤儿扫描**:重启/重连对账后无法归档到任何线的残余仓位,跳过宽限期立即按层②处置;恢复完成前不开新仓。
4. **层④ 交易所侧兜底单**:在支持触发单的交易所(aster / binance / grvt / ondoperps),净多时挂 SELL STOP_MARKET @ 下界×(1stopLossPct),净空时挂 BUY STOP_MARKET @ 上界×(1+stopLossPct)。即使机器人进程死亡,交易所也会在极端行情中兜底平仓。方向变化、触发价偏移或订单消失时自动重挂,仓位归零时自动撤销。
## 智能跟随网格(移格)
开启 `GRID_SHIFT_ENABLED=true` 后,价格偏离锚定价超过 `GRID_SHIFT_TRIGGER_PCT` 且持续 `GRID_SHIFT_CONFIRM_MS`,策略执行三阶段移格:
1. **cancelling**:撤销全部挂单(含兜底止损单);
2. **closing**:市价平掉全部持仓(受滑点守卫保护);
3. **rebuilding**:以当前价为新锚定价,新区间 = 锚定价 × (1 ± `GRID_SHIFT_RANGE_PCT`),网格版本 +1,全部线重置后重新铺网。
每个阶段进度都持久化,进程在任一阶段崩溃后重启会从记录的阶段续跑。移格期间冻结开仓,层①②止损照常生效。移格会实现当前浮动盈亏——趋势行情中这意味着接受每次移格的亏损换取网格持续贴近现价,请结合波动性谨慎开启。
## 监控界面
Ink 仪表盘除价格与区间外,新增以下信息:
- **锚定价 / 网格版本**`v1` 起步,每次移格或重启重建 +1
- **移格状态**:移格进行中显示当前阶段(cancelling / closing / rebuilding);
- **止损防护行**:实时显示未覆盖仓位数量与交易所兜底止损单(方向 @ 触发价);
- **网格线表**:每条线的价格、方向(BUY / SELL / `-` 表示不开仓线)、状态(idle / entry_placed / holding / exit_placed)、是否有活跃挂单、线上持仓量。
## 常见问题
### Q: 为什么只有靠近现价的几个网格有订单?
A: 每笔网格单都会占用一定仓位上限。当 `GRID_MAX_POSITION_SIZE / GRID_ORDER_SIZE < GRID_LEVELS` 时,只会展示足以满足仓位限制的那几条网格。调整任一参数即可扩大覆盖面
### Q: 为什么启动后不是一次性挂满所有网格单?
A: 引擎每轮最多新挂 1 笔限价单(按离现价由近到远),既控制请求频率也便于逐单登记意图。以默认 1 秒轮询计,20 条网格约 1 分钟内铺满
### Q: 价格突破上界后为何立即平仓
A: 这是止损保护触发,避免庄外行情继续拉扯,默认 2% 触发后网格会全部撤单,并用市价平掉现有仓位
### Q: 为什么同一价位出现两笔同向挂单
A: 中性模式的正常形态:一笔是下方线的止盈平仓单,另一笔是该线自身的空头开仓单。价格穿越时两笔都成交,等于「平多 + 开空」。策略内部按意图分别登记,不会混淆
### Q: 平仓单为什么不带 reduceOnly
A: 部分交易所会拒绝与反向挂单共存的 reduce-only 限价单。策略通过自身的意图登记区分开/平仓,不需要该标志。若你的交易所支持且希望强制,只需设 `GRID_USE_REDUCE_ONLY=true`
### Q: 修改了参数重启后旧挂单怎么办?
A: 若改的是配置指纹字段(方向 / 网格数 / 单笔数量等),策略会全新建格并撤销所有无法归属的旧挂单;平仓方向的旧挂单会被保留为孤儿平仓单继续保护仓位。若只是重启未改参数,挂单和线状态原样恢复,不重复挂单。
### Q: 想要手动调仓怎么办?
A: 暂停策略(Ctrl+C 或 dashboard 退出)后手动操作,完成后再启动,策略会以新的仓位/挂单为基准重新布网
A: 停止策略后手动操作,再启动即可——对账机制会以新的仓位/挂单为基准归档:手动加的仓归到最近的线,手动挂的平仓方向订单被收编,其余手动挂单被撤销。若想彻底重来,删除 `data/grid-record.json` 后重启
## 小结
### Q: 价格突破边界后发生了什么?
A: 依次发生:交易所侧兜底止损单先行触发(若启用且进程已死);进程存活时层①撤单并市价平仓后停机;若 `GRID_AUTO_RESTART_ENABLED=true`,价格回到边界内缓冲区后自动以当前价重新建格。开启移格时则优先整体平移网格而不是停机。
通过上述配置,你就可以在 ASTERUSDT 合约上运行一个自动化的等比网格策略。请务必先在沙盒或小仓位测试,确保参数适应当前波动性和手续费结构,再逐步提升资金规模。
## 风险提示
- 网格策略在震荡行情中赚取格差,在单边行情中会累积逆势仓位。`GRID_MAX_POSITION_SIZE``GRID_STOP_LOSS_PCT` 是最重要的两道闸门,务必按可承受亏损设置。
- 移格功能会在每次移格时实现浮亏,等于把「区间失效」的损失分期支付,并不消除趋势风险。
- 请先用 `ritmex-bot strategy run --strategy grid --dry-run` 或小仓位验证参数与手续费结构,再逐步放大资金规模。
祝交易顺利!
+24 -13
View File
@@ -1,11 +1,21 @@
{
"name": "ritmex-bot",
"version": "0.1.0",
"module": "index.ts",
"type": "module",
"private": true,
"private": false,
"repository": {
"type": "git",
"url": "https://github.com/discountry/ritmex-bot.git"
},
"bin": {
"ritmex-bot": "./bin/ritmex-bot"
},
"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",
@@ -18,25 +28,26 @@
"pm2:start:maker-points": "pm2 start bun --name ritmex-maker-points --cwd . --restart-delay 5000 -- run index.ts --strategy maker-points --exchange standx --silent"
},
"devDependencies": {
"@types/bun": "latest",
"vitest": "^3.2.4"
"@types/bun": "^1.3.9",
"oxlint": "^1.54.0",
"vitest": "^4.0.18"
},
"peerDependencies": {
"typescript": "^5.9.2"
"typescript": "^5.9.3"
},
"dependencies": {
"@grvt/client": "^1.6.25",
"@nadohq/client": "^0.1.0-alpha.45",
"@grvt/client": "^1.6.27",
"@nadohq/client": "^0.1.0-alpha.51",
"@noble/ed25519": "^3.0.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",
"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",
"trading-signals": "^7.4.3",
"viem": "^2.45.1",
"viem": "^2.46.3",
"ws": "^8.19.0"
}
}
+306
View File
@@ -0,0 +1,306 @@
---
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.
+8 -1
View File
@@ -88,6 +88,8 @@ function assignExchange(options: CliOptions, raw: string): void {
options.exchange = normalized as CliOptions["exchange"];
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
options.exchange = "grvt";
} else if (normalized === "ondo" || normalized === "ondoperp") {
options.exchange = "ondoperps";
}
}
@@ -101,5 +103,10 @@ 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`);
` --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`);
}
+680
View File
@@ -0,0 +1,680 @@
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(),
triggerOrders: adapter.supportsTriggerOrders?.() ?? false,
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);
}
+528
View File
@@ -0,0 +1,528 @@
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}'`);
}
+86
View File
@@ -0,0 +1,86 @@
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;
}
+29 -12
View File
@@ -2,6 +2,7 @@ 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";
@@ -16,6 +17,7 @@ import type { StrategyId } from "./args";
interface RunnerOptions {
silent?: boolean;
dryRun?: boolean;
}
type StrategyRunner = (options: RunnerOptions) => Promise<void>;
@@ -43,12 +45,13 @@ 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);
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
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),
@@ -56,12 +59,13 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
},
swing: async (opts) => {
const config = swingConfig;
const adapter = createAdapterOrThrow(config.symbol);
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
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),
@@ -69,12 +73,13 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
},
guardian: async (opts) => {
const config = tradingConfig;
const adapter = createAdapterOrThrow(config.symbol);
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
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),
@@ -82,12 +87,13 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
},
maker: async (opts) => {
const config = makerConfig;
const adapter = createAdapterOrThrow(config.symbol);
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
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),
@@ -99,12 +105,13 @@ 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);
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
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),
@@ -112,12 +119,13 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
},
"offset-maker": async (opts) => {
const config = makerConfig;
const adapter = createAdapterOrThrow(config.symbol);
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
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),
@@ -125,12 +133,13 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
},
"liquidity-maker": async (opts) => {
const config = liquidityMakerConfig;
const adapter = createAdapterOrThrow(config.symbol);
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
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),
@@ -144,12 +153,13 @@ 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);
const adapter = createAdapterOrThrow(basisConfig.futuresSymbol, opts.dryRun);
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),
@@ -157,12 +167,13 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
},
grid: async (opts) => {
const config = gridConfig;
const adapter = createAdapterOrThrow(config.symbol);
const adapter = createAdapterOrThrow(config.symbol, opts.dryRun);
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),
@@ -174,6 +185,7 @@ 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;
@@ -226,7 +238,8 @@ async function runEngine<
onUpdate(emitter);
engine.start();
console.info(`[${label}] Starting on ${exchangeName}. Mode: ${silent ? "silent" : "interactive"}. Press Ctrl+C to exit.`);
const modeLabel = `${silent ? "silent" : "interactive"}${harness.dryRun ? "+dry-run" : ""}`;
console.info(`[${label}] Starting on ${exchangeName}. Mode: ${modeLabel}. Press Ctrl+C to exit.`);
const shutdown = (signal: NodeJS.Signals) => {
try {
@@ -251,8 +264,12 @@ async function runEngine<
});
}
function createAdapterOrThrow(symbol: string): ExchangeAdapter {
return buildAdapterFromEnv({ exchangeId: resolveExchangeId(), symbol });
function createAdapterOrThrow(symbol: string, dryRun?: boolean): ExchangeAdapter {
const adapter = buildAdapterFromEnv({ exchangeId: resolveExchangeId(), symbol });
if (dryRun) {
return new DryRunExchangeAdapter(adapter);
}
return adapter;
}
type TradeLogEntry = { time: string; type: string; detail: string };
+17
View File
@@ -98,6 +98,7 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
nado: { envKeys: ["NADO_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-PERP" },
standx: { envKeys: ["STANDX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD" },
binance: { envKeys: ["BINANCE_SYMBOL", "TRADE_SYMBOL"], fallback: "BTCUSDT" },
ondoperps: { envKeys: ["ONDOPERPS_SYMBOL", "ONDOPERP_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD.P" },
};
export function resolveSymbolFromEnv(explicitExchangeId?: SupportedExchangeId | string | null): string {
@@ -292,6 +293,14 @@ export interface GridConfig {
autoRestart: boolean;
gridMode: "geometric";
maxCloseSlippagePct: number;
gridShiftEnabled: boolean;
gridShiftTriggerPct: number;
gridShiftRangePct: number;
gridShiftConfirmMs: number;
useReduceOnlyForExit: boolean;
exchangeStopEnabled: boolean;
reconcileIntervalMs: number;
uncoveredGraceMs: number;
}
const resolveBasisSymbol = (envKeys: string[], fallback: string): string => {
@@ -372,6 +381,14 @@ export const gridConfig: GridConfig = {
0.05
)
),
gridShiftEnabled: parseBoolean(process.env.GRID_SHIFT_ENABLED, false),
gridShiftTriggerPct: Math.max(0, parseNumber(process.env.GRID_SHIFT_TRIGGER_PCT, 0.05)),
gridShiftRangePct: Math.max(0, parseNumber(process.env.GRID_SHIFT_RANGE_PCT, 0.05)),
gridShiftConfirmMs: Math.max(0, parseNumber(process.env.GRID_SHIFT_CONFIRM_MS, 3000)),
useReduceOnlyForExit: parseBoolean(process.env.GRID_USE_REDUCE_ONLY, false),
exchangeStopEnabled: parseBoolean(process.env.GRID_EXCHANGE_STOP_ENABLED, true),
reconcileIntervalMs: Math.max(1000, parseNumber(process.env.GRID_RECONCILE_INTERVAL_MS, 30_000)),
uncoveredGraceMs: Math.max(0, parseNumber(process.env.GRID_UNCOVERED_GRACE_MS, 5000)),
};
gridConfig.maxPositionSize = resolveGridMaxPosition(gridConfig.orderSize, gridConfig.gridLevels);
+4 -4
View File
@@ -1,4 +1,4 @@
import type { AsterOrder } from "../../exchanges/types";
import type { Order } from "../../exchanges/types";
export interface OrderTarget {
side: "BUY" | "SELL";
@@ -8,11 +8,11 @@ export interface OrderTarget {
}
export function makeOrderPlan(
openOrders: AsterOrder[],
openOrders: Order[],
targets: OrderTarget[]
): { toCancel: AsterOrder[]; toPlace: OrderTarget[] } {
): { toCancel: Order[]; toPlace: OrderTarget[] } {
const unmatched = new Set(targets.map((_, idx) => idx));
const toCancel: AsterOrder[] = [];
const toCancel: Order[] = [];
for (const order of openOrders) {
const orderPrice = String(order.price);
+2 -2
View File
@@ -1,11 +1,11 @@
import type { ExchangeAdapter } from "../../exchanges/adapter";
import type { AsterOrder } from "../../exchanges/types";
import type { Order } from "../../exchanges/types";
import { isUnknownOrderError } from "../../utils/errors";
export async function safeCancelOrder(
exchange: ExchangeAdapter,
symbol: string,
order: AsterOrder,
order: Order,
onResolved: (orderId: number | string) => void,
onUnknown: () => void,
onError: (err: unknown) => void
+13 -11
View File
@@ -1,5 +1,5 @@
import type { ExchangeAdapter } from "../exchanges/adapter";
import type { AsterOrder } from "../exchanges/types";
import type { Order } from "../exchanges/types";
import {
routeCloseOrder,
routeLimitOrder,
@@ -88,7 +88,7 @@ export function unlockOperating(
export async function deduplicateOrders(
adapter: ExchangeAdapter,
symbol: string,
openOrders: AsterOrder[],
openOrders: Order[],
locks: OrderLockMap,
timers: OrderTimerMap,
pendings: OrderPendingMap,
@@ -134,12 +134,13 @@ type PlaceOrderOptions = {
skipDedupe?: boolean;
slPrice?: number;
tpPrice?: number;
clientOrderId?: string;
};
export async function placeOrder(
adapter: ExchangeAdapter,
symbol: string,
openOrders: AsterOrder[],
openOrders: Order[],
locks: OrderLockMap,
timers: OrderTimerMap,
pendings: OrderPendingMap,
@@ -150,7 +151,7 @@ export async function placeOrder(
reduceOnly = false,
guard?: OrderGuardOptions,
opts?: PlaceOrderOptions
): Promise<AsterOrder | undefined> {
): Promise<Order | undefined> {
const type = "LIMIT";
if (isOperating(locks, type)) return;
const priceNum = Number(price);
@@ -180,6 +181,7 @@ 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}` : ""}`);
@@ -197,7 +199,7 @@ export async function placeOrder(
export async function placeMarketOrder(
adapter: ExchangeAdapter,
symbol: string,
openOrders: AsterOrder[],
openOrders: Order[],
locks: OrderLockMap,
timers: OrderTimerMap,
pendings: OrderPendingMap,
@@ -207,7 +209,7 @@ export async function placeMarketOrder(
reduceOnly = false,
guard?: OrderGuardOptions,
opts?: { qtyStep: number }
): Promise<AsterOrder | undefined> {
): Promise<Order | undefined> {
const type = "MARKET";
if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
@@ -247,7 +249,7 @@ export async function placeMarketOrder(
export async function placeStopLossOrder(
adapter: ExchangeAdapter,
symbol: string,
openOrders: AsterOrder[],
openOrders: Order[],
locks: OrderLockMap,
timers: OrderTimerMap,
pendings: OrderPendingMap,
@@ -258,7 +260,7 @@ export async function placeStopLossOrder(
log: LogHandler,
guard?: OrderGuardOptions,
opts?: { priceTick: number; qtyStep: number }
): Promise<AsterOrder | undefined> {
): Promise<Order | undefined> {
const type = "STOP_MARKET";
if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
@@ -314,7 +316,7 @@ export async function placeStopLossOrder(
export async function placeTrailingStopOrder(
adapter: ExchangeAdapter,
symbol: string,
openOrders: AsterOrder[],
openOrders: Order[],
locks: OrderLockMap,
timers: OrderTimerMap,
pendings: OrderPendingMap,
@@ -325,7 +327,7 @@ export async function placeTrailingStopOrder(
log: LogHandler,
guard?: OrderGuardOptions,
opts?: { priceTick: number; qtyStep: number }
): Promise<AsterOrder | undefined> {
): Promise<Order | undefined> {
const type = "TRAILING_STOP_MARKET";
if (isOperating(locks, type)) return;
if (!adapter.supportsTrailingStops()) {
@@ -375,7 +377,7 @@ export async function placeTrailingStopOrder(
export async function marketClose(
adapter: ExchangeAdapter,
symbol: string,
openOrders: AsterOrder[],
openOrders: Order[],
locks: OrderLockMap,
timers: OrderTimerMap,
pendings: OrderPendingMap,
+91
View File
@@ -0,0 +1,91 @@
import { extractMessage } from "../utils/errors";
/**
* Wrap a callback so exceptions inside it are swallowed and logged.
* Used by adapter watch* methods to prevent gateway callback errors
* from killing the event loop.
*/
export function createSafeInvoke(adapterName: string) {
return function safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[${adapterName}] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
};
}
export interface InitManager {
ensureInitialized(context?: string): Promise<void>;
}
/**
* Creates a reusable init-once-with-retry manager.
* Every adapter uses the same pattern: cache the init promise, retry on
* failure with exponential back-off, and deduplicate context error logs.
*/
export function createInitManager(
adapterName: string,
doInitialize: () => Promise<void>,
): InitManager {
let initPromise: Promise<void> | null = null;
const initContexts = new Set<string>();
let retryTimer: ReturnType<typeof setTimeout> | null = null;
let retryDelayMs = 3000;
let lastInitErrorAt = 0;
function clearRetry(): void {
if (retryTimer) {
clearTimeout(retryTimer);
retryTimer = null;
}
retryDelayMs = 3000;
}
function handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - lastInitErrorAt < 5000) return;
lastInitErrorAt = now;
console.error(`[${adapterName}] ${context} failed`, error);
}
function scheduleRetry(): void {
if (retryTimer) return;
retryTimer = setTimeout(() => {
retryTimer = null;
if (initPromise) return;
retryDelayMs = Math.min(retryDelayMs * 2, 60_000);
void ensureInitialized("retry");
}, retryDelayMs);
}
function ensureInitialized(context?: string): Promise<void> {
if (!initPromise) {
initContexts.clear();
initPromise = doInitialize()
.then((value) => {
clearRetry();
return value;
})
.catch((error) => {
handleInitError("initialize", error);
initPromise = null;
scheduleRetry();
throw error;
});
}
if (context && !initContexts.has(context)) {
initContexts.add(context);
initPromise.catch((error) => {
handleInitError(context, error);
scheduleRetry();
});
}
return initPromise;
}
return { ensureInitialized };
}
+16 -14
View File
@@ -1,30 +1,30 @@
import type {
AsterAccountSnapshot,
AsterOrder,
AsterDepth,
AsterTicker,
AsterKline,
AccountSnapshot,
Order,
Depth,
Ticker,
Kline,
CreateOrderParams,
} from "./types";
export interface AccountListener {
(snapshot: AsterAccountSnapshot): void;
(snapshot: AccountSnapshot): void;
}
export interface OrderListener {
(orders: AsterOrder[]): void;
(orders: Order[]): void;
}
export interface DepthListener {
(depth: AsterDepth): void;
(depth: Depth): void;
}
export interface TickerListener {
(ticker: AsterTicker): void;
(ticker: Ticker): void;
}
export interface KlineListener {
(klines: AsterKline[]): void;
(klines: Kline[]): void;
}
export interface FundingRateSnapshot {
@@ -66,24 +66,26 @@ export interface ConnectionEventListener {
export interface ExchangeAdapter {
readonly id: string;
supportsTrailingStops(): boolean;
/** 是否支持交易所侧触发单(STOP_MARKET 兜底止损),缺省视为 false */
supportsTriggerOrders?(): boolean;
watchAccount(cb: AccountListener): void;
watchOrders(cb: OrderListener): void;
watchDepth(symbol: string, cb: DepthListener): void;
watchTicker(symbol: string, cb: TickerListener): void;
watchKlines(symbol: string, interval: string, cb: KlineListener): void;
watchFundingRate?(symbol: string, cb: FundingRateListener): void;
createOrder(params: CreateOrderParams): Promise<AsterOrder>;
createOrder(params: CreateOrderParams): Promise<Order>;
cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>;
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
cancelAllOrders(params: { symbol: string }): Promise<void>;
getPrecision?(): Promise<ExchangePrecision | null>;
// 连接保护相关方法(可选,仅 StandX 支持)
// Connection protection methods (optional)
onConnectionEvent?(listener: ConnectionEventListener): void;
offConnectionEvent?(listener: ConnectionEventListener): void;
onRestHealthEvent?(listener: RestHealthListener): void;
offRestHealthEvent?(listener: RestHealthListener): void;
queryOpenOrders?(): Promise<AsterOrder[]>;
queryAccountSnapshot?(): Promise<AsterAccountSnapshot | null>;
queryOpenOrders?(): Promise<Order[]>;
queryAccountSnapshot?(): Promise<AccountSnapshot | null>;
changeMarginMode?(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void>;
forceCancelAllOrders?(): Promise<boolean>;
}
-168
View File
@@ -1,168 +0,0 @@
import type {
AccountListener,
DepthListener,
ExchangeAdapter,
ExchangePrecision,
KlineListener,
OrderListener,
TickerListener,
} from "./adapter";
import type { AsterOrder, CreateOrderParams, AsterDepth, AsterTicker, AsterKline } from "./types";
import { extractMessage } from "../utils/errors";
import { AsterGateway } from "./aster/client";
export interface AsterCredentials {
apiKey?: string;
apiSecret?: string;
symbol?: string;
}
export class AsterExchangeAdapter implements ExchangeAdapter {
readonly id = "aster";
private readonly gateway: AsterGateway;
private readonly symbol: string;
private initPromise: Promise<void> | null = null;
private lastInitErrorAt = 0;
private readonly initContexts = new Set<string>();
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
constructor(credentials: AsterCredentials = {}) {
this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret });
this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase();
}
supportsTrailingStops(): boolean {
return true;
}
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[AsterExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway.ensureInitialized(this.symbol).then((value) => {
this.clearRetry();
return value;
}).catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[AsterExchangeAdapter] ${context} failed`, error);
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => {
cb(snapshot);
}));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => {
cb(orders);
}));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: AsterDepth) => {
cb(depth);
}));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: AsterTicker) => {
cb(ticker);
}));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: AsterKline[]) => {
cb(klines);
}));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) });
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList });
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
try {
const precision = await this.gateway.getPrecision(this.symbol);
if (!precision) return null;
return {
priceTick: precision.priceTick,
qtyStep: precision.qtyStep,
priceDecimals: precision.priceDecimals,
sizeDecimals: precision.sizeDecimals,
};
} catch (error) {
console.error("[AsterExchangeAdapter] getPrecision failed", error);
return null;
}
}
}
+113
View File
@@ -0,0 +1,113 @@
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;
}
supportsTriggerOrders(): boolean {
return true;
}
watchAccount(cb: AccountListener): void {
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => {
cb(snapshot);
}));
}
watchOrders(cb: OrderListener): void {
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => {
cb(orders);
}));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: Depth) => {
cb(depth);
}));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: Ticker) => {
cb(ticker);
}));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: Kline[]) => {
cb(klines);
}));
}
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) });
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList });
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
try {
const precision = await this.gateway.getPrecision(this.symbol);
if (!precision) return null;
return {
priceTick: precision.priceTick,
qtyStep: precision.qtyStep,
priceDecimals: precision.priceDecimals,
sizeDecimals: precision.sizeDecimals,
};
} catch (error) {
console.error("[AsterExchangeAdapter] getPrecision failed", error);
return null;
}
}
}
@@ -1,11 +1,16 @@
import crypto from "crypto";
import { setInterval, clearInterval, setTimeout, clearTimeout } from "timers";
import type {
AsterAccountPosition,
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AccountPosition,
AccountSnapshot,
Depth,
Kline,
Order,
Ticker,
CreateOrderParams,
PositionSide,
} from "../types";
import type {
AsterSpotAccount,
AsterSpotAggTrade,
AsterSpotBookTicker,
@@ -18,18 +23,15 @@ 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";
@@ -244,7 +246,7 @@ export class AsterSpotRestClient {
};
}
async createOrder(params: CreateSpotOrderParams): Promise<AsterOrder> {
async createOrder(params: CreateSpotOrderParams): Promise<Order> {
const response = await this.request<any>({
path: "/api/v1/order",
method: "POST",
@@ -255,7 +257,7 @@ export class AsterSpotRestClient {
return toOrderFromRest(response);
}
async cancelOrder(params: CancelSpotOrderParams): Promise<AsterOrder> {
async cancelOrder(params: CancelSpotOrderParams): Promise<Order> {
const response = await this.request<any>({
path: "/api/v1/order",
method: "DELETE",
@@ -270,7 +272,7 @@ export class AsterSpotRestClient {
return toOrderFromRest(response);
}
async getOrder(params: QuerySpotOrderParams): Promise<AsterOrder> {
async getOrder(params: QuerySpotOrderParams): Promise<Order> {
const response = await this.request<any>({
path: "/api/v1/order",
method: "GET",
@@ -285,7 +287,7 @@ export class AsterSpotRestClient {
return toOrderFromRest(response);
}
async getOpenOrders(params: SpotOpenOrdersParams = {}): Promise<AsterOrder[]> {
async getOpenOrders(params: SpotOpenOrdersParams = {}): Promise<Order[]> {
const response = await this.request<any[]>({
path: "/api/v1/openOrders",
method: "GET",
@@ -319,7 +321,7 @@ export class AsterSpotRestClient {
});
}
async getAllOrders(params: SpotAllOrdersParams): Promise<AsterOrder[]> {
async getAllOrders(params: SpotAllOrdersParams): Promise<Order[]> {
const response = await this.request<any[]>({
path: "/api/v1/allOrders",
method: "GET",
@@ -543,13 +545,13 @@ export class AsterSpotRestClient {
}
try {
return JSON.parse(text) as T;
} catch (error) {
} catch {
throw new Error(`[AsterSpotRestClient] 无法解析响应: ${text.slice(0, 200)}`);
}
}
}
function toDepth(streamSymbol: string, data: any): AsterDepth {
function toDepth(streamSymbol: string, data: any): Depth {
return {
eventType: data.e,
eventTime: data.E,
@@ -561,7 +563,7 @@ function toDepth(streamSymbol: string, data: any): AsterDepth {
};
}
function toTicker(data: any): AsterTicker {
function toTicker(data: any): Ticker {
return {
eventType: data.e,
eventTime: data.E,
@@ -584,7 +586,7 @@ function toTicker(data: any): AsterTicker {
};
}
function toKline(data: any): AsterKline {
function toKline(data: any): Kline {
return {
eventType: data.e,
eventTime: data.E,
@@ -607,7 +609,7 @@ function toKline(data: any): AsterKline {
};
}
function fromRestKline(entry: any[], interval: string, symbol: string): AsterKline {
function fromRestKline(entry: any[], interval: string, symbol: string): Kline {
return {
eventType: undefined,
eventTime: undefined,
@@ -628,7 +630,7 @@ function fromRestKline(entry: any[], interval: string, symbol: string): AsterKli
};
}
function toOrderFromRest(raw: any): AsterOrder {
function toOrderFromRest(raw: any): Order {
return {
avgPrice: raw.avgPrice ?? "0",
clientOrderId: raw.clientOrderId ?? "",
@@ -656,7 +658,7 @@ function toOrderFromRest(raw: any): AsterOrder {
};
}
function toOrderFromEvent(event: any): AsterOrder {
function toOrderFromEvent(event: any): Order {
return {
avgPrice: event.ap ?? "0",
clientOrderId: event.c ?? "",
@@ -684,7 +686,7 @@ function toOrderFromEvent(event: any): AsterOrder {
};
}
function toPositionFromRisk(raw: any): AsterAccountPosition {
function toPositionFromRisk(raw: any): AccountPosition {
const positionSide = String(raw.positionSide ?? raw.ps ?? "BOTH").toUpperCase() as PositionSide;
return {
symbol: raw.symbol ?? raw.s ?? "",
@@ -708,16 +710,16 @@ function toPositionFromRisk(raw: any): AsterAccountPosition {
};
}
function deepCloneAccount(snapshot: AsterAccountSnapshot | null): AsterAccountSnapshot | null {
function deepCloneAccount(snapshot: AccountSnapshot | null): AccountSnapshot | null {
return snapshot ? JSON.parse(JSON.stringify(snapshot)) : null;
}
function sumUnrealizedProfit(positions: AsterAccountPosition[]): string {
function sumUnrealizedProfit(positions: AccountPosition[]): string {
const total = positions.reduce((acc, position) => acc + Number(position.unrealizedProfit ?? 0), 0);
return total.toFixed(8);
}
function clonePositions(positions: AsterAccountPosition[]): AsterAccountPosition[] {
function clonePositions(positions: AccountPosition[]): AccountPosition[] {
return positions.map((position) => ({
...position,
updateTime: position.updateTime ?? Date.now(),
@@ -763,18 +765,18 @@ export class AsterRestClient {
this.apiSecret = requireEnv(options.apiSecret ?? process.env.ASTER_API_SECRET, "ASTER_API_SECRET");
}
async getAccount(): Promise<AsterAccountSnapshot> {
return this.signedRequest<AsterAccountSnapshot>({ path: "/fapi/v2/account", method: "GET", params: {} });
async getAccount(): Promise<AccountSnapshot> {
return this.signedRequest<AccountSnapshot>({ path: "/fapi/v2/account", method: "GET", params: {} });
}
async getOpenOrders(symbol?: string): Promise<AsterOrder[]> {
async getOpenOrders(symbol?: string): Promise<Order[]> {
const params: Record<string, unknown> = {};
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<AsterAccountPosition[]> {
async getPositions(symbol?: string): Promise<AccountPosition[]> {
const params: Record<string, unknown> = {};
if (symbol) params.symbol = symbol.toUpperCase();
const raw = await this.signedRequest<any[]>({ path: "/fapi/v2/positionRisk", method: "GET", params });
@@ -795,12 +797,12 @@ export class AsterRestClient {
}
try {
return JSON.parse(text) as AsterFuturesExchangeInfo;
} catch (error) {
} catch {
throw new Error(`[AsterRestClient] 无法解析交易规则响应: ${text.slice(0, 200)}`);
}
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
// Sanitize and normalize params for Aster futures API. Paradex-specific flags
// like reduceOnly/closePosition on STOP/TRAILING should not leak here.
const payload: Record<string, unknown> = {};
@@ -813,6 +815,7 @@ 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.
@@ -830,12 +833,12 @@ export class AsterRestClient {
return toOrderFromRest(response);
}
async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<AsterOrder> {
async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<Order> {
const response = await this.signedRequest<any>({ path: "/fapi/v1/order", method: "DELETE", params });
return toOrderFromRest(response);
}
async cancelOrders(params: { symbol: string; orderIdList?: Array<number | string>; origClientOrderIdList?: string[] }): Promise<AsterOrder[]> {
async cancelOrders(params: { symbol: string; orderIdList?: Array<number | string>; origClientOrderIdList?: string[] }): Promise<Order[]> {
const payload: Record<string, unknown> = { symbol: params.symbol };
if (params.orderIdList?.length) {
payload.orderIdList = `[${params.orderIdList
@@ -853,7 +856,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<AsterKline[]> {
async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise<Kline[]> {
const upper = symbol.toUpperCase();
const url = `${FUTURES_REST_BASE}/fapi/v1/continuousKlines?pair=${upper}&contractType=PERPETUAL&interval=${encodeURIComponent(interval)}&limit=${limit}`;
let response: Response;
@@ -869,7 +872,7 @@ export class AsterRestClient {
try {
const payload = JSON.parse(text) as any[];
return payload.map((entry) => fromRestKline(entry, interval, upper));
} catch (error) {
} catch {
throw new Error(`[AsterRestClient] 无法解析K线响应: ${text.slice(0, 200)}`);
}
}
@@ -899,7 +902,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 (error) {
} catch {
throw new Error(`[AsterRestClient] 无法解析资金费率响应: ${text.slice(0, 200)}`);
}
}
@@ -942,16 +945,16 @@ export class AsterRestClient {
}
try {
return JSON.parse(text) as T;
} catch (error) {
} catch {
throw new Error(`[AsterRestClient] 无法解析响应: ${text.slice(0, 200)}`);
}
}
}
type DepthHandler = (depth: AsterDepth) => void;
type TickerHandler = (ticker: AsterTicker) => void;
type KlineHandler = (kline: AsterKline) => void;
type DepthHandler = (depth: Depth) => void;
type TickerHandler = (ticker: Ticker) => void;
type KlineHandler = (kline: Kline) => void;
type StreamKind = "depth" | "ticker" | "kline";
@@ -1244,7 +1247,7 @@ export class AsterUserStream {
}
}
function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AsterAccountSnapshot | null {
function updateAccountSnapshot(snapshot: AccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AccountSnapshot | null {
if (!snapshot) return snapshot;
const next = deepCloneAccount(snapshot);
if (!next) return snapshot;
@@ -1294,7 +1297,7 @@ function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { e
return next;
}
function mergeOrderSnapshot(map: Map<string, AsterOrder>, order: AsterOrder): void {
function mergeOrderSnapshot(map: Map<string, Order>, order: Order): void {
const rawId = order.orderId;
if (rawId === undefined || rawId === null) return;
const key = String(rawId);
@@ -1311,18 +1314,18 @@ export class AsterGateway {
private readonly publicStreams: AsterPublicStreams;
private readonly userStream: AsterUserStream;
private accountSnapshot: AsterAccountSnapshot | null = null;
private readonly openOrders = new Map<string, AsterOrder>();
private accountSnapshot: AccountSnapshot | null = null;
private readonly openOrders = new Map<string, Order>();
private positionSyncTimer: ReturnType<typeof setInterval> | null = null;
private positionSyncInFlight = false;
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 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 klineStores = new Map<string, AsterKline[]>();
private readonly klineStores = new Map<string, Kline[]>();
private readonly klineRefreshTimers = new Map<string, ReturnType<typeof setInterval>>();
private readonly klineInitialFetches = new Map<string, Promise<void>>();
private initialized = false;
@@ -1366,7 +1369,7 @@ export class AsterGateway {
});
}
async ensureInitialized(symbol: string): Promise<void> {
async ensureInitialized(_symbol: string): Promise<void> {
if (this.initialized) return;
if (this.initializing) return this.initializing;
this.initializing = (async () => {
@@ -1381,21 +1384,21 @@ export class AsterGateway {
return this.initializing;
}
onAccount(listener: (snapshot: AsterAccountSnapshot) => void): void {
onAccount(listener: (snapshot: AccountSnapshot) => void): void {
this.accountEvent.add(listener);
if (this.accountSnapshot) listener(this.accountSnapshot);
}
onOrders(listener: (orders: AsterOrder[]) => void): void {
onOrders(listener: (orders: Order[]) => void): void {
this.ordersEvent.add(listener);
listener(Array.from(this.openOrders.values()));
}
onDepth(symbol: string, listener: (depth: AsterDepth) => void): void {
onDepth(symbol: string, listener: (depth: Depth) => void): void {
const upper = symbol.toUpperCase();
let event = this.depthEvents.get(upper);
if (!event) {
event = new SimpleEvent<AsterDepth>();
event = new SimpleEvent<Depth>();
this.depthEvents.set(upper, event);
this.publicStreams.subscribeDepth(upper, (depth) => {
event?.emit(depth);
@@ -1404,11 +1407,11 @@ export class AsterGateway {
event.add(listener);
}
onTicker(symbol: string, listener: (ticker: AsterTicker) => void): void {
onTicker(symbol: string, listener: (ticker: Ticker) => void): void {
const upper = symbol.toUpperCase();
let event = this.tickerEvents.get(upper);
if (!event) {
event = new SimpleEvent<AsterTicker>();
event = new SimpleEvent<Ticker>();
this.tickerEvents.set(upper, event);
this.publicStreams.subscribeTicker(upper, (ticker) => {
event?.emit(ticker);
@@ -1417,12 +1420,12 @@ export class AsterGateway {
event.add(listener);
}
onKlines(symbol: string, interval: string, listener: (klines: AsterKline[]) => void): void {
onKlines(symbol: string, interval: string, listener: (klines: Kline[]) => void): void {
const upper = symbol.toUpperCase();
const key = `${upper}:${interval}`;
let event = this.klineEvents.get(key);
if (!event) {
event = new SimpleEvent<AsterKline[]>();
event = new SimpleEvent<Kline[]>();
this.klineEvents.set(key, event);
this.publicStreams.subscribeKline(symbol, interval, (kline) => {
const storeKey = `${upper}:${interval}`;
@@ -1509,7 +1512,7 @@ export class AsterGateway {
console.error("[AsterGateway] 刷新持仓失败", positionError);
}
const normalizedPositions = clonePositions(positions);
const snapshot: AsterAccountSnapshot = {
const snapshot: AccountSnapshot = {
...account,
positions: normalizedPositions,
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
@@ -1547,7 +1550,7 @@ export class AsterGateway {
if (!Array.isArray(positions)) return;
const normalizedPositions = clonePositions(positions);
if (!this.accountSnapshot) {
const snapshot: AsterAccountSnapshot = {
const snapshot: AccountSnapshot = {
canTrade: true,
canDeposit: true,
canWithdraw: true,
@@ -1561,7 +1564,7 @@ export class AsterGateway {
this.accountEvent.emit(snapshot);
return;
}
const nextSnapshot: AsterAccountSnapshot = {
const nextSnapshot: AccountSnapshot = {
...this.accountSnapshot,
positions: normalizedPositions,
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
@@ -1576,15 +1579,15 @@ export class AsterGateway {
}
}
getAccountSnapshot(): AsterAccountSnapshot | null {
getAccountSnapshot(): AccountSnapshot | null {
return this.accountSnapshot;
}
getOpenOrdersSnapshot(): AsterOrder[] {
getOpenOrdersSnapshot(): Order[] {
return Array.from(this.openOrders.values());
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
const normalized = await this.normalizeOrderParams(params);
const order = await this.rest.createOrder(normalized);
mergeOrderSnapshot(this.openOrders, order);
+14 -98
View File
@@ -1,101 +1,17 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
import { createOrderHandlers } from "../order-handlers";
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;
}
const handlers = createOrderHandlers({
exchangeName: "Aster",
defaultLimitTimeInForce: "GTX",
supportsTrailingStop: true,
supportsTriggerType: true,
});
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);
}
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+248
View File
@@ -0,0 +1,248 @@
import type { DepthLevel, OrderSide, OrderType, TimeInForce } from "../types";
export interface AsterSpotRateLimit {
rateLimitType: string;
interval: string;
intervalNum: number;
limit: number;
}
export interface AsterSpotExchangeFilter {
filterType: string;
[key: string]: string | number | boolean | undefined;
}
export interface AsterFuturesSymbolFilter {
filterType: string;
tickSize?: string;
stepSize?: string;
minPrice?: string;
maxPrice?: string;
minQty?: string;
maxQty?: string;
[key: string]: string | number | boolean | undefined;
}
export interface AsterFuturesSymbolInfo {
symbol: string;
pair?: string;
contractType?: string;
pricePrecision?: number;
quantityPrecision?: number;
baseAssetPrecision?: number;
quotePrecision?: number;
underlyingType?: string;
filters?: AsterFuturesSymbolFilter[];
}
export interface AsterFuturesExchangeInfo {
timezone?: string;
serverTime?: number;
symbols?: AsterFuturesSymbolInfo[];
}
export interface AsterSpotAssetInfo {
asset: string;
}
export interface AsterSpotSymbolInfo {
symbol: string;
status: string;
baseAsset: string;
quoteAsset: string;
baseAssetPrecision?: number;
quotePrecision?: number;
pricePrecision?: number;
quantityPrecision?: number;
orderTypes: string[];
timeInForce: string[];
ocoAllowed: boolean;
filters: AsterSpotExchangeFilter[];
}
export interface AsterSpotExchangeInfo {
timezone: string;
serverTime: number;
rateLimits: AsterSpotRateLimit[];
exchangeFilters: AsterSpotExchangeFilter[];
assets?: AsterSpotAssetInfo[];
symbols: AsterSpotSymbolInfo[];
}
export interface AsterSpotDepth {
lastUpdateId: number;
E?: number;
T?: number;
bids: DepthLevel[];
asks: DepthLevel[];
}
export interface AsterSpotTrade {
id: number;
price: string;
qty: string;
baseQty?: string;
quoteQty?: string;
time: number;
isBuyerMaker: boolean;
}
export interface AsterSpotHistoricalTrade extends AsterSpotTrade {
isBestMatch?: boolean;
}
export interface AsterSpotAggTrade {
a: number;
p: string;
q: string;
f: number;
l: number;
T: number;
m: boolean;
M?: boolean;
}
export interface AsterSpotKline {
openTime: number;
open: string;
high: string;
low: string;
close: string;
volume: string;
closeTime: number;
quoteAssetVolume: string;
numberOfTrades: number;
takerBuyBaseAssetVolume: string;
takerBuyQuoteAssetVolume: string;
}
export interface AsterSpotTicker24h {
symbol: string;
priceChange: string;
priceChangePercent: string;
weightedAvgPrice: string;
prevClosePrice: string;
lastPrice: string;
lastQty: string;
bidPrice: string;
bidQty: string;
askPrice: string;
askQty: string;
openPrice: string;
highPrice: string;
lowPrice: string;
volume: string;
quoteVolume: string;
openTime: number;
closeTime: number;
firstId: number;
lastId: number;
count: number;
baseAsset?: string;
quoteAsset?: string;
}
export interface AsterSpotPriceTicker {
symbol: string;
price: string;
time?: number;
}
export interface AsterSpotBookTicker {
symbol: string;
bidPrice: string;
bidQty: string;
askPrice: string;
askQty: string;
time?: number;
}
export interface AsterSpotCommissionRate {
symbol: string;
makerCommissionRate: string;
takerCommissionRate: string;
}
export interface CreateSpotOrderParams {
symbol: string;
side: OrderSide;
type: OrderType;
timeInForce?: TimeInForce;
quantity?: number | string;
quoteOrderQty?: number | string;
price?: number | string;
newClientOrderId?: string;
stopPrice?: number | string;
recvWindow?: number;
}
export interface CancelSpotOrderParams {
symbol: string;
orderId?: number | string;
origClientOrderId?: string;
recvWindow?: number;
}
export interface QuerySpotOrderParams extends CancelSpotOrderParams {}
export interface SpotOpenOrdersParams {
symbol?: string;
recvWindow?: number;
orderIdList?: Array<number | string>;
origClientOrderIdList?: string[];
}
export interface SpotAllOrdersParams {
symbol: string;
orderId?: number;
startTime?: number;
endTime?: number;
limit?: number;
recvWindow?: number;
}
export interface AsterSpotAccountBalance {
asset: string;
free: string;
locked: string;
}
export interface AsterSpotAccount {
feeTier: number;
canTrade: boolean;
canDeposit: boolean;
canWithdraw: boolean;
canBurnAsset?: boolean;
updateTime: number;
makerCommission?: string;
takerCommission?: string;
buyerCommission?: string;
sellerCommission?: string;
balances: AsterSpotAccountBalance[];
}
export interface SpotUserTradesParams {
symbol?: string;
orderId?: number;
startTime?: number;
endTime?: number;
fromId?: number;
limit?: number;
recvWindow?: number;
}
export interface AsterSpotUserTrade {
symbol: string;
id: number;
orderId: number;
side: OrderSide;
price: string;
qty: string;
quoteQty?: string;
commission: string;
commissionAsset: string;
time: number;
counterpartyId?: number;
maker: boolean;
buyer: boolean;
}
+17 -84
View File
@@ -6,9 +6,10 @@ import type {
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import type { Order, 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;
@@ -23,11 +24,8 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
readonly id = "backpack";
private readonly gateway: BackpackGateway;
private readonly symbol: string;
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 readonly safeInvoke = createSafeInvoke("BackpackExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
constructor(credentials: BackpackCredentials = {}) {
const apiKey = credentials.apiKey ?? process.env.BACKPACK_API_KEY;
@@ -54,6 +52,9 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
this.gateway = new BackpackGateway(gatewayOptions);
this.symbol = symbol;
this.init = createInitManager("BackpackExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
}
supportsTrailingStops(): boolean {
@@ -61,118 +62,50 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(_symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth");
void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
}
watchTicker(_symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker");
void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
}
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized(`watchKlines:${interval}`);
void this.init.ensureInitialized(`watchKlines:${interval}`);
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
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.ensureInitialized("cancelOrder");
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder({ orderId: params.orderId });
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
}
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.init.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)}`);
+43 -46
View File
@@ -7,14 +7,13 @@ 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 {
AsterAccountSnapshot,
AsterAccountPosition,
AsterOrder,
AsterDepth,
AsterTicker,
AsterKline,
AccountSnapshot,
AccountPosition,
Order,
Depth,
Ticker,
Kline,
CreateOrderParams,
OrderType,
} from "../types";
@@ -93,8 +92,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, AsterOrder>();
private lastBalanceSnapshot: AsterAccountSnapshot | null = null;
private readonly localOrders = new Map<string, Order>();
private lastBalanceSnapshot: AccountSnapshot | null = null;
private marketId = "";
private ws: WebSocket | null = null;
@@ -137,17 +136,16 @@ export class BackpackGateway {
}
private async doInitialize(symbol?: string): Promise<void> {
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);
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,
@@ -155,10 +153,7 @@ export class BackpackGateway {
marketId: this.marketId,
});
}
this.initialized = true;
} catch (error) {
throw error;
}
this.initialized = true;
}
private findMarket(requested: string): any | null {
@@ -249,7 +244,7 @@ export class BackpackGateway {
this.exchange.fetchOpenOrders(this.marketSymbol),
this.exchange.fetchOrders(this.marketSymbol, undefined, 200, {}),
]);
const active = new Map<string, AsterOrder>();
const active = new Map<string, Order>();
for (const entry of [...openOrders, ...allOrders]) {
const status = this.normalizeStatus(entry.status ?? (entry.info?.status as string));
if (this.isTerminalStatus(status)) continue;
@@ -320,7 +315,7 @@ export class BackpackGateway {
// ---- Order actions -----------------------------------------------------
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized();
const symbol = this.marketSymbol;
const normalizedType = this.normalizeOrderType(params.type);
@@ -404,11 +399,11 @@ export class BackpackGateway {
// ---- Mapping helpers ---------------------------------------------------
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
private mapBalanceToAccountSnapshot(balance: Balances): AccountSnapshot {
return this.mapBalanceToAccountSnapshotWithPositions(balance, []);
}
private async fetchAccountSnapshot(): Promise<AsterAccountSnapshot> {
private async fetchAccountSnapshot(): Promise<AccountSnapshot> {
await this.ensureInitialized();
const [balance, positions] = await Promise.all([
this.exchange.fetchBalance(),
@@ -422,7 +417,7 @@ export class BackpackGateway {
return this.mapBalanceToAccountSnapshotWithPositions(balance, positions ?? []);
}
private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AsterAccountSnapshot {
private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AccountSnapshot {
const now = Date.now();
const assets = this.normalizeAssets(balance, now);
const positions = this.normalizePositions(rawPositions, now);
@@ -430,7 +425,7 @@ export class BackpackGateway {
const totalUnrealized = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0"));
const availableBalance = this.sumStrings(assets.map((asset) => asset.availableBalance));
const snapshot: AsterAccountSnapshot = {
const snapshot: AccountSnapshot = {
canTrade: true,
canDeposit: true,
canWithdraw: true,
@@ -452,9 +447,9 @@ export class BackpackGateway {
return snapshot;
}
private normalizeAssets(balance: Balances, now: number): AsterAccountSnapshot["assets"] {
private normalizeAssets(balance: Balances, now: number): AccountSnapshot["assets"] {
const metaKeys = new Set(["free", "used", "total", "info", "timestamp", "datetime", "debt"]);
const assets: AsterAccountSnapshot["assets"] = [];
const assets: AccountSnapshot["assets"] = [];
for (const [currency, value] of Object.entries(balance)) {
if (metaKeys.has(currency)) continue;
if (!value || typeof value !== "object") continue;
@@ -465,27 +460,29 @@ export class BackpackGateway {
return assets;
}
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] {
private normalizePositions(rawPositions: any[], now: number): AccountSnapshot["positions"] {
if (!Array.isArray(rawPositions)) return [];
const positions: AsterAccountSnapshot["positions"] = [];
const positions: AccountSnapshot["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: this.symbol,
symbol,
positionAmt: positionAmt.toString(),
entryPrice,
unrealizedProfit: unrealized,
positionSide: "BOTH",
positionSide: isLong ? "LONG" : isShort ? "SHORT" : "BOTH",
updateTime: now,
markPrice,
liquidationPrice,
@@ -496,7 +493,7 @@ export class BackpackGateway {
return positions;
}
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
private mapOrderBookToDepth(orderbook: CcxtOrderBook): Depth {
return {
lastUpdateId: orderbook.nonce || Date.now(),
bids: (orderbook.bids ?? [])
@@ -509,7 +506,7 @@ export class BackpackGateway {
};
}
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
private mapTickerToAsterTicker(ticker: CcxtTicker): Ticker {
return {
symbol: ticker.symbol,
lastPrice: ticker.last?.toString() ?? "0",
@@ -525,7 +522,7 @@ export class BackpackGateway {
private mapOHLCVToKline(
candle: [number, number, number, number, number, number],
interval: string
): AsterKline {
): Kline {
const [openTime, open, high, low, close, volume] = candle;
return {
openTime,
@@ -539,7 +536,7 @@ export class BackpackGateway {
};
}
private mapRestOrder(order: CcxtOrder): AsterOrder {
private mapRestOrder(order: CcxtOrder): Order {
const info = (order.info ?? {}) as Record<string, unknown>;
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
let type = this.normalizeOrderType(order.type ?? (info.o as string)) as OrderType;
@@ -582,7 +579,7 @@ export class BackpackGateway {
};
}
private mapWsOrder(data: Record<string, unknown>): AsterOrder {
private mapWsOrder(data: Record<string, unknown>): Order {
const sideRaw = String(data.S ?? "").toUpperCase();
const side: "BUY" | "SELL" = sideRaw === "BID" ? "BUY" : "SELL";
const triggerPresent = data.P != null || data.B != null;
@@ -629,7 +626,7 @@ export class BackpackGateway {
}
}
private mapWsPosition(data: Record<string, unknown>): AsterAccountPosition | null {
private mapWsPosition(data: Record<string, unknown>): AccountPosition | null {
const quantityRaw = data.q ?? data.Q;
const qty = Number(this.toStringAmount(quantityRaw));
if (!Number.isFinite(qty)) return null;
@@ -654,8 +651,8 @@ export class BackpackGateway {
};
}
private mergeWsPosition(position: AsterAccountPosition): void {
const snapshot: AsterAccountSnapshot = this.lastBalanceSnapshot
private mergeWsPosition(position: AccountPosition): void {
const snapshot: AccountSnapshot = this.lastBalanceSnapshot
? {
...this.lastBalanceSnapshot,
positions: this.lastBalanceSnapshot.positions ? [...this.lastBalanceSnapshot.positions] : [],
@@ -695,7 +692,7 @@ export class BackpackGateway {
this.emitAccount(snapshot);
}
private emitAccount(snapshot: AsterAccountSnapshot): void {
private emitAccount(snapshot: AccountSnapshot): void {
for (const listener of this.accountListeners) {
try {
listener(snapshot);
@@ -772,7 +769,7 @@ export class BackpackGateway {
void this.resubscribeAllTopics();
};
private handleWsClose = (event: any): void => {
private handleWsClose = (_event: any): void => {
if (this.wsCleanup) {
try {
this.wsCleanup();
+14 -82
View File
@@ -1,84 +1,16 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
import { createOrderHandlers } from "../order-handlers";
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;
}
const handlers = createOrderHandlers({
exchangeName: "Backpack",
defaultLimitTimeInForce: "GTX",
supportsTrailingStop: false,
supportsTriggerType: false,
});
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);
}
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+34 -100
View File
@@ -1,4 +1,3 @@
import { clearTimeout, setTimeout } from "timers";
import type {
AccountListener,
DepthListener,
@@ -9,9 +8,9 @@ import type {
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import type { Order, CreateOrderParams } from "../types";
import { BinanceGateway, type BinanceGatewayOptions } from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
export interface BinanceCredentials {
apiKey?: string;
@@ -32,11 +31,8 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
private readonly gateway: BinanceGateway;
private readonly symbol: string;
private readonly marketType: "spot" | "perp" | "auto";
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 readonly safeInvoke = createSafeInvoke("BinanceExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
constructor(credentials: BinanceCredentials = {}) {
const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY;
@@ -61,171 +57,109 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
futuresWsUrl: credentials.futuresWsUrl,
logger: credentials.logger,
});
this.init = createInitManager("BinanceExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
}
supportsTrailingStops(): boolean {
return this.marketType !== "spot";
}
supportsTriggerOrders(): boolean {
return this.marketType !== "spot";
}
watchAccount(cb: AccountListener): void {
const safe = this.safeInvoke("watchAccount", cb);
void this.ensureInitialized("watchAccount")
void this.init.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.ensureInitialized("watchOrders")
void this.init.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.ensureInitialized(`watchDepth:${symbol}`)
void this.init.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.ensureInitialized(`watchTicker:${symbol}`)
void this.init.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.ensureInitialized(`watchKlines:${symbol}:${interval}`)
void this.init.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.ensureInitialized(`watchFundingRate:${symbol}`)
void this.init.ensureInitialized(`watchFundingRate:${symbol}`)
.then(() => {
this.gateway.onFundingRate(symbol, safe);
})
.catch((error) => this.handleInitError("watchFundingRate", error));
});
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
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.ensureInitialized("cancelOrder");
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
await this.ensureInitialized("getPrecision");
await this.init.ensureInitialized("getPrecision");
return this.gateway.getPrecision(this.symbol);
}
async queryOpenOrders(): Promise<AsterOrder[]> {
await this.ensureInitialized("queryOpenOrders");
async queryOpenOrders(): Promise<Order[]> {
await this.init.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders();
}
async queryAccountSnapshot() {
await this.ensureInitialized("queryAccountSnapshot");
await this.init.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot();
}
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
await this.ensureInitialized("changeMarginMode");
await this.init.ensureInitialized("changeMarginMode");
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
}
async forceCancelAllOrders(): Promise<boolean> {
await this.ensureInitialized("forceCancelAllOrders");
await this.init.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);
}
}
+37 -37
View File
@@ -1,21 +1,18 @@
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 {
AsterAccountAsset,
AsterAccountPosition,
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountAsset,
AccountPosition,
AccountSnapshot,
Depth,
Kline,
Order,
Ticker,
CreateOrderParams,
OrderType,
PositionSide,
@@ -228,9 +225,9 @@ export class BinanceGateway {
private readonly accountListeners = new Set<AccountListener>();
private readonly orderListeners = new Set<OrderListener>();
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 depthSubs = new Map<string, PublicSubscription<Depth>>();
private readonly tickerSubs = new Map<string, PublicSubscription<Ticker>>();
private readonly klineSubs = new Map<string, PublicSubscription<Kline[]>>();
private readonly fundingSubs = new Map<string, PublicSubscription<{ symbol: string; fundingRate: number; updateTime: number }>>();
private accountPollTimer: ReturnType<typeof setInterval> | null = null;
@@ -257,15 +254,15 @@ export class BinanceGateway {
},
};
private readonly localOrders = new Map<string, AsterOrder>();
private readonly localOrders = new Map<string, Order>();
private readonly spotBalances = new Map<string, { free: number; locked: number }>();
private readonly perpBalances = new Map<string, { wallet: number; available: number }>();
private readonly perpPositions = new Map<string, AsterAccountPosition>();
private readonly perpPositions = new Map<string, AccountPosition>();
private readonly lastMarkPriceBySymbol = new Map<string, number>();
private lastSpotSnapshot: AsterAccountSnapshot | null = null;
private lastPerpSnapshot: AsterAccountSnapshot | null = null;
private lastSpotSnapshot: AccountSnapshot | null = null;
private lastPerpSnapshot: AccountSnapshot | null = null;
constructor(options: BinanceGatewayOptions) {
this.apiKey = options.apiKey;
@@ -468,7 +465,7 @@ export class BinanceGateway {
});
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized(params.symbol);
const market = this.resolveMarket(params.symbol);
const exchange = this.getExchange(market.kind);
@@ -490,6 +487,9 @@ 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<AsterOrder[]> {
async queryOpenOrders(): Promise<Order[]> {
await this.ensureInitialized(this.defaultSymbol);
const kinds = this.getPrivateKinds();
const result: AsterOrder[] = [];
const result: Order[] = [];
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<AsterAccountSnapshot | null> {
async queryAccountSnapshot(): Promise<AccountSnapshot | 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): AsterOrder | null {
private mapSpotExecutionReport(payload: any): Order | 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): AsterOrder | null {
private mapPerpOrderTradeUpdate(payload: any): Order | 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): AsterDepth | null {
private mapDepthPayload(payload: unknown, market: BinanceMarketRef): Depth | 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): AsterTicker | null {
private mapTickerPayload(payload: unknown, market: BinanceMarketRef): Ticker | 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): AsterKline[] | null {
private mapKlinePayload(payload: unknown, interval: string): Kline[] | 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, AsterAccountPosition>();
const next = new Map<string, AccountPosition>();
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, AsterOrder>();
const remote = new Map<string, Order>();
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: AsterOrder, kind: MarketKind, marketId: string): void {
private upsertOrder(order: Order, 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: AsterOrder): boolean {
private isOrderActive(order: Order): 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(): AsterAccountSnapshot | null {
private buildCombinedAccountSnapshot(): AccountSnapshot | 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: AsterAccountAsset[] = perp.assets.map((asset) => ({
const perpAssetsTagged: AccountAsset[] = perp.assets.map((asset) => ({
...asset,
asset: `${asset.asset}0`,
}));
@@ -1444,8 +1444,8 @@ export class BinanceGateway {
};
}
private buildSpotSnapshot(): AsterAccountSnapshot {
const assets: AsterAccountAsset[] = [];
private buildSpotSnapshot(): AccountSnapshot {
const assets: AccountAsset[] = [];
let totalWallet = 0;
const now = Date.now();
for (const [asset, balance] of this.spotBalances.entries()) {
@@ -1474,9 +1474,9 @@ export class BinanceGateway {
};
}
private buildPerpSnapshot(): AsterAccountSnapshot {
private buildPerpSnapshot(): AccountSnapshot {
const now = Date.now();
const assets: AsterAccountAsset[] = [];
const assets: AccountAsset[] = [];
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): AsterOrder {
private mapCcxtOrder(order: CcxtOrder, kind: MarketKind, marketId: string): Order {
const symbol = this.resolveDisplaySymbol(kind, marketId);
const side = String(order.side ?? "buy").toUpperCase() === "SELL" ? "SELL" : "BUY";
return {
+14 -98
View File
@@ -1,100 +1,16 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
import { createOrderHandlers } from "../order-handlers";
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;
}
const handlers = createOrderHandlers({
exchangeName: "Binance",
defaultLimitTimeInForce: "GTX",
supportsTrailingStop: true,
supportsTriggerType: true,
});
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);
}
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+14 -1
View File
@@ -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,6 +7,7 @@ import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapt
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
import { StandxExchangeAdapter, type StandxCredentials } from "./standx/adapter";
import { BinanceExchangeAdapter, type BinanceCredentials } from "./binance/adapter";
import { OndoperpsExchangeAdapter, type OndoperpsCredentials } from "./ondoperps/adapter";
export const SUPPORTED_EXCHANGE_IDS = [
"aster",
@@ -17,6 +18,7 @@ export const SUPPORTED_EXCHANGE_IDS = [
"nado",
"standx",
"binance",
"ondoperps",
] as const;
export const BASIS_SUPPORTED_EXCHANGE_IDS = [
@@ -37,6 +39,8 @@ export interface ExchangeFactoryOptions {
nado?: NadoCredentials;
standx?: StandxCredentials;
binance?: BinanceCredentials;
ondoperps?: OndoperpsCredentials;
ondoperp?: OndoperpsCredentials;
}
export type SupportedExchangeId = (typeof SUPPORTED_EXCHANGE_IDS)[number];
@@ -51,6 +55,7 @@ const EXCHANGE_DISPLAY_NAME: Record<SupportedExchangeId, string> = {
nado: "Nado",
standx: "StandX",
binance: "Binance",
ondoperps: "Ondo Perps",
};
const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
@@ -63,6 +68,9 @@ const EXCHANGE_ALIAS_MAP: Record<string, SupportedExchangeId> = {
standx: "standx",
binance: "binance",
bnb: "binance",
ondoperps: "ondoperps",
ondoperp: "ondoperps",
ondo: "ondoperps",
};
export function isSupportedExchangeId(value: string): value is SupportedExchangeId {
@@ -104,5 +112,10 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
case "binance":
return new BinanceExchangeAdapter({ ...options.binance, symbol: options.symbol });
case "ondoperps":
return new OndoperpsExchangeAdapter({
...(options.ondoperps ?? options.ondoperp),
symbol: options.symbol,
});
}
}
+167
View File
@@ -0,0 +1,167 @@
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();
}
supportsTriggerOrders(): boolean {
return this.inner.supportsTriggerOrders?.() ?? false;
}
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);
}
+21 -83
View File
@@ -1,4 +1,3 @@
import { setTimeout, clearTimeout } from "timers";
import path from "path";
import { createRequire } from "module";
import type {
@@ -9,8 +8,7 @@ import type {
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import type { Order, CreateOrderParams } from "../types";
import {
GrvtGateway,
type GrvtEnvironment,
@@ -18,6 +16,7 @@ import {
type GrvtHostsOverride,
type GrvtSignatureProvider,
} from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
export interface GrvtCredentials {
cookie?: string;
@@ -40,11 +39,8 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
private readonly gateway: GrvtGateway;
private readonly symbol: string;
private readonly instrument: string;
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 readonly safeInvoke = createSafeInvoke("GrvtExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
private klineInterval = "1m";
constructor(credentials: GrvtCredentials = {}) {
@@ -91,122 +87,64 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
pollIntervals: credentials.pollIntervals,
logger: credentials.logger,
});
this.init = createInitManager("GrvtExchangeAdapter", () =>
this.gateway.ensureInitialized(this.klineInterval),
);
}
supportsTrailingStops(): boolean {
return false;
}
supportsTriggerOrders(): boolean {
return true;
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(_symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth");
void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
}
watchTicker(_symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker");
void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
}
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
this.klineInterval = interval ?? this.klineInterval;
void this.ensureInitialized("watchKlines", this.klineInterval);
void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(this.safeInvoke("watchKlines", cb));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
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.ensureInitialized("cancelOrder");
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.init.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 {
+52 -50
View File
@@ -23,21 +23,23 @@ import type {
IOrder,
} from "@grvt/client/interfaces";
import type {
AsterAccountSnapshot,
AsterAccountPosition,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountSnapshot,
AccountPosition,
Depth,
Kline,
Order,
Ticker,
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;
@@ -172,7 +174,7 @@ interface HostsConfig {
interface KlineCache {
interval: string;
values: AsterKline[];
values: Kline[];
}
interface InstrumentInfo {
@@ -215,12 +217,12 @@ export class GrvtGateway {
private instrumentInfo: InstrumentInfo | null = null;
private sessionPromise: Promise<void> | null = null;
private accountSnapshot: AsterAccountSnapshot | null = null;
private openOrders: AsterOrder[] = [];
private positions: AsterAccountPosition[] = [];
private accountSnapshot: AccountSnapshot | null = null;
private openOrders: Order[] = [];
private positions: AccountPosition[] = [];
private lastPositionsUpdateAt: number = 0;
private depthSnapshot: AsterDepth | null = null;
private tickerSnapshot: AsterTicker | null = null;
private depthSnapshot: Depth | null = null;
private tickerSnapshot: Ticker | null = null;
private klineCache: KlineCache | null = null;
// WebSocket state
@@ -228,11 +230,11 @@ export class GrvtGateway {
private wsConnected = false;
private positionsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
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 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 accountTimer: ReturnType<typeof setInterval> | null = null;
private ordersTimer: ReturnType<typeof setInterval> | null = null;
@@ -316,7 +318,7 @@ export class GrvtGateway {
this.initialized = true;
}
onAccount(listener: (snapshot: AsterAccountSnapshot) => void): () => void {
onAccount(listener: (snapshot: AccountSnapshot) => void): () => void {
this.accountListeners.add(listener);
if (this.accountSnapshot) listener(cloneAccount(this.accountSnapshot));
return () => {
@@ -324,7 +326,7 @@ export class GrvtGateway {
};
}
onOrders(listener: (orders: AsterOrder[]) => void): () => void {
onOrders(listener: (orders: Order[]) => void): () => void {
this.ordersListeners.add(listener);
if (this.openOrders.length) listener(cloneOrders(this.openOrders));
return () => {
@@ -332,7 +334,7 @@ export class GrvtGateway {
};
}
onDepth(listener: (depth: AsterDepth) => void): () => void {
onDepth(listener: (depth: Depth) => void): () => void {
this.depthListeners.add(listener);
if (this.depthSnapshot) listener(cloneDepth(this.depthSnapshot));
return () => {
@@ -340,7 +342,7 @@ export class GrvtGateway {
};
}
onTicker(listener: (ticker: AsterTicker) => void): () => void {
onTicker(listener: (ticker: Ticker) => void): () => void {
this.tickerListeners.add(listener);
if (this.tickerSnapshot) listener(cloneTicker(this.tickerSnapshot));
return () => {
@@ -348,7 +350,7 @@ export class GrvtGateway {
};
}
onKlines(listener: (klines: AsterKline[]) => void): () => void {
onKlines(listener: (klines: Kline[]) => void): () => void {
this.klineListeners.add(listener);
if (this.klineCache) listener(cloneKlines(this.klineCache.values));
return () => {
@@ -356,19 +358,19 @@ export class GrvtGateway {
};
}
getAccountSnapshot(): AsterAccountSnapshot | null {
getAccountSnapshot(): AccountSnapshot | null {
return this.accountSnapshot ? cloneAccount(this.accountSnapshot) : null;
}
getOpenOrders(): AsterOrder[] {
getOpenOrders(): Order[] {
return cloneOrders(this.openOrders);
}
getPositions(): AsterAccountPosition[] {
getPositions(): AccountPosition[] {
return this.positions.map((position) => ({ ...position }));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
if (params.type === "TRAILING_STOP_MARKET") {
throw new Error(TRAILING_NOT_SUPPORTED_ERROR);
}
@@ -660,7 +662,7 @@ export class GrvtGateway {
this.emitKlines(klines);
}
private mergeOrder(order: AsterOrder): void {
private mergeOrder(order: Order): void {
const index = this.openOrders.findIndex((item) => String(item.orderId) === String(order.orderId));
if (index >= 0) {
this.openOrders[index] = order;
@@ -678,7 +680,7 @@ export class GrvtGateway {
}
}
private emitAccount(snapshot: AsterAccountSnapshot): void {
private emitAccount(snapshot: AccountSnapshot): void {
const cloned = cloneAccount(snapshot);
this.accountListeners.forEach((listener) => {
try {
@@ -689,7 +691,7 @@ export class GrvtGateway {
});
}
private emitOrders(orders: AsterOrder[]): void {
private emitOrders(orders: Order[]): void {
const cloned = cloneOrders(orders);
this.ordersListeners.forEach((listener) => {
try {
@@ -700,7 +702,7 @@ export class GrvtGateway {
});
}
private emitDepth(depth: AsterDepth): void {
private emitDepth(depth: Depth): void {
const cloned = cloneDepth(depth);
this.depthListeners.forEach((listener) => {
try {
@@ -711,7 +713,7 @@ export class GrvtGateway {
});
}
private emitTicker(ticker: AsterTicker): void {
private emitTicker(ticker: Ticker): void {
const cloned = cloneTicker(ticker);
this.tickerListeners.forEach((listener) => {
try {
@@ -722,7 +724,7 @@ export class GrvtGateway {
});
}
private emitKlines(klines: AsterKline[]): void {
private emitKlines(klines: Kline[]): void {
const cloned = cloneKlines(klines);
this.klineListeners.forEach((listener) => {
try {
@@ -974,15 +976,15 @@ function normalizeHex(value: string): string {
return `0x${trimmed.toLowerCase()}`;
}
function cloneAccount(snapshot: AsterAccountSnapshot): AsterAccountSnapshot {
function cloneAccount(snapshot: AccountSnapshot): AccountSnapshot {
return JSON.parse(JSON.stringify(snapshot));
}
function cloneOrders(orders: AsterOrder[]): AsterOrder[] {
function cloneOrders(orders: Order[]): Order[] {
return orders.map((order) => ({ ...order }));
}
function cloneDepth(depth: AsterDepth): AsterDepth {
function cloneDepth(depth: Depth): Depth {
return {
...depth,
bids: depth.bids.map((level) => [...level] as [string, string]),
@@ -990,11 +992,11 @@ function cloneDepth(depth: AsterDepth): AsterDepth {
};
}
function cloneTicker(ticker: AsterTicker): AsterTicker {
function cloneTicker(ticker: Ticker): Ticker {
return { ...ticker };
}
function cloneKlines(klines: AsterKline[]): AsterKline[] {
function cloneKlines(klines: Kline[]): Kline[] {
return klines.map((kline) => ({ ...kline }));
}
@@ -1023,11 +1025,11 @@ function scaleDecimal(value: string | number | undefined, decimals: number): big
return scaleDecimal(fixed.toFixed(decimals), decimals);
}
function sumUnrealized(positions: AsterAccountPosition[]): number {
function sumUnrealized(positions: AccountPosition[]): number {
return positions.reduce((total, position) => total + Number(position.unrealizedProfit ?? 0), 0);
}
function getNewestPositionEventTime(positions: AsterAccountPosition[]): number {
function getNewestPositionEventTime(positions: AccountPosition[]): number {
if (!positions.length) return Date.now();
return positions.reduce((max, p) => Math.max(max, Number(p.updateTime) || 0), 0);
}
@@ -1036,7 +1038,7 @@ function mapAccountSnapshot(
response: IApiSubAccountSummaryResponse,
symbol: string,
instrument: string
): AsterAccountSnapshot {
): AccountSnapshot {
const result = response.result;
if (!result) {
return emptyAccount(symbol);
@@ -1068,7 +1070,7 @@ function mapPositions(
response: IApiPositionsResponse,
symbol: string,
instrument: string
): AsterAccountPosition[] {
): AccountPosition[] {
return (response.result ?? [])
.filter((entry) => !entry.instrument || entry.instrument === instrument)
.map((entry) => ({
@@ -1084,11 +1086,11 @@ function mapPositions(
}));
}
function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): AsterOrder[] {
function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): Order[] {
return (response.result ?? []).map((order) => mapOrder(order, symbol));
}
function mapOrder(order: IOrder, symbol: string): AsterOrder {
function mapOrder(order: IOrder, symbol: string): Order {
const leg = order.legs?.[0];
const state = order.state;
const metadata = order.metadata;
@@ -1126,7 +1128,7 @@ function mapOrder(order: IOrder, symbol: string): AsterOrder {
};
}
function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): AsterDepth | null {
function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): Depth | null {
const result = response.result;
if (!result) return null;
const toLevel = (entries: Array<{ price?: string; size?: string }> | undefined) =>
@@ -1140,7 +1142,7 @@ function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): AsterD
};
}
function mapTicker(response: IApiTickerResponse, symbol: string): AsterTicker | null {
function mapTicker(response: IApiTickerResponse, symbol: string): Ticker | null {
const result = response.result;
if (!result) return null;
const buyVolume = Number(result.buy_volume_24h_b ?? 0);
@@ -1161,7 +1163,7 @@ function mapTicker(response: IApiTickerResponse, symbol: string): AsterTicker |
};
}
function mapKlines(response: IApiCandlestickResponse, symbol: string): AsterKline[] {
function mapKlines(response: IApiCandlestickResponse, _symbol: string): Kline[] {
return (response.result ?? []).reverse().map((entry) => ({
openTime: nsToMs(entry.open_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
closeTime: nsToMs(entry.close_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
@@ -1174,7 +1176,7 @@ function mapKlines(response: IApiCandlestickResponse, symbol: string): AsterKlin
}));
}
function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): AsterOrder {
function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): Order {
const order = response.result ?? (response as unknown as { order?: IOrder }).order;
if (!order) {
return {
@@ -1551,7 +1553,7 @@ function padPrivateKey(value: string): string {
return hex;
}
function emptyAccount(symbol: string): AsterAccountSnapshot {
function emptyAccount(symbol: string): AccountSnapshot {
return {
canTrade: true,
canDeposit: true,
@@ -1568,5 +1570,5 @@ function emptyAccount(symbol: string): AsterAccountSnapshot {
updateTime: Date.now(),
},
],
} as AsterAccountSnapshot;
} as AccountSnapshot;
}
+18 -89
View File
@@ -1,92 +1,21 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
import { createOrderHandlers } from "../order-handlers";
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;
}
const handlers = createOrderHandlers({
exchangeName: "GRVT",
defaultLimitTimeInForce: "GTX",
supportsTrailingStop: false,
supportsTriggerType: true,
defaultStopTriggerType: "STOP_LOSS",
stopDefaultReduceOnly: true,
stopDefaultClosePosition: true,
closeDefaultClosePosition: true,
});
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);
}
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+202
View File
@@ -0,0 +1,202 @@
export interface GrvtOrderLeg {
instrument: string;
size: string;
limit_price?: string;
is_buying_asset?: boolean;
}
export type GrvtTimeInForce =
| "GOOD_TILL_TIME"
| "ALL_OR_NONE"
| "IMMEDIATE_OR_CANCEL"
| "FILL_OR_KILL";
export interface GrvtOrderMetadata {
client_order_id?: string;
create_time?: string;
broker?: string | null;
trigger?: GrvtTriggerMetadata;
}
export interface GrvtOrderState {
status?: string;
reject_reason?: string | null;
book_size?: string[];
traded_size?: string[];
update_time?: string;
avg_fill_price?: string[];
}
export interface GrvtOrder {
order_id: string;
client_order_id?: string;
sub_account_id?: string;
is_market?: boolean;
time_in_force?: GrvtTimeInForce;
post_only?: boolean;
reduce_only?: boolean;
legs?: GrvtOrderLeg[];
metadata?: GrvtOrderMetadata;
state?: GrvtOrderState;
instrument?: string;
}
export interface GrvtTrade {
price: string;
size: string;
taker_side: "BUY" | "SELL";
timestamp: string;
}
export interface GrvtTradeHistoryResponse {
result?: GrvtTrade[];
}
export interface GrvtWebsocketMessage<T> {
stream: string;
selector: string;
sequence_number?: string;
feed: T;
}
export interface GrvtOrderUpdateFeed {
order_id: string;
client_order_id?: string;
sub_account_id?: string;
state?: GrvtOrderState;
traded_size?: string[];
update_time?: string;
}
export interface GrvtPositionUpdateFeed {
instrument: string;
size: string;
entry_price?: string;
mark_price?: string;
unrealized_pnl?: string;
sub_account_id?: string;
update_time?: string;
}
export interface GrvtDepthUpdateFeed {
instrument: string;
bids: GrvtDepthLevel[];
asks: GrvtDepthLevel[];
event_time?: string;
}
export interface GrvtTickerUpdateFeed {
instrument: string;
mark_price?: string;
last_trade_price?: string;
best_bid_price?: string;
best_ask_price?: string;
volume_24h?: string;
}
export interface GrvtOpenOrdersResponse {
result?: GrvtOrder[];
}
export interface GrvtPositionsResponse {
result?: GrvtPosition[];
}
export interface GrvtPosition {
instrument: string;
size: string;
entry_price?: string;
mark_price?: string;
unrealized_pnl?: string;
}
export interface GrvtAccountSnapshot {
total_unrealized_pnl?: string;
positions: GrvtPosition[];
settle_currency?: string;
available_balance?: string;
}
export interface GrvtBalancesResponse {
result?: {
total_unrealized_pnl?: string;
positions?: GrvtPosition[];
};
}
export interface GrvtDepthLevel {
price: string;
size: string;
}
export interface GrvtDepth {
instrument: string;
event_time?: string;
bids: GrvtDepthLevel[];
asks: GrvtDepthLevel[];
}
export interface GrvtTicker {
instrument: string;
mark_price?: string;
last_trade_price?: string;
best_bid_price?: string;
best_ask_price?: string;
volume_24h?: string;
}
export interface GrvtKline {
open_time: number;
close_time: number;
open: string;
high: string;
low: string;
close: string;
volume: string;
number_of_trades?: number;
}
export interface GrvtSignature {
signer: string;
r: string;
s: string;
v: number;
expiration: string;
nonce: number;
}
export interface GrvtUnsignedOrderLeg {
instrument: string;
size: string;
limit_price?: string;
is_buying_asset: boolean;
}
export interface GrvtTriggerMetadata {
trigger_type: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
tpsl: {
trigger_by: "UNSPECIFIED" | "INDEX" | "LAST" | "MID" | "MARK";
trigger_price: string;
close_position: boolean;
};
}
export interface GrvtOrderMetadataInput {
client_order_id: string;
trigger?: GrvtTriggerMetadata;
broker?: string | null;
}
export interface GrvtUnsignedOrder {
sub_account_id: string;
is_market: boolean;
time_in_force: GrvtTimeInForce;
post_only: boolean;
reduce_only: boolean;
legs: GrvtUnsignedOrderLeg[];
metadata: GrvtOrderMetadataInput;
}
export interface GrvtSignedOrder extends GrvtUnsignedOrder {
signature: GrvtSignature;
}
+2 -2
View File
@@ -7,7 +7,7 @@ import type {
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import type { Order, 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<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
+19 -22
View File
@@ -8,20 +8,18 @@ import type {
TickerListener,
} from "../adapter";
import type {
AsterAccountAsset,
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountAsset,
AccountSnapshot,
Depth,
Kline,
Order,
Ticker,
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,
@@ -39,7 +37,6 @@ import {
LIGHTER_HOSTS,
LIGHTER_ORDER_TYPE,
LIGHTER_TIME_IN_FORCE,
DEFAULT_ORDER_EXPIRY_PLACEHOLDER,
IMMEDIATE_OR_CANCEL_EXPIRY_PLACEHOLDER,
type LighterEnvironment,
} from "./constants";
@@ -192,12 +189,12 @@ export class LighterGateway {
private accountPollInFlight = false;
private ordersResyncTimer: ReturnType<typeof setInterval> | null = null;
private ordersResyncInFlight = false;
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 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 auth = { token: null as string | null, expiresAt: 0 };
private readonly l1Address: string | null;
private loggedCreateOrderPayload = false;
@@ -362,8 +359,8 @@ export class LighterGateway {
this.klinesEvent.add(handler);
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
const run = async (): Promise<AsterOrder> => {
async createOrder(params: CreateOrderParams): Promise<Order> {
const run = async (): Promise<Order> => {
await this.ensureInitialized();
const conversion = this.mapCreateOrderParams(params);
const { baseAmountScaledString, priceScaledString, triggerPriceScaledString, ...signParams } = conversion;
@@ -1713,7 +1710,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: AsterTicker = {
const ticker: Ticker = {
symbol: this.displaySymbol,
eventType: "lighterSyntheticTicker",
eventTime: Date.now(),
@@ -1739,10 +1736,10 @@ export class LighterGateway {
this.tickerEvent.emit(ticker);
}
private buildAccountAssets(): AsterAccountAsset[] {
private buildAccountAssets(): AccountAsset[] {
if (!this.assets.size) return [];
const now = Date.now();
const list: AsterAccountAsset[] = [];
const list: AccountAsset[] = [];
for (const asset of this.assets.values()) {
const balanceNum = parseNumber(asset.balance);
const lockedNum = parseNumber(asset.locked_balance ?? 0);
@@ -2042,7 +2039,7 @@ function mergeLevels(existing: LighterOrderBookLevel[], updates: LighterOrderBoo
return Array.from(map.entries()).map(([price, size]) => ({ price, size } as LighterOrderBookLevel));
}
function cloneKlines(klines: AsterKline[]): AsterKline[] {
function cloneKlines(klines: Kline[]): Kline[] {
return klines.map((kline) => ({ ...kline }));
}
@@ -2210,7 +2207,7 @@ function extractJsonRequestId(message: any): string | null {
function tryParseTxInfo(value: string): unknown {
try {
return JSON.parse(value);
} catch (_) {
} catch {
return value;
}
}
+22 -22
View File
@@ -1,12 +1,12 @@
import type {
AsterAccountAsset,
AsterAccountPosition,
AsterAccountSnapshot,
AsterDepth,
AsterDepthLevel,
AsterKline,
AsterOrder,
AsterTicker,
AccountAsset,
AccountPosition,
AccountSnapshot,
Depth,
DepthLevel,
Kline,
Order,
Ticker,
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): AsterDepth {
const toLevels = (levels: LighterOrderBookLevel[]): AsterDepthLevel[] =>
export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): Depth {
const toLevels = (levels: LighterOrderBookLevel[]): DepthLevel[] =>
levels.map((level) => [level.price, level.size]);
return {
symbol,
@@ -36,7 +36,7 @@ export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): Ast
};
}
export function toTicker(symbol: string, stats: LighterMarketStats): AsterTicker {
export function toTicker(symbol: string, stats: LighterMarketStats): Ticker {
return {
symbol,
eventType: "lighterTicker",
@@ -50,10 +50,10 @@ export function toTicker(symbol: string, stats: LighterMarketStats): AsterTicker
priceChange: stats.daily_price_change != null ? String(stats.daily_price_change) : undefined,
markPrice: stats.mid_price ?? stats.mark_price ?? stats.index_price,
weightedAvgPrice: undefined,
} as AsterTicker;
} as Ticker;
}
export function toKlines(symbol: string, interval: string, klines: LighterKline[]): AsterKline[] {
export function toKlines(symbol: string, interval: string, klines: LighterKline[]): Kline[] {
return klines.map((entry) => ({
symbol,
eventType: "lighterKline",
@@ -72,11 +72,11 @@ export function toKlines(symbol: string, interval: string, klines: LighterKline[
}));
}
export function toOrders(symbol: string, orders: LighterOrder[]): AsterOrder[] {
export function toOrders(symbol: string, orders: LighterOrder[]): Order[] {
return orders.map((order) => lighterOrderToAster(symbol, order));
}
export function lighterOrderToAster(symbol: string, order: LighterOrder): AsterOrder {
export function lighterOrderToAster(symbol: string, order: LighterOrder): Order {
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: AsterAccountAsset[] = [],
assets: AccountAsset[] = [],
options?: {
marketSymbol?: string | null;
marketId?: number | null;
@@ -172,7 +172,7 @@ export function toAccountSnapshot(
baseAssetId?: number | null;
quoteAssetId?: number | null;
}
): AsterAccountSnapshot {
): AccountSnapshot {
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): AsterAccountAsset[] {
function defaultAsset(details: LighterAccountDetails, quoteAsset: string): AccountAsset[] {
return [
{
asset: quoteAsset || "USDC",
@@ -239,7 +239,7 @@ function defaultAsset(details: LighterAccountDetails, quoteAsset: string): Aster
];
}
function computeTotalWalletBalance(assets: AsterAccountAsset[], details: LighterAccountDetails): string {
function computeTotalWalletBalance(assets: AccountAsset[], details: LighterAccountDetails): string {
const sum = assets.reduce((acc, asset) => acc + Number(asset.walletBalance ?? 0), 0);
if (Number.isFinite(sum) && sum > 0) {
return sum.toString();
@@ -247,7 +247,7 @@ function computeTotalWalletBalance(assets: AsterAccountAsset[], details: Lighter
return details.total_asset_value ?? details.collateral ?? "0";
}
function findAsset(assets: AsterAccountAsset[], target: string | undefined | null): AsterAccountAsset | undefined {
function findAsset(assets: AccountAsset[], target: string | undefined | null): AccountAsset | undefined {
if (!target) return undefined;
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): AsterAccountPosition {
function lighterPositionToAster(symbol: string, position: LighterPosition): AccountPosition {
const sign = position.sign ?? 0;
const positionSide = sign > 0 ? "LONG" : sign < 0 ? "SHORT" : "BOTH";
const magnitude = Number(position.position ?? 0);
+19 -90
View File
@@ -1,93 +1,22 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
import { createOrderHandlers } from "../order-handlers";
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;
}
const handlers = createOrderHandlers({
exchangeName: "Lighter",
defaultLimitTimeInForce: "GTC",
defaultMarketTimeInForce: "IOC",
defaultCloseTimeInForce: "IOC",
supportsTrailingStop: false,
supportsTriggerType: false,
stopDefaultReduceOnly: true,
stopDefaultClosePosition: true,
closeDefaultClosePosition: true,
});
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);
}
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+28 -7
View File
@@ -100,6 +100,13 @@ 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);
}
@@ -181,7 +188,7 @@ export class LighterSigner {
await this.ensureReady();
const apiKeyIndex = params.apiKeyIndex ?? this.defaultKeyIndex;
const result = await this.bridge.call("sign_create_order", {
const bridgePayload = await this.bridge.call("sign_create_order", {
apiKeyIndex,
marketIndex: params.marketIndex,
clientOrderIndex: params.clientOrderIndex.toString(),
@@ -193,13 +200,14 @@ 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 = String(result);
const txInfo = this.resolveTxInfo(bridgePayload);
let signature: string | undefined;
let txHash: string | undefined;
let txHash: string | undefined = this.resolveTxHash(bridgePayload);
try {
const parsed = JSON.parse(txInfo);
if (typeof parsed?.Sig === "string") signature = parsed.Sig;
@@ -220,7 +228,7 @@ export class LighterSigner {
await this.ensureReady();
const apiKeyIndex = params.apiKeyIndex ?? this.defaultKeyIndex;
const result = await this.bridge.call("sign_cancel_order", {
const bridgePayload = await this.bridge.call("sign_cancel_order", {
apiKeyIndex,
marketIndex: params.marketIndex,
orderIndex: params.orderIndex.toString(),
@@ -228,7 +236,7 @@ export class LighterSigner {
accountIndex: this.accountIndex.toString(),
});
const txInfo = String(result);
const txInfo = this.resolveTxInfo(bridgePayload);
let signature: string | undefined;
try {
const parsed = JSON.parse(txInfo);
@@ -248,7 +256,7 @@ export class LighterSigner {
await this.ensureReady();
const apiKeyIndex = params.apiKeyIndex ?? this.defaultKeyIndex;
const result = await this.bridge.call("sign_cancel_all", {
const bridgePayload = await this.bridge.call("sign_cancel_all", {
apiKeyIndex,
timeInForce: params.timeInForce,
scheduledTime: params.scheduledTime.toString(),
@@ -256,7 +264,7 @@ export class LighterSigner {
accountIndex: this.accountIndex.toString(),
});
const txInfo = String(result);
const txInfo = this.resolveTxInfo(bridgePayload);
let signature: string | undefined;
try {
const parsed = JSON.parse(txInfo);
@@ -282,4 +290,17 @@ 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;
}
}
+18 -82
View File
@@ -1,4 +1,3 @@
import { setTimeout, clearTimeout } from "timers";
import type {
AccountListener,
DepthListener,
@@ -9,9 +8,10 @@ import type {
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import type { Order, 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,11 +35,8 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
private readonly gateway: NadoGateway;
private readonly symbol: string;
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 readonly safeInvoke = createSafeInvoke("NadoExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
constructor(credentials: NadoCredentials = {}) {
const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY;
@@ -79,6 +76,9 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
(process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined),
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.ensureInitialized("watchAccount");
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized(`watchDepth:${symbol}`);
void this.init.ensureInitialized(`watchDepth:${symbol}`);
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized(`watchTicker:${symbol}`);
void this.init.ensureInitialized(`watchTicker:${symbol}`);
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
}
watchFundingRate(symbol: string, cb: FundingRateListener): void {
void this.ensureInitialized(`watchFundingRate:${symbol}`);
void this.init.ensureInitialized(`watchFundingRate:${symbol}`);
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
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.ensureInitialized("cancelOrder");
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
@@ -144,70 +144,6 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
}
}
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[NadoExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[NadoExchangeAdapter] ${context} failed`, error);
}
private logError(context: string, error: unknown): void {
const detail = extractMessage(error);
const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`;
+23 -23
View File
@@ -23,13 +23,13 @@ import type {
TickerListener,
} from "../adapter";
import type {
AsterAccountAsset,
AsterAccountPosition,
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountAsset,
AccountPosition,
AccountSnapshot,
Depth,
Kline,
Order,
Ticker,
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: AsterAccountSnapshot | null = null;
private accountSnapshot: AccountSnapshot | 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: AsterKline[] }>();
private klinesState = new Map<string, { productId: number; periodSec: number; klines: Kline[] }>();
private subscriptionRequestId = 1;
private subscriptionAuthComplete = false;
@@ -567,7 +567,7 @@ export class NadoGateway {
};
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized(params.symbol);
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<AsterKline[]> {
private async fetchCandlesticks(productId: number, periodSec: number, limit: number): Promise<Kline[]> {
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(): AsterOrder[] {
const orders: AsterOrder[] = [];
private buildAsterOrdersSnapshot(): Order[] {
const orders: Order[] = [];
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): AsterDepth | null {
private buildDepthSnapshot(productId: number, symbol: string): Depth | 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): AsterTicker | null {
private buildTickerSnapshot(productId: number, symbol: string): Ticker | 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"]>): AsterAccountSnapshot {
private mapSubaccountInfoToAsterSnapshot(data: NonNullable<NadoSubaccountInfoResponse["data"]>): AccountSnapshot {
const now = nowMs();
const assets: AsterAccountAsset[] = [];
const positions: AsterAccountPosition[] = [];
const assets: AccountAsset[] = [];
const positions: AccountPosition[] = [];
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 (_error) {
} catch {
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: AsterKline = {
const next: Kline = {
openTime,
closeTime,
open: fromX18(event.open_x18).toFixed(),
@@ -1795,7 +1795,7 @@ export class NadoGateway {
}
}
private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
const side = params.side;
const qty = params.quantity ?? 0;
@@ -1905,7 +1905,7 @@ export class NadoGateway {
};
}
private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
const side = params.side;
const qty = params.quantity ?? 0;
@@ -2017,7 +2017,7 @@ export class NadoGateway {
};
}
private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)");
const side = params.side;
const qty = params.quantity ?? 0;
+19 -90
View File
@@ -1,93 +1,22 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
import { createOrderHandlers } from "../order-handlers";
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;
}
const handlers = createOrderHandlers({
exchangeName: "Nado",
defaultLimitTimeInForce: "GTC",
defaultMarketTimeInForce: "IOC",
defaultCloseTimeInForce: "IOC",
supportsTrailingStop: false,
supportsTriggerType: false,
stopDefaultReduceOnly: true,
stopDefaultClosePosition: true,
closeDefaultClosePosition: true,
});
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);
}
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+154
View File
@@ -0,0 +1,154 @@
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;
}
supportsTriggerOrders(): boolean {
return true;
}
watchAccount(cb: AccountListener): void {
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
}
watchFundingRate(symbol: string, cb: FundingRateListener): void {
void this.init.ensureInitialized("watchFundingRate");
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
}
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.init.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
await this.init.ensureInitialized("getPrecision");
return this.gateway.getPrecision(this.symbol);
}
onConnectionEvent(listener: ConnectionEventListener): void {
this.gateway.onConnectionEvent(listener);
}
offConnectionEvent(listener: ConnectionEventListener): void {
this.gateway.offConnectionEvent(listener);
}
async queryOpenOrders(): Promise<Order[]> {
await this.init.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders();
}
async queryAccountSnapshot() {
await this.init.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot();
}
async forceCancelAllOrders(): Promise<boolean> {
await this.init.ensureInitialized("forceCancelAllOrders");
return this.gateway.forceCancelAllOrders();
}
}
function parseBoolean(value: string | undefined): boolean {
if (!value) return false;
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
}
function parseNumber(value: string | undefined): number | undefined {
if (!value) return undefined;
const number = Number(value);
return Number.isFinite(number) ? number : undefined;
}
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
import { createOrderHandlers } from "../order-handlers";
const handlers = createOrderHandlers({
exchangeName: "Ondo Perps",
defaultLimitTimeInForce: "GTX",
defaultMarketTimeInForce: "IOC",
defaultCloseTimeInForce: "IOC",
defaultStopTimeInForce: "GTC",
defaultStopTriggerType: "STOP_LOSS",
supportsTrailingStop: false,
supportsTriggerType: true,
stopDefaultReduceOnly: true,
stopDefaultClosePosition: true,
closeDefaultClosePosition: true,
});
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+173
View File
@@ -0,0 +1,173 @@
export interface OndoperpsApiResponse<T> {
success: boolean;
result?: T;
error?: string;
error_code?: string;
}
export interface OndoperpsOrder {
orderId: string;
clientOrderId?: string;
parentOrderId?: string;
side: "buy" | "sell";
price?: string;
size: string;
market: string;
filledSize?: string;
lastFillSize?: string;
filledCost?: string;
realizedPnl?: string;
fee?: string;
feeRebate?: string;
status: "open" | "fullyfilled" | "canceled" | "pending" | "untriggered" | string;
createdAt: string;
filledAt?: string;
canceledAt?: string;
cancelReason?: string;
type: "limit" | "market" | "stopMarket" | "takeProfitMarket" | string;
timeInForce?: "GTC" | "IOC" | "FOK";
reduceOnly?: boolean;
closePosition?: boolean;
stopOrderType?: "stopLoss" | "takeProfit";
triggerPrice?: string;
}
export interface OndoperpsPosition {
market: string;
direction: "long" | "short" | "neutral";
netQuantity: string;
averageEntryPrice: string;
usedMargin: string;
unrealizedPnl: string;
markPrice: string;
liquidationPrice: string;
bankruptcyPrice: string;
maintenanceMargin: string;
notionalValue: string;
leverage: string;
netFundingSinceNeutral: string;
returnOnEquity: string;
stopLossTriggerPrice?: string;
takeProfitTriggerPrice?: string;
}
export interface OndoperpsBalance {
walletBalance: string;
realizedPnl: string;
unrealizedPnl: string;
marginBalance: string;
usedMargin: string;
availableMargin: string;
withdrawableMargin: string;
maintenanceMarginRequirement: string;
totalMaintenanceMargin: string;
marginRatio: string;
leverage: string;
underLiquidation: boolean;
totalFundingPayments: string;
totalTradingFees: string;
totalPnL: string;
netInvested?: string;
}
export interface OndoperpsStopOrders {
market: string;
positionDirection: "long" | "short" | "neutral";
stopLoss?: string | null;
takeProfit?: string | null;
}
export type OndoperpsBookLevel = [string, string];
export interface OndoperpsBookSnapshot {
market: string;
time: string;
bids: OndoperpsBookLevel[];
asks: OndoperpsBookLevel[];
depthLevels?: string;
}
export interface OndoperpsCandle {
startTime: string;
open: string;
high: string;
low: string;
close: string;
volume: string;
}
export interface OndoperpsWsKline {
m: string;
t: number;
s: number;
e: number;
o: number;
h: number;
l: number;
c: number;
v: number;
x?: boolean;
}
export interface OndoperpsContract {
market: string;
displayName?: string;
productType: string;
contractType: string;
baseCurrency: string;
quoteCurrency: string;
disabled: boolean;
lastPrice?: string;
baseVolume?: string;
quoteVolume?: string;
usdVolume?: string;
bid?: string;
ask?: string;
high?: string;
low?: string;
openInterest?: string;
openInterestUsd?: string;
indexPrice?: string;
fundingRate?: string;
nextFundingRate?: string;
nextFundingRateTimestamp?: string;
makerFee?: string;
takerFee?: string;
priceChangePercent?: string;
isClosed?: boolean;
}
export interface OndoperpsMarkPrice {
market: string;
price: string;
markPrice: string;
oraclePrice?: string;
lastExternalPrice?: string;
lastUpdatedTime?: string;
}
export interface OndoperpsFundingRate {
market: string;
rate: string;
intervalEnds?: string;
}
export interface OndoperpsTradingPair {
market: string;
baseIncrement: string;
quoteIncrement: string;
}
export interface OndoperpsMarketsResult {
perps?: {
tradingPairs?: OndoperpsTradingPair[];
};
}
export interface OndoperpsWsMessage {
type: "pong" | "loggedIn" | "subscribed" | "unsubscribed" | "update" | "error" | string;
channel?: string;
code?: number;
msg?: string;
data?: unknown;
}
+147
View File
@@ -0,0 +1,147 @@
import type { Order, CreateOrderParams, TimeInForce } from "./types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "./order-schema";
import { toStringBoolean } from "./order-schema";
export interface OrderHandlerConfig {
exchangeName: string;
defaultLimitTimeInForce: TimeInForce | "GTX";
defaultMarketTimeInForce?: TimeInForce;
defaultCloseTimeInForce?: TimeInForce;
defaultStopTimeInForce?: TimeInForce;
defaultStopTriggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
supportsTrailingStop: boolean;
supportsTriggerType: boolean;
stopDefaultReduceOnly?: boolean;
stopDefaultClosePosition?: boolean;
closeDefaultClosePosition?: boolean;
stopPriceAsPrice?: boolean;
}
export interface OrderHandlers {
createLimitOrder(intent: LimitOrderIntent): Promise<Order>;
createMarketOrder(intent: MarketOrderIntent): Promise<Order>;
createStopOrder(intent: StopOrderIntent): Promise<Order>;
createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order>;
createClosePositionOrder(intent: ClosePositionIntent): Promise<Order>;
}
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
if (params.quantity === undefined) {
params.quantity = intent.quantity;
}
if (params.timeInForce === undefined && intent.timeInForce) {
params.timeInForce = intent.timeInForce;
}
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
if (intent.clientOrderId !== undefined) {
params.clientOrderId = intent.clientOrderId;
}
return params;
}
export function createOrderHandlers(config: OrderHandlerConfig): OrderHandlers {
return {
async createLimitOrder(intent: LimitOrderIntent): Promise<Order> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "LIMIT",
quantity: intent.quantity,
price: intent.price,
timeInForce: intent.timeInForce ?? config.defaultLimitTimeInForce,
slPrice: intent.slPrice,
tpPrice: intent.tpPrice,
},
intent,
);
return intent.adapter.createOrder(params);
},
async createMarketOrder(intent: MarketOrderIntent): Promise<Order> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
timeInForce: intent.timeInForce ?? config.defaultMarketTimeInForce,
},
intent,
);
return intent.adapter.createOrder(params);
},
async createStopOrder(intent: StopOrderIntent): Promise<Order> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? (config.defaultStopTimeInForce ?? "GTC"),
triggerType: config.supportsTriggerType ? (intent.triggerType ?? config.defaultStopTriggerType) : undefined,
price: config.stopPriceAsPrice ? intent.stopPrice : undefined,
},
intent,
);
if (config.stopDefaultReduceOnly && intent.reduceOnly === undefined) {
params.reduceOnly = "true";
}
if (config.stopDefaultClosePosition && intent.closePosition === undefined) {
params.closePosition = "true";
}
return intent.adapter.createOrder(params);
},
async createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order> {
if (!config.supportsTrailingStop) {
throw new Error(`${config.exchangeName} does not support trailing stop orders`);
}
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "TRAILING_STOP_MARKET",
quantity: intent.quantity,
activationPrice: intent.activationPrice,
callbackRate: intent.callbackRate,
timeInForce: intent.timeInForce ?? "GTC",
},
intent,
);
return intent.adapter.createOrder(params);
},
async createClosePositionOrder(intent: ClosePositionIntent): Promise<Order> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
timeInForce: intent.timeInForce ?? config.defaultCloseTimeInForce,
},
intent,
);
if (config.closeDefaultClosePosition && intent.closePosition === undefined) {
params.closePosition = "true";
}
return intent.adapter.createOrder(params);
},
};
}
+21 -12
View File
@@ -1,5 +1,5 @@
import type { ExchangeAdapter } from "./adapter";
import type { AsterOrder } from "./types";
import type { Order } from "./types";
import { SUPPORTED_EXCHANGE_IDS, type SupportedExchangeId } from "./create-adapter";
import type {
BaseOrderIntent,
@@ -17,15 +17,16 @@ 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<AsterOrder>;
market(intent: MarketOrderIntent): Promise<AsterOrder>;
stop(intent: StopOrderIntent): Promise<AsterOrder>;
trailingStop?: (intent: TrailingStopOrderIntent) => Promise<AsterOrder>;
close(intent: ClosePositionIntent): Promise<AsterOrder>;
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>;
}
const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
@@ -85,13 +86,21 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
trailingStop: binanceOrders.createTrailingStopOrder,
close: binanceOrders.createClosePositionOrder,
},
ondoperps: {
limit: ondoperpsOrders.createLimitOrder,
market: ondoperpsOrders.createMarketOrder,
stop: ondoperpsOrders.createStopOrder,
trailingStop: ondoperpsOrders.createTrailingStopOrder,
close: ondoperpsOrders.createClosePositionOrder,
},
};
const knownExchanges: ExchangeKey[] = [...SUPPORTED_EXCHANGE_IDS];
function normalizeExchangeId(value: string | undefined | null): string | undefined {
if (!value) return undefined;
return value.trim().toLowerCase();
const normalized = value.trim().toLowerCase();
return normalized === "ondoperp" ? "ondoperps" : normalized;
}
function resolveExchangeKey(adapter: ExchangeAdapter): ExchangeKey {
@@ -117,19 +126,19 @@ function getHandlers(intent: BaseOrderIntent): ExchangeOrderHandlers {
return handlers;
}
export function routeLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
export function routeLimitOrder(intent: LimitOrderIntent): Promise<Order> {
return getHandlers(intent).limit(intent);
}
export function routeMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
export function routeMarketOrder(intent: MarketOrderIntent): Promise<Order> {
return getHandlers(intent).market(intent);
}
export function routeStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
export function routeStopOrder(intent: StopOrderIntent): Promise<Order> {
return getHandlers(intent).stop(intent);
}
export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order> {
const handlers = getHandlers(intent);
if (!handlers.trailingStop) {
throw new Error("Trailing stop orders are not supported by the current exchange");
@@ -137,6 +146,6 @@ export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise
return handlers.trailingStop(intent);
}
export function routeCloseOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
export function routeCloseOrder(intent: ClosePositionIntent): Promise<Order> {
return getHandlers(intent).close(intent);
}
+3 -3
View File
@@ -9,13 +9,13 @@ export interface BaseOrderIntent {
reduceOnly?: boolean;
closePosition?: boolean;
timeInForce?: TimeInForce | "GTX";
clientOrderId?: string;
}
export interface LimitOrderIntent extends BaseOrderIntent {
price: number;
// StandX TPSL 参数
slPrice?: number; // 止损价格
tpPrice?: number; // 止盈价格
slPrice?: number;
tpPrice?: number;
}
export interface MarketOrderIntent extends BaseOrderIntent {
+17 -81
View File
@@ -1,4 +1,3 @@
import { setTimeout, clearTimeout } from "timers";
import type {
AccountListener,
DepthListener,
@@ -7,9 +6,10 @@ import type {
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import type { Order, 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,11 +26,8 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
private readonly gateway: ParadexGateway;
private readonly symbol: string;
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 readonly safeInvoke = createSafeInvoke("ParadexExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
constructor(credentials: ParadexCredentials = {}) {
const privateKey = credentials.privateKey ?? process.env.PARADEX_PRIVATE_KEY;
@@ -54,6 +51,9 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
});
this.symbol = symbol;
this.init = createInitManager("ParadexExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
}
supportsTrailingStops(): boolean {
@@ -61,114 +61,50 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized(`watchDepth:${symbol}`);
void this.init.ensureInitialized(`watchDepth:${symbol}`);
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized(`watchTicker:${symbol}`);
void this.init.ensureInitialized(`watchTicker:${symbol}`);
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
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.ensureInitialized("cancelOrder");
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.init.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)) {
+23 -24
View File
@@ -7,12 +7,12 @@ import ccxt, {
} from "ccxt";
import { createRequire } from "module";
import type {
AsterAccountAsset,
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountAsset,
AccountSnapshot,
Depth,
Kline,
Order,
Ticker,
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 (_error) {
} catch {
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, AsterOrder>();
private lastBalanceSnapshot: AsterAccountSnapshot | null = null;
private readonly localOrders = new Map<string, Order>();
private lastBalanceSnapshot: AccountSnapshot | 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<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
await this.ensureInitialized(params.symbol);
const symbol = this.marketSymbol;
const type = this.mapOrderTypeToCcxt(params.type);
@@ -571,7 +571,6 @@ 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;
@@ -593,7 +592,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 (_normalizeError) {
} catch {
// Swallow precision normalization errors and let exchange validation surface if any
}
@@ -679,7 +678,7 @@ export class ParadexGateway {
}
}
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
private mapBalanceToAccountSnapshot(balance: Balances): AccountSnapshot {
const now = Date.now();
const rawPositions = (() => {
@@ -712,7 +711,7 @@ export class ParadexGateway {
...Object.keys(total),
]);
const assets: AsterAccountAsset[] = Array.from(assetKeys).map((asset) => ({
const assets: AccountAsset[] = Array.from(assetKeys).map((asset) => ({
asset,
walletBalance: String(total[asset] ?? 0),
availableBalance: String(free[asset] ?? 0),
@@ -738,7 +737,7 @@ export class ParadexGateway {
};
}
private mapBalanceToAccountSnapshotFromPositions(rawPositions: any[]): AsterAccountSnapshot {
private mapBalanceToAccountSnapshotFromPositions(rawPositions: any[]): AccountSnapshot {
const now = Date.now();
const positions = this.normalizePositions(rawPositions, now);
this.logger("positions", JSON.stringify({ raw: rawPositions, mapped: positions }));
@@ -762,7 +761,7 @@ export class ParadexGateway {
};
}
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] {
private normalizePositions(rawPositions: any[], now: number): AccountSnapshot["positions"] {
return rawPositions
.filter((pos) => pos)
.map((pos: any) => {
@@ -808,7 +807,7 @@ export class ParadexGateway {
});
}
private mapOrderToAsterOrder(order: CcxtOrder): AsterOrder {
private mapOrderToAsterOrder(order: CcxtOrder): Order {
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
const mappedType = this.mapCcxtOrderTypeToAster(order.type);
return {
@@ -831,7 +830,7 @@ export class ParadexGateway {
};
}
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
private mapOrderBookToDepth(orderbook: CcxtOrderBook): Depth {
return {
lastUpdateId: orderbook.nonce || Date.now(),
bids: (orderbook.bids || [])
@@ -844,7 +843,7 @@ export class ParadexGateway {
};
}
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
private mapTickerToAsterTicker(ticker: CcxtTicker): Ticker {
return {
symbol: ticker.symbol,
lastPrice: ticker.last?.toString() || "0",
@@ -857,7 +856,7 @@ export class ParadexGateway {
};
}
private mapOHLCVToKline(candle: CcxtOhlcv, interval: string): AsterKline {
private mapOHLCVToKline(candle: CcxtOhlcv, interval: string): Kline {
const [timestampRaw, openRaw, highRaw, lowRaw, closeRaw, volumeRaw] = candle;
const timestamp = typeof timestampRaw === "number" && Number.isFinite(timestampRaw)
? timestampRaw
@@ -927,7 +926,7 @@ export class ParadexGateway {
return base[interval] ?? 60 * 1000;
}
private upsertLocalOrder(order: AsterOrder): void {
private upsertLocalOrder(order: Order): void {
const key = String(order.orderId);
if (this.isOrderClosed(order)) {
this.localOrders.delete(key);
@@ -944,8 +943,8 @@ export class ParadexGateway {
}
}
private updateOrdersFromRemote(open: CcxtOrder[], closed: CcxtOrder[]): void {
const nextOpen = new Map<string, AsterOrder>();
private updateOrdersFromRemote(open: CcxtOrder[], _closed: CcxtOrder[]): void {
const nextOpen = new Map<string, Order>();
for (const order of open) {
const mapped = this.mapOrderToAsterOrder(order);
@@ -974,7 +973,7 @@ export class ParadexGateway {
}
}
private isOrderClosed(order: AsterOrder): boolean {
private isOrderClosed(order: Order): boolean {
const status = (order.status ?? "").toUpperCase();
if (
status.includes("CLOSE") ||
+18 -91
View File
@@ -1,93 +1,20 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
import { createOrderHandlers } from "../order-handlers";
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;
}
const handlers = createOrderHandlers({
exchangeName: "Paradex",
defaultLimitTimeInForce: "GTC",
supportsTrailingStop: false,
supportsTriggerType: false,
stopDefaultReduceOnly: true,
stopDefaultClosePosition: true,
stopPriceAsPrice: true,
closeDefaultClosePosition: true,
});
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);
}
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+26 -1
View File
@@ -1,12 +1,13 @@
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";
@@ -50,6 +51,10 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
const credentials = resolveBinanceCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
}
case "ondoperps": {
const credentials = resolveOndoperpsCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, ondoperps: credentials });
}
}
}
@@ -200,6 +205,26 @@ function resolveBinanceCredentials(symbol: string): BinanceCredentials {
};
}
function resolveOndoperpsCredentials(symbol: string): OndoperpsCredentials {
const apiKeyId = process.env.ONDOPERPS_API_KEY_ID ?? process.env.ONDOPERP_API_KEY_ID ?? process.env.ONDO_KEY_ID;
const apiSecret = process.env.ONDOPERPS_API_SECRET ?? process.env.ONDOPERP_API_SECRET ?? process.env.ONDO_API_SECRET;
if (!apiKeyId || !apiSecret) {
throw new Error(t("env.missingOndoperps"));
}
const sandbox = parseOptionalBoolean(process.env.ONDOPERPS_SANDBOX ?? process.env.ONDOPERP_SANDBOX) === true;
return {
apiKeyId,
apiSecret,
symbol: process.env.ONDOPERPS_SYMBOL ?? process.env.ONDOPERP_SYMBOL ?? symbol,
baseUrl: process.env.ONDOPERPS_BASE_URL ?? process.env.ONDOPERP_BASE_URL ?? (sandbox ? "https://api.ondoperps-sandbox.xyz" : undefined),
wsUrl: process.env.ONDOPERPS_WS_URL ?? process.env.ONDOPERP_WS_URL ?? (sandbox ? "wss://api.ondoperps-sandbox.xyz/ws" : undefined),
builderCode: process.env.ONDOPERPS_BUILDER_CODE ?? process.env.ONDOPERP_BUILDER_CODE ?? undefined,
builderFeeRateBps: parseOptionalNumber(
process.env.ONDOPERPS_BUILDER_FEE_RATE_BPS ?? process.env.ONDOPERP_BUILDER_FEE_RATE_BPS,
),
};
}
function isHex32(value: string): boolean {
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
}
+23 -86
View File
@@ -1,4 +1,3 @@
import { setTimeout, clearTimeout } from "timers";
import type {
AccountListener,
DepthListener,
@@ -10,9 +9,9 @@ import type {
RestHealthListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import type { Order, CreateOrderParams } from "../types";
import { StandxGateway, type StandxGatewayOptions, type ConnectionEventListener, type ConnectionEventType } from "./gateway";
import { createSafeInvoke, createInitManager } from "../adapter-utils";
export type { ConnectionEventListener, ConnectionEventType };
@@ -31,11 +30,8 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
private readonly gateway: StandxGateway;
private readonly symbol: string;
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 readonly safeInvoke = createSafeInvoke("StandxExchangeAdapter");
private readonly init: ReturnType<typeof createInitManager>;
constructor(credentials: StandxCredentials = {}) {
const token = credentials.token ?? process.env.STANDX_TOKEN;
@@ -52,6 +48,9 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
signingKey: credentials.signingKey,
logger: credentials.logger,
});
this.init = createInitManager("StandxExchangeAdapter", () =>
this.gateway.ensureInitialized(this.symbol),
);
}
supportsTrailingStops(): boolean {
@@ -59,52 +58,52 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
}
watchAccount(cb: AccountListener): void {
void this.ensureInitialized("watchAccount");
void this.init.ensureInitialized("watchAccount");
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
}
watchOrders(cb: OrderListener): void {
void this.ensureInitialized("watchOrders");
void this.init.ensureInitialized("watchOrders");
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
}
watchDepth(symbol: string, cb: DepthListener): void {
void this.ensureInitialized("watchDepth");
void this.init.ensureInitialized("watchDepth");
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
}
watchTicker(symbol: string, cb: TickerListener): void {
void this.ensureInitialized("watchTicker");
void this.init.ensureInitialized("watchTicker");
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
void this.ensureInitialized("watchKlines");
void this.init.ensureInitialized("watchKlines");
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
}
watchFundingRate(symbol: string, cb: FundingRateListener): void {
void this.ensureInitialized("watchFundingRate");
void this.init.ensureInitialized("watchFundingRate");
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
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.ensureInitialized("cancelOrder");
await this.init.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.init.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.init.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
@@ -151,18 +150,18 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
* HTTP API
*
*/
async queryOpenOrders(): Promise<AsterOrder[]> {
await this.ensureInitialized("queryOpenOrders");
async queryOpenOrders(): Promise<Order[]> {
await this.init.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders(this.symbol);
}
async queryAccountSnapshot() {
await this.ensureInitialized("queryAccountSnapshot");
await this.init.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot();
}
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
await this.ensureInitialized("changeMarginMode");
await this.init.ensureInitialized("changeMarginMode");
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
}
@@ -171,69 +170,7 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
*
*/
async forceCancelAllOrders(): Promise<boolean> {
await this.ensureInitialized("forceCancelAllOrders");
await this.init.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);
}
}
+33 -33
View File
@@ -14,13 +14,13 @@ import type {
TickerListener,
} from "../adapter";
import type {
AsterAccountAsset,
AsterAccountPosition,
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountAsset,
AccountPosition,
AccountSnapshot,
Depth,
Kline,
Order,
Ticker,
CreateOrderParams,
OrderSide,
OrderType,
@@ -77,7 +77,7 @@ type FundingState = {
};
type VirtualStop = {
order: AsterOrder;
order: Order;
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, AsterOrder>, order: AsterOrder): void {
function mergeOrderSnapshot(map: Map<string, Order>, order: Order): 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, AsterOrder>();
private readonly positions = new Map<string, AsterAccountPosition>();
private readonly balances = new Map<string, AsterAccountAsset>();
private readonly openOrders = new Map<string, Order>();
private readonly positions = new Map<string, AccountPosition>();
private readonly balances = new Map<string, AccountAsset>();
private readonly virtualStops = new Map<string, VirtualStop>();
private accountSnapshot: AsterAccountSnapshot | null = null;
private accountSnapshot: AccountSnapshot | 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<AsterOrder[]> {
async queryOpenOrders(symbol: string): Promise<Order[]> {
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: AsterOrder[] = [];
const result: Order[] = [];
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<AsterOrder> {
async createOrder(params: CreateOrderParams): Promise<Order> {
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<AsterOrder> {
private async createVirtualStopOrder(symbol: string, params: CreateOrderParams): Promise<Order> {
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: AsterOrder = {
const order: Order = {
orderId: clientOrderId,
clientOrderId,
symbol,
@@ -825,7 +825,7 @@ export class StandxGateway {
return order;
}
private async submitOrder(symbol: string, params: CreateOrderParams): Promise<AsterOrder> {
private async submitOrder(symbol: string, params: CreateOrderParams): Promise<Order> {
const orderType = params.type === "MARKET" ? "market" : "limit";
const 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: AsterOrder = {
const order: Order = {
orderId: clientOrderId,
clientOrderId,
symbol,
@@ -1038,7 +1038,7 @@ export class StandxGateway {
}
this.logDebug("ws depth stats", detail);
}
const depth: AsterDepth = {
const depth: Depth = {
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: AsterDepth): void {
private emitDepth(symbol: string, depth: Depth): 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: AsterTicker): void {
private emitTicker(symbol: string, ticker: Ticker): 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: AsterAccountSnapshot = {
const snapshot: AccountSnapshot = {
canTrade: true,
canDeposit: true,
canWithdraw: true,
@@ -1427,7 +1427,7 @@ export class StandxGateway {
}
}
private async refreshAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
private async refreshAccountSnapshot(): Promise<AccountSnapshot | 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: AsterAccountAsset = {
const asset: AccountAsset = {
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<AsterAccountSnapshot | null> {
async queryAccountSnapshot(): Promise<AccountSnapshot | 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: AsterDepth = {
const depth: Depth = {
lastUpdateId: Date.now(),
bids,
asks,
@@ -1554,7 +1554,7 @@ export class StandxGateway {
},
});
if (!response || response.s !== "ok" || !Array.isArray(response.t)) return;
const klines: AsterKline[] = response.t.map((openTime, index) => {
const klines: Kline[] = response.t.map((openTime, index) => {
const o = response.o?.[index];
const h = response.h?.[index];
const l = response.l?.[index];
@@ -1647,7 +1647,7 @@ export class StandxGateway {
}
}
private mapOrder(data: StandxOrder): AsterOrder {
private mapOrder(data: StandxOrder): Order {
const clientOrderId = data.cl_ord_id ? String(data.cl_ord_id) : data.id != null ? String(data.id) : "";
const 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): AsterAccountPosition {
private mapPosition(data: StandxPosition): AccountPosition {
return {
symbol: data.symbol,
positionAmt: String(data.qty ?? "0"),
@@ -1690,7 +1690,7 @@ export class StandxGateway {
};
}
private mapBalance(data: StandxBalance): AsterAccountAsset {
private mapBalance(data: StandxBalance): AccountAsset {
const walletBalance = data.total ?? data.free ?? "0";
const availableBalance = data.free ?? walletBalance;
return {
@@ -1701,7 +1701,7 @@ export class StandxGateway {
};
}
private mapTicker(data: StandxPrice): AsterTicker {
private mapTicker(data: StandxPrice): Ticker {
const spread = data.spread ?? [data.spread_bid ?? "0", data.spread_ask ?? "0"];
const lastPrice = data.last_price ?? data.mark_price ?? data.index_price ?? data.mid_price ?? "0";
return {
+19 -92
View File
@@ -1,94 +1,21 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
import { createOrderHandlers } from "../order-handlers";
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;
}
const handlers = createOrderHandlers({
exchangeName: "StandX",
defaultLimitTimeInForce: "GTX",
defaultMarketTimeInForce: "IOC",
defaultCloseTimeInForce: "IOC",
supportsTrailingStop: false,
supportsTriggerType: false,
stopDefaultReduceOnly: true,
stopDefaultClosePosition: true,
closeDefaultClosePosition: true,
});
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);
}
export const {
createLimitOrder,
createMarketOrder,
createStopOrder,
createTrailingStopOrder,
createClosePositionOrder,
} = handlers;
+15 -465
View File
@@ -25,12 +25,12 @@ export interface CreateOrderParams {
reduceOnly?: StringBoolean;
closePosition?: StringBoolean;
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
// StandX TPSL 参数
slPrice?: number; // 止损价格
tpPrice?: number; // 止盈价格
slPrice?: number;
tpPrice?: number;
clientOrderId?: string;
}
export interface AsterAccountPosition {
export interface AccountPosition {
symbol: string;
positionAmt: string;
entryPrice: string;
@@ -51,210 +51,7 @@ export interface AsterAccountPosition {
markPrice?: string;
}
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 {
export interface AccountAsset {
asset: string;
walletBalance: string;
availableBalance: string;
@@ -272,7 +69,7 @@ export interface AsterAccountAsset {
marginAvailable?: boolean;
}
export interface AsterAccountSnapshot {
export interface AccountSnapshot {
canTrade: boolean;
canDeposit: boolean;
canWithdraw: boolean;
@@ -288,8 +85,8 @@ export interface AsterAccountSnapshot {
totalCrossUnPnl?: string;
availableBalance?: string;
maxWithdrawAmount?: string;
positions: AsterAccountPosition[];
assets: AsterAccountAsset[];
positions: AccountPosition[];
assets: AccountAsset[];
marketType?: "perp" | "spot";
baseAsset?: string;
quoteAsset?: string;
@@ -297,22 +94,22 @@ export interface AsterAccountSnapshot {
quoteAssetId?: number;
}
export interface AsterDepthLevel extends Array<string> {
export interface DepthLevel extends Array<string> {
0: string; // price
1: string; // quantity
}
export interface AsterDepth {
export interface Depth {
lastUpdateId: number;
bids: AsterDepthLevel[];
asks: AsterDepthLevel[];
bids: DepthLevel[];
asks: DepthLevel[];
eventTime?: number;
eventType?: string;
tradeTime?: number;
symbol?: string;
}
export interface AsterTicker {
export interface Ticker {
symbol: string;
lastPrice: string;
openPrice: string;
@@ -336,254 +133,7 @@ export interface AsterTicker {
count?: number;
}
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 {
export interface Kline {
eventType?: string;
eventTime?: number;
symbol?: string;
@@ -604,7 +154,7 @@ export interface AsterKline {
isClosed?: boolean;
}
export interface AsterOrder {
export interface Order {
orderId: number | string;
clientOrderId: string;
symbol: string;
+14
View File
@@ -288,6 +288,16 @@ const translations: Record<string, TranslationEntry> = {
en: "Last price: {lastPrice} | Lower: {lower} | Upper: {upper} | Grid count: {count}",
},
"grid.dataStatus": { zh: "数据状态:", en: "Data status:" },
"grid.anchorLine": {
zh: "锚定价: {anchor} 网格版本: v{version}",
en: "Anchor: {anchor} | Grid version: v{version}",
},
"grid.shiftState": { zh: "移格进行中: {phase}", en: "Shifting: {phase}" },
"grid.stopProtection": {
zh: "止损防护: 未覆盖 {uncovered} 兜底止损单: {stop}",
en: "Stop protection: uncovered {uncovered} | exchange stop: {stop}",
},
"grid.stopProtection.none": { zh: "无", en: "none" },
"grid.stopReason": { zh: "暂停原因: {reason}", en: "Pause reason: {reason}" },
"grid.configTitle": { zh: "网格配置", en: "Grid Config" },
"grid.configSize": {
@@ -426,6 +436,10 @@ const translations: Record<string, TranslationEntry> = {
zh: "StandX 需要配置 STANDX_TOKEN",
en: "StandX requires STANDX_TOKEN",
},
"env.missingOndoperps": {
zh: "Ondo Perps 需要配置 ONDOPERPS_API_KEY_ID 与 ONDOPERPS_API_SECRET(兼容旧 ONDOPERP_ 前缀)",
en: "Ondo Perps requires ONDOPERPS_API_KEY_ID and ONDOPERPS_API_SECRET (legacy ONDOPERP_ prefix is supported)",
},
"log.subscribe.accountFail": {
zh: "订阅账户失败: {error}",
en: "Failed to subscribe account: {error}",
+56 -22
View File
@@ -4,30 +4,64 @@ import { App } from "./ui/App";
import { setupGlobalErrorHandlers } from "./runtime-errors";
import { parseCliArgs, printCliHelp } from "./cli/args";
import { startStrategy } from "./cli/strategy-runner";
import { resolveExchangeId } from "./exchanges/create-adapter";
import { CommandParseError, parseCommandArgv, printCommandHelp } from "./cli/command-parser";
import { executeCliCommand, renderCommandPayload } from "./cli/command-executor";
setupGlobalErrorHandlers();
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;
}
void run();
if (options.help) {
printCliHelp();
process.exit(0);
}
async function run(): Promise<void> {
const rawArgv = process.argv.slice(2);
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 />);
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 />);
}
}
+10 -9
View File
@@ -1,7 +1,8 @@
import type { BasisArbConfig } from "../config";
import type { ExchangeAdapter, FundingRateSnapshot } from "../exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterSpotBookTicker } from "../exchanges/types";
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/client";
import type { AccountSnapshot, Depth } from "../exchanges/types";
import type { AsterSpotBookTicker } from "../exchanges/aster/types";
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/gateway";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { StrategyEventEmitter } from "./common/event-emitter";
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
@@ -155,7 +156,7 @@ export class BasisArbEngine {
private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterDepth>(
safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.futuresSymbol),
(depth) => {
this.applyFuturesDepth(depth);
@@ -168,7 +169,7 @@ export class BasisArbEngine {
);
if (this.exchange.id === "nado" || this.exchange.id === "standx" || this.exchange.id === "binance") {
safeSubscribe<AsterDepth>(
safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
(depth) => {
this.applySpotDepth(depth);
@@ -196,7 +197,7 @@ export class BasisArbEngine {
);
}
safeSubscribe<AsterAccountSnapshot>(
safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange),
(snapshot) => {
this.applyAccountSnapshot(snapshot);
@@ -210,7 +211,7 @@ export class BasisArbEngine {
}
}
private applyFuturesDepth(depth: AsterDepth): void {
private applyFuturesDepth(depth: Depth): void {
if (!depth?.bids?.length || !depth?.asks?.length) {
return;
}
@@ -360,7 +361,7 @@ export class BasisArbEngine {
this.emitUpdate();
}
private applySpotDepth(depth: AsterDepth): void {
private applySpotDepth(depth: Depth): void {
if (!depth?.bids?.length || !depth?.asks?.length) {
return;
}
@@ -395,7 +396,7 @@ export class BasisArbEngine {
this.emitUpdate();
}
private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void {
private applyAccountSnapshot(snapshot: AccountSnapshot): void {
const assets = Array.isArray(snapshot.assets) ? snapshot.assets : [];
const spotBalances: SpotBalanceStateEntry[] = [];
@@ -409,7 +410,7 @@ export class BasisArbEngine {
if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue;
if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue;
const isTaggedFuturesAsset = /0$/.test(name);
const isTaggedFuturesAsset = name.endsWith("0");
if (isTaggedFuturesAsset) {
futuresBalances.push({ asset: name.replace(/0$/, ""), wallet, available });
continue;
+10 -10
View File
@@ -1,5 +1,5 @@
import NodeWebSocket from "ws";
import type { AsterDepthLevel } from "../../exchanges/types";
import type { DepthLevel } 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: AsterDepthLevel[];
asks: AsterDepthLevel[];
bids: DepthLevel[];
asks: DepthLevel[];
}
interface DepthSnapshotResponse {
lastUpdateId: number;
bids: AsterDepthLevel[];
asks: AsterDepthLevel[];
bids: DepthLevel[];
asks: DepthLevel[];
}
export class BinanceDepthTracker {
@@ -540,7 +540,7 @@ export class BinanceDepthTracker {
return true;
}
private applyLevels(book: Map<string, number>, levels: AsterDepthLevel[]): void {
private applyLevels(book: Map<string, number>, levels: DepthLevel[]): void {
for (const level of levels) {
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 AsterDepthLevel[]) : [];
const asks = Array.isArray(json.asks) ? (json.asks as AsterDepthLevel[]) : [];
const bids = Array.isArray(json.bids) ? (json.bids as DepthLevel[]) : [];
const asks = Array.isArray(json.asks) ? (json.asks as DepthLevel[]) : [];
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 AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[];
const asks = asksRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[];
const bids = bidsRaw.filter((level): level is DepthLevel => Array.isArray(level)) as DepthLevel[];
const asks = asksRaw.filter((level): level is DepthLevel => Array.isArray(level)) as DepthLevel[];
return { U, u, bids, asks };
} catch {
+70 -16
View File
@@ -1,28 +1,80 @@
import { promises as fs } from "fs";
import path from "path";
import type { GridDirection } from "../../config";
import type { LevelPhase, StoredGridStateV2, StoredLevelV2 } from "../grid-logic";
const DATA_DIR = process.env.GRID_DATA_DIR?.trim() || path.resolve("data");
const GRID_FILE = path.resolve(DATA_DIR, "grid-record.json");
export type { LevelPhase, StoredGridStateV2, StoredLevelV2 };
export interface StoredGridState {
// 惰性解析,测试可通过 GRID_DATA_DIR 切换目录
function dataDir(): string {
return process.env.GRID_DATA_DIR?.trim() || path.resolve("data");
}
function gridFile(): string {
return path.resolve(dataDir(), "grid-record.json");
}
/** v1 遗留格式(无 schemaVersion 字段) */
interface StoredGridStateV1 {
symbol: string;
lowerPrice: number;
upperPrice: number;
gridLevels: number;
orderSize: number;
maxPositionSize: number;
direction: GridDirection;
longExposure: Record<string, number>;
shortExposure: Record<string, number>;
direction: string;
levels: Record<
string,
{ state: "idle" | "filled" | "exit_placed"; sourceLevel: number; targetLevel: number | null; exitOrderId?: string }
>;
updatedAt: number;
}
type GridStateMap = Record<string, StoredGridState>;
type StoredGridStateAny = StoredGridStateV1 | StoredGridStateV2;
type GridStateMap = Record<string, StoredGridStateAny>;
function isV2(entry: StoredGridStateAny): entry is StoredGridStateV2 {
return (entry as StoredGridStateV2).schemaVersion === 2;
}
/** v1 → v2filled→holding、exit_placed→exit_placedholdQty 取 orderSize,锚定价缺失由引擎补齐 */
export function migrateV1ToV2(v1: StoredGridStateV1): StoredGridStateV2 {
const levels: Record<string, StoredLevelV2> = {};
for (const [key, info] of Object.entries(v1.levels ?? {})) {
if (!info || info.state === "idle") continue;
const phase: LevelPhase = info.state === "filled" ? "holding" : "exit_placed";
const entry: StoredLevelV2 = {
phase,
exitTarget: info.targetLevel ?? null,
holdQty: Number.isFinite(v1.orderSize) ? v1.orderSize : 0,
};
if (info.exitOrderId) entry.exitOrderId = info.exitOrderId;
levels[key] = entry;
}
return {
schemaVersion: 2,
symbol: v1.symbol,
exchangeId: "",
gridVersion: 1,
anchorPrice: null,
lowerPrice: v1.lowerPrice,
upperPrice: v1.upperPrice,
gridLevels: v1.gridLevels,
orderSize: v1.orderSize,
maxPositionSize: v1.maxPositionSize,
direction: v1.direction,
gridMode: "geometric",
levels,
intents: [],
inflight: null,
shift: null,
exchangeStop: null,
updatedAt: v1.updatedAt ?? 0,
};
}
async function ensureDataDir(): Promise<void> {
try {
await fs.mkdir(DATA_DIR, { recursive: true });
await fs.mkdir(dataDir(), { recursive: true });
} catch {
// ignore
}
@@ -30,7 +82,7 @@ async function ensureDataDir(): Promise<void> {
async function readStateFile(): Promise<GridStateMap> {
try {
const content = await fs.readFile(GRID_FILE, "utf8");
const content = await fs.readFile(gridFile(), "utf8");
const parsed = JSON.parse(content);
if (parsed && typeof parsed === "object") {
return parsed as GridStateMap;
@@ -44,17 +96,19 @@ async function readStateFile(): Promise<GridStateMap> {
}
}
export async function loadGridState(symbol: string): Promise<StoredGridState | null> {
export async function loadGridState(symbol: string): Promise<StoredGridStateV2 | null> {
const map = await readStateFile();
const snapshot = map[symbol];
return snapshot ?? null;
if (!snapshot) return null;
if (isV2(snapshot)) return snapshot;
return migrateV1ToV2(snapshot);
}
export async function saveGridState(snapshot: StoredGridState): Promise<void> {
export async function saveGridState(snapshot: StoredGridStateV2): Promise<void> {
await ensureDataDir();
const map = await readStateFile();
map[snapshot.symbol] = snapshot;
await fs.writeFile(GRID_FILE, JSON.stringify(map, null, 2), "utf8");
await fs.writeFile(gridFile(), JSON.stringify(map, null, 2), "utf8");
}
export async function clearGridState(symbol: string): Promise<void> {
@@ -66,7 +120,7 @@ export async function clearGridState(symbol: string): Promise<void> {
const entries = Object.keys(map);
if (!entries.length) {
try {
await fs.unlink(GRID_FILE);
await fs.unlink(gridFile());
} catch (error: any) {
if (!error || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
throw error;
@@ -75,5 +129,5 @@ export async function clearGridState(symbol: string): Promise<void> {
return;
}
await ensureDataDir();
await fs.writeFile(GRID_FILE, JSON.stringify(map, null, 2), "utf8");
await fs.writeFile(gridFile(), JSON.stringify(map, null, 2), "utf8");
}
+840 -1061
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -1,6 +1,6 @@
import type { TradingConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter";
import type { AsterAccountSnapshot, AsterOrder, AsterTicker } from "../exchanges/types";
import type { AccountSnapshot, Order, Ticker } from "../exchanges/types";
import {
calcStopLossPrice,
calcTrailingActivationPrice,
@@ -30,11 +30,11 @@ export interface GuardianEngineSnapshot {
unrealized: number;
targetStopPrice: number | null;
trailingActivationPrice: number | null;
stopOrder: AsterOrder | null;
trailingOrder: AsterOrder | null;
stopOrder: Order | null;
trailingOrder: Order | null;
requiresStop: boolean;
tradeLog: TradeLogEntry[];
openOrders: AsterOrder[];
openOrders: Order[];
lastUpdated: number | null;
guardStatus: "idle" | "protecting" | "pending";
}
@@ -43,9 +43,9 @@ type GuardianEngineEvent = "update";
type GuardianEngineListener = (snapshot: GuardianEngineSnapshot) => void;
export class GuardianEngine {
private accountSnapshot: AsterAccountSnapshot | null = null;
private openOrders: AsterOrder[] = [];
private tickerSnapshot: AsterTicker | null = null;
private accountSnapshot: AccountSnapshot | null = null;
private openOrders: Order[] = [];
private tickerSnapshot: Ticker | 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<AsterAccountSnapshot>(
safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange),
(snapshot) => {
this.accountSnapshot = snapshot;
@@ -114,7 +114,7 @@ export class GuardianEngine {
}
);
safeSubscribe<AsterOrder[]>(
safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange),
(orders) => {
this.synchronizeLocks(orders);
@@ -141,7 +141,7 @@ export class GuardianEngine {
}
);
safeSubscribe<AsterTicker>(
safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => {
this.tickerSnapshot = ticker;
@@ -155,7 +155,7 @@ export class GuardianEngine {
);
}
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
private synchronizeLocks(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : [];
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?: AsterOrder;
currentTrailing?: AsterOrder;
currentStop?: Order;
currentTrailing?: Order;
}): 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: AsterOrder,
currentOrder: Order,
nextStopPrice: number,
lastPrice: number
): Promise<void> {
@@ -559,7 +559,7 @@ export class GuardianEngine {
}
}
private isProtectiveOrder(order: AsterOrder): boolean {
private isProtectiveOrder(order: Order): boolean {
if (order.symbol !== this.config.symbol) {
return false;
}
@@ -571,14 +571,14 @@ export class GuardianEngine {
return type === "STOP_MARKET" || hasStopPrice;
}
private findStopOrder(side: "BUY" | "SELL"): AsterOrder | undefined {
private findStopOrder(side: "BUY" | "SELL"): Order | 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"): AsterOrder | undefined {
private findTrailingOrder(side: "BUY" | "SELL"): Order | undefined {
return this.openOrders.find((order) => order.type === "TRAILING_STOP_MARKET" && order.side === side);
}
+20 -20
View File
@@ -1,11 +1,11 @@
import type { LiquidityMakerConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter";
import type {
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountSnapshot,
Depth,
Kline,
Order,
Ticker,
} 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: AsterAccountSnapshot | null = null;
private depthSnapshot: AsterDepth | null = null;
private tickerSnapshot: AsterTicker | null = null;
private lastKline: AsterKline | null = null;
private accountSnapshot: AccountSnapshot | null = null;
private depthSnapshot: Depth | null = null;
private tickerSnapshot: Ticker | null = null;
private lastKline: Kline | null = null;
private liveCandle: { startMs: number; open: number; close: number } | null = null;
private openOrders: AsterOrder[] = [];
private openOrders: Order[] = [];
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<AsterAccountSnapshot>(
safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange),
(snapshot) => {
this.accountSnapshot = snapshot;
@@ -224,7 +224,7 @@ export class LiquidityMakerEngine {
}
);
safeSubscribe<AsterOrder[]>(
safeSubscribe<Order[]>(
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: AsterOrder[] = [];
const activeOrders: Order[] = [];
if (Array.isArray(orders)) {
for (const order of orders) {
@@ -268,7 +268,7 @@ export class LiquidityMakerEngine {
}
);
safeSubscribe<AsterDepth>(
safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => {
this.depthSnapshot = depth;
@@ -282,7 +282,7 @@ export class LiquidityMakerEngine {
}
);
safeSubscribe<AsterTicker>(
safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => {
this.tickerSnapshot = ticker;
@@ -296,7 +296,7 @@ export class LiquidityMakerEngine {
}
);
safeSubscribe<AsterKline[]>(
safeSubscribe<Kline[]>(
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
(klines) => {
if (!Array.isArray(klines) || !klines.length) return;
@@ -318,7 +318,7 @@ export class LiquidityMakerEngine {
}
/** 检测成交订单 */
private detectFills(orders: AsterOrder[] | null | undefined): void {
private detectFills(orders: Order[] | null | undefined): void {
if (!Array.isArray(orders)) return;
// 查找已成交或部分成交的订单
@@ -357,7 +357,7 @@ export class LiquidityMakerEngine {
}
}
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type];
@@ -759,7 +759,7 @@ export class LiquidityMakerEngine {
* depthImbalanceRatio
* 簿
*/
private evaluateDepth(depth: AsterDepth): {
private evaluateDepth(depth: Depth): {
buySum: number;
sellSum: number;
skipBuySide: boolean;
@@ -1186,7 +1186,7 @@ export class LiquidityMakerEngine {
return position;
}
private getSpotBalances(snapshot: AsterAccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
private getSpotBalances(snapshot: AccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
const assets = snapshot?.assets ?? [];
if (!assets.length) return null;
const parsed = parseSymbolParts(this.config.symbol);
+14 -15
View File
@@ -1,11 +1,10 @@
import type { MakerConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter";
import type {
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountSnapshot,
Depth,
Order,
Ticker,
} from "../exchanges/types";
import { formatPriceToString } from "../utils/math";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
@@ -48,7 +47,7 @@ export interface MakerEngineSnapshot {
pnl: number;
accountUnrealized: number;
sessionVolume: number;
openOrders: AsterOrder[];
openOrders: Order[];
desiredOrders: DesiredOrder[];
tradeLog: TradeLogEntry[];
lastUpdated: number | null;
@@ -67,10 +66,10 @@ const EPS = 1e-5;
const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000;
export class MakerEngine {
private accountSnapshot: AsterAccountSnapshot | null = null;
private depthSnapshot: AsterDepth | null = null;
private tickerSnapshot: AsterTicker | null = null;
private openOrders: AsterOrder[] = [];
private accountSnapshot: AccountSnapshot | null = null;
private depthSnapshot: Depth | null = null;
private tickerSnapshot: Ticker | null = null;
private openOrders: Order[] = [];
private readonly locks: OrderLockMap = {};
private readonly timers: OrderTimerMap = {};
@@ -155,7 +154,7 @@ export class MakerEngine {
private bootstrap(): void {
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
safeSubscribe<AsterAccountSnapshot>(
safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange),
(snapshot) => {
this.accountSnapshot = snapshot;
@@ -179,7 +178,7 @@ export class MakerEngine {
}
);
safeSubscribe<AsterOrder[]>(
safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange),
(orders) => {
this.syncLocksWithOrders(orders);
@@ -212,7 +211,7 @@ export class MakerEngine {
}
);
safeSubscribe<AsterDepth>(
safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => {
this.depthSnapshot = depth;
@@ -230,7 +229,7 @@ export class MakerEngine {
}
);
safeSubscribe<AsterTicker>(
safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => {
this.tickerSnapshot = ticker;
@@ -251,7 +250,7 @@ export class MakerEngine {
// Maker strategy does not require realtime klines.
}
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type];
+20 -20
View File
@@ -1,10 +1,10 @@
import type { MakerPointsConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter";
import type {
AsterAccountSnapshot,
AsterDepth,
AsterOrder,
AsterTicker,
AccountSnapshot,
Depth,
Order,
Ticker,
} 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: AsterOrder[];
openOrders: Order[];
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: AsterAccountSnapshot | null = null;
private depthSnapshot: AsterDepth | null = null;
private tickerSnapshot: AsterTicker | null = null;
private openOrders: AsterOrder[] = [];
private accountSnapshot: AccountSnapshot | null = null;
private depthSnapshot: Depth | null = null;
private tickerSnapshot: Ticker | null = null;
private openOrders: Order[] = [];
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<AsterAccountSnapshot>(
safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange),
(snapshot) => {
this.applyAccountSnapshot(snapshot);
@@ -310,7 +310,7 @@ export class MakerPointsEngine {
}
);
safeSubscribe<AsterOrder[]>(
safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange),
(orders) => {
this.syncLocksWithOrders(orders);
@@ -339,7 +339,7 @@ export class MakerPointsEngine {
}
);
safeSubscribe<AsterDepth>(
safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => {
this.depthSnapshot = depth;
@@ -358,7 +358,7 @@ export class MakerPointsEngine {
}
);
safeSubscribe<AsterTicker>(
safeSubscribe<Ticker>(
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: AsterAccountSnapshot): void {
private applyAccountSnapshot(snapshot: AccountSnapshot): void {
this.accountSnapshot = snapshot;
// StandX: WS 推送使用本地接收时间戳;REST 快照使用响应里的 time 字段映射到 snapshot.updateTime
this.lastStandxAccountTime =
@@ -545,7 +545,7 @@ export class MakerPointsEngine {
}
}
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : [];
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: AsterDepth | null;
depth: Depth | null;
}): DesiredOrder[] {
const { bid1, ask1, skipBuy, skipSell, depth } = params;
@@ -837,7 +837,7 @@ export class MakerPointsEngine {
*
*/
private checkDepthStatusChanged(
depth: AsterDepth | null,
depth: Depth | null,
bid1: number,
ask1: number
): boolean {
@@ -879,7 +879,7 @@ export class MakerPointsEngine {
/**
*
*/
private shouldTriggerImmediateDepthProtection(depth: AsterDepth | null): boolean {
private shouldTriggerImmediateDepthProtection(depth: Depth | null): boolean {
if (!depth) return false;
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
@@ -917,7 +917,7 @@ export class MakerPointsEngine {
/**
* minRepriceBps
*/
private shouldTriggerImmediateReprice(depth: AsterDepth | null): boolean {
private shouldTriggerImmediateReprice(depth: Depth | 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: AsterAccountSnapshot | null): string | null {
private getStandxMarginMode(snapshot: AccountSnapshot | null): string | null {
if (this.exchange.id !== "standx") return null;
const positions = snapshot?.positions ?? [];
const match = positions.find((pos) => pos.symbol === this.config.symbol);
+18 -18
View File
@@ -1,11 +1,11 @@
import type { MakerConfig } from "../config";
import type { ExchangeAdapter } from "../exchanges/adapter";
import type {
AsterAccountSnapshot,
AsterDepth,
AsterKline,
AsterOrder,
AsterTicker,
AccountSnapshot,
Depth,
Kline,
Order,
Ticker,
} 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: AsterAccountSnapshot | null = null;
private depthSnapshot: AsterDepth | null = null;
private tickerSnapshot: AsterTicker | null = null;
private lastKline: AsterKline | null = null;
private accountSnapshot: AccountSnapshot | null = null;
private depthSnapshot: Depth | null = null;
private tickerSnapshot: Ticker | null = null;
private lastKline: Kline | null = null;
private liveCandle: { startMs: number; open: number; close: number } | null = null;
private openOrders: AsterOrder[] = [];
private openOrders: Order[] = [];
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<AsterAccountSnapshot>(
safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange),
(snapshot) => {
this.accountSnapshot = snapshot;
@@ -207,7 +207,7 @@ export class OffsetMakerEngine {
}
);
safeSubscribe<AsterOrder[]>(
safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange),
(orders) => {
this.syncLocksWithOrders(orders);
@@ -236,7 +236,7 @@ export class OffsetMakerEngine {
}
);
safeSubscribe<AsterDepth>(
safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => {
this.depthSnapshot = depth;
@@ -250,7 +250,7 @@ export class OffsetMakerEngine {
}
);
safeSubscribe<AsterTicker>(
safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => {
this.tickerSnapshot = ticker;
@@ -264,7 +264,7 @@ export class OffsetMakerEngine {
}
);
safeSubscribe<AsterKline[]>(
safeSubscribe<Kline[]>(
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
(klines) => {
if (!Array.isArray(klines) || !klines.length) return;
@@ -284,7 +284,7 @@ export class OffsetMakerEngine {
);
}
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : [];
Object.keys(this.pending).forEach((type) => {
const pendingId = this.pending[type];
@@ -600,7 +600,7 @@ export class OffsetMakerEngine {
}
}
private evaluateDepth(depth: AsterDepth): {
private evaluateDepth(depth: Depth): {
buySum: number;
sellSum: number;
skipBuySide: boolean;
@@ -1038,7 +1038,7 @@ export class OffsetMakerEngine {
return position;
}
private getSpotBalances(snapshot: AsterAccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
private getSpotBalances(snapshot: AccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
const assets = snapshot?.assets ?? [];
if (!assets.length) return null;
const parsed = parseSymbolParts(this.config.symbol);
+14 -15
View File
@@ -1,5 +1,5 @@
import type { ExchangeAdapter } from "../exchanges/adapter";
import type { AsterAccountSnapshot, AsterDepth, AsterOrder, AsterTicker } from "../exchanges/types";
import type { AccountSnapshot, Depth, Order, Ticker } from "../exchanges/types";
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { 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: AsterOrder[];
depth: AsterDepth | null;
ticker: AsterTicker | null;
openOrders: Order[];
depth: Depth | null;
ticker: Ticker | null;
tradeLog: TradeLogEntry[];
lastUpdated: number | null;
@@ -70,10 +70,10 @@ type SwingListener = (snapshot: SwingEngineSnapshot) => void;
const EPS = 1e-5;
export class SwingEngine {
private accountSnapshot: AsterAccountSnapshot | null = null;
private openOrders: AsterOrder[] = [];
private depthSnapshot: AsterDepth | null = null;
private tickerSnapshot: AsterTicker | null = null;
private accountSnapshot: AccountSnapshot | null = null;
private openOrders: Order[] = [];
private depthSnapshot: Depth | null = null;
private tickerSnapshot: Ticker | 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<AsterAccountSnapshot>(
safeSubscribe<AccountSnapshot>(
this.exchange.watchAccount.bind(this.exchange),
(snapshot) => {
this.accountSnapshot = snapshot;
@@ -192,7 +192,7 @@ export class SwingEngine {
}
);
safeSubscribe<AsterOrder[]>(
safeSubscribe<Order[]>(
this.exchange.watchOrders.bind(this.exchange),
(orders) => {
this.synchronizeLocks(orders);
@@ -214,7 +214,7 @@ export class SwingEngine {
}
);
safeSubscribe<AsterDepth>(
safeSubscribe<Depth>(
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
(depth) => {
this.depthSnapshot = depth;
@@ -227,7 +227,7 @@ export class SwingEngine {
}
);
safeSubscribe<AsterTicker>(
safeSubscribe<Ticker>(
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
(ticker) => {
this.tickerSnapshot = ticker;
@@ -241,7 +241,7 @@ export class SwingEngine {
);
}
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
private synchronizeLocks(orders: Order[] | null | undefined): void {
const list = Array.isArray(orders) ? orders : [];
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 ?? Number(this.tickerSnapshot?.lastPrice) ?? null;
const lastPrice = referencePrice;
// Kill-switch (always-on).
const triggerKill =
@@ -621,4 +621,3 @@ export class SwingEngine {
});
}
}

Some files were not shown because too many files have changed in this diff Show More