perp-cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +129 -535
  4. package/README.zh.md +243 -0
  5. package/dist/api/public/lighter.d.ts +5 -0
  6. package/dist/api/public/lighter.js +5 -0
  7. package/dist/api/public/pacifica.js +2 -2
  8. package/dist/arb/sizing.js +4 -2
  9. package/dist/arb/state.d.ts +6 -0
  10. package/dist/balance-by-address.d.ts +39 -0
  11. package/dist/balance-by-address.js +44 -0
  12. package/dist/bot/quote-rules.js +1 -1
  13. package/dist/bot/strategies/funding-arb-strategy.js +10 -3
  14. package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
  15. package/dist/bot/strategies/funding-arb-utils.js +48 -15
  16. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  17. package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
  18. package/dist/bot/strategies/spot-perp-executor.js +2 -2
  19. package/dist/cli-program.js +1 -1
  20. package/dist/commands/account.js +168 -24
  21. package/dist/commands/arb/index.js +19 -77
  22. package/dist/commands/arb-auto.d.ts +24 -0
  23. package/dist/commands/arb-auto.js +155 -81
  24. package/dist/commands/arb.d.ts +8 -0
  25. package/dist/commands/arb.js +49 -23
  26. package/dist/commands/backtest.js +23 -21
  27. package/dist/commands/market.d.ts +48 -0
  28. package/dist/commands/market.js +241 -78
  29. package/dist/commands/options.d.ts +5 -0
  30. package/dist/commands/options.js +20 -3
  31. package/dist/commands/pacifica-earn.js +8 -21
  32. package/dist/commands/portfolio.d.ts +16 -0
  33. package/dist/commands/portfolio.js +51 -8
  34. package/dist/commands/trade.js +4 -4
  35. package/dist/cross-chain-margin.d.ts +3 -1
  36. package/dist/cross-chain-margin.js +4 -2
  37. package/dist/dashboard/server.js +2 -2
  38. package/dist/dashboard/ui.js +9 -6
  39. package/dist/errors.d.ts +5 -0
  40. package/dist/errors.js +10 -0
  41. package/dist/exchanges/address.d.ts +8 -0
  42. package/dist/exchanges/address.js +47 -0
  43. package/dist/exchanges/arcus/adapter.d.ts +10 -6
  44. package/dist/exchanges/arcus/adapter.js +33 -6
  45. package/dist/exchanges/arcus/public.js +3 -2
  46. package/dist/exchanges/aster.d.ts +27 -7
  47. package/dist/exchanges/aster.js +124 -42
  48. package/dist/exchanges/canonical-symbols.d.ts +7 -0
  49. package/dist/exchanges/canonical-symbols.js +20 -0
  50. package/dist/exchanges/capabilities.d.ts +2 -0
  51. package/dist/exchanges/capabilities.js +2 -1
  52. package/dist/exchanges/derive/options.d.ts +5 -0
  53. package/dist/exchanges/derive/options.js +20 -1
  54. package/dist/exchanges/extended/adapter.d.ts +16 -2
  55. package/dist/exchanges/extended/adapter.js +96 -34
  56. package/dist/exchanges/extended/public.d.ts +3 -0
  57. package/dist/exchanges/extended/public.js +9 -2
  58. package/dist/exchanges/grvt/adapter.d.ts +16 -2
  59. package/dist/exchanges/grvt/adapter.js +98 -21
  60. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  61. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  62. package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
  63. package/dist/exchanges/hyperliquid-spot.js +20 -12
  64. package/dist/exchanges/hyperliquid.d.ts +180 -13
  65. package/dist/exchanges/hyperliquid.js +487 -185
  66. package/dist/exchanges/interface.d.ts +64 -8
  67. package/dist/exchanges/interface.js +31 -0
  68. package/dist/exchanges/lighter-public.d.ts +66 -11
  69. package/dist/exchanges/lighter-public.js +249 -81
  70. package/dist/exchanges/lighter-spot.d.ts +7 -1
  71. package/dist/exchanges/lighter-spot.js +99 -90
  72. package/dist/exchanges/lighter.d.ts +70 -11
  73. package/dist/exchanges/lighter.js +285 -92
  74. package/dist/exchanges/ondo/adapter.d.ts +34 -8
  75. package/dist/exchanges/ondo/adapter.js +73 -16
  76. package/dist/exchanges/ondo/public.d.ts +27 -2
  77. package/dist/exchanges/ondo/public.js +63 -31
  78. package/dist/exchanges/outcome-interface.d.ts +4 -0
  79. package/dist/exchanges/pacifica-spot.d.ts +6 -0
  80. package/dist/exchanges/pacifica-spot.js +23 -15
  81. package/dist/exchanges/pacifica-vaults.d.ts +9 -0
  82. package/dist/exchanges/pacifica-vaults.js +14 -1
  83. package/dist/exchanges/pacifica.d.ts +77 -6
  84. package/dist/exchanges/pacifica.js +321 -130
  85. package/dist/exchanges/public-balances.d.ts +25 -0
  86. package/dist/exchanges/public-balances.js +158 -0
  87. package/dist/exchanges/public-positions.d.ts +43 -0
  88. package/dist/exchanges/public-positions.js +154 -0
  89. package/dist/exchanges/qfex/adapter.d.ts +52 -9
  90. package/dist/exchanges/qfex/adapter.js +74 -9
  91. package/dist/exchanges/qfex/public.d.ts +62 -9
  92. package/dist/exchanges/qfex/public.js +128 -22
  93. package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
  94. package/dist/exchanges/risex/adapter.d.ts +19 -7
  95. package/dist/exchanges/risex/adapter.js +58 -12
  96. package/dist/exchanges/spot-factory.js +1 -1
  97. package/dist/exchanges/txflow/adapter.d.ts +14 -7
  98. package/dist/exchanges/txflow/adapter.js +51 -19
  99. package/dist/exchanges/txflow/client.js +3 -1
  100. package/dist/exchanges/txflow/public.d.ts +12 -1
  101. package/dist/exchanges/txflow/public.js +82 -11
  102. package/dist/exchanges/unknown-symbol.d.ts +6 -1
  103. package/dist/exchanges/unknown-symbol.js +7 -2
  104. package/dist/exchanges/variational/adapter.d.ts +2 -6
  105. package/dist/exchanges/venue-types.d.ts +46 -3
  106. package/dist/exchanges/venue-types.js +80 -9
  107. package/dist/exchanges/venue.d.ts +99 -3
  108. package/dist/exchanges/venue.js +72 -0
  109. package/dist/exchanges/venues/hyperliquid.js +9 -0
  110. package/dist/exchanges/venues/lighter-rh.js +13 -3
  111. package/dist/exchanges/venues/lighter.js +13 -3
  112. package/dist/exchanges/venues/pacifica.js +9 -0
  113. package/dist/exchanges/venues/risex.js +15 -0
  114. package/dist/funding/legacy-public.d.ts +5 -3
  115. package/dist/funding/legacy-public.js +33 -37
  116. package/dist/funding/rates.d.ts +54 -5
  117. package/dist/funding/rates.js +49 -13
  118. package/dist/liquidity.d.ts +3 -1
  119. package/dist/liquidity.js +8 -4
  120. package/dist/mcp-pacifica-earn.js +11 -5
  121. package/dist/mcp-server.js +104 -15
  122. package/dist/pacifica/client.d.ts +2 -1
  123. package/dist/pacifica/client.js +11 -0
  124. package/dist/positions-by-address.d.ts +34 -0
  125. package/dist/positions-by-address.js +46 -0
  126. package/dist/strategies/funding-arb.js +35 -62
  127. package/dist/utils.d.ts +11 -1
  128. package/dist/venue-health.js +1 -1
  129. package/dist/ws/arcus-market.d.ts +9 -0
  130. package/dist/ws/arcus-market.js +9 -0
  131. package/dist/ws/aster-market.d.ts +33 -0
  132. package/dist/ws/aster-market.js +30 -0
  133. package/dist/ws/index.d.ts +30 -9
  134. package/dist/ws/index.js +180 -15
  135. package/dist/ws/lighter-market.d.ts +14 -0
  136. package/dist/ws/lighter-market.js +13 -0
  137. package/dist/ws/node.d.ts +30 -9
  138. package/dist/ws/node.js +204 -16
  139. package/dist/ws/unclassified.d.ts +16 -0
  140. package/dist/ws/unclassified.js +43 -0
  141. package/package.json +15 -2
  142. package/skills/perp-cli/SKILL.md +97 -33
  143. package/skills/perp-cli/package.json +15 -2
  144. package/skills/perp-cli/references/agent-operations.md +15 -12
  145. package/skills/perp-cli/references/commands.md +67 -12
  146. package/skills/perp-cli/references/strategies.md +38 -3
  147. package/skills/perp-cli/references/ws-library.md +55 -24
  148. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  149. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  150. package/skills/perp-cli/scripts/preflight.sh +27 -19
  151. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  152. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  153. package/dist/cli-spec.d.ts +0 -46
  154. package/dist/cli-spec.js +0 -81
package/README.zh.md ADDED
@@ -0,0 +1,243 @@
1
+ [English](README.md) | [한국어](README.ko.md) | **中文** | [日本語](README.ja.md)
2
+
3
+ > 以英文版 [README](README.md) 为准;本译文可能滞后于英文版。
4
+
5
+ # perp-cli
6
+
7
+ [![npm version](https://img.shields.io/npm/v/perp-cli.svg)](https://www.npmjs.com/package/perp-cli)
8
+ [![npm downloads](https://img.shields.io/npm/dw/perp-cli.svg)](https://www.npmjs.com/package/perp-cli)
9
+ [![license](https://img.shields.io/npm/l/perp-cli.svg)](https://github.com/hypurrquant/perp-cli/blob/main/LICENSE)
10
+
11
+ 一个 CLI 搞定多个 DEX 的永续合约 — 在终端中交易、比较资金费率、调拨资金和运行机器人,或交给 AI agent 使用(JSON 输出、MCP 服务器、agent 技能)。
12
+
13
+ - **8 个可交易场所:** Hyperliquid(含 HIP-3 dex)、Pacifica、Lighter、Lighter RH、Aster、Ondo Perps、Arcus、QFEX(跨链桥和永续-永续套利覆盖最初的四个:Pacifica、Hyperliquid、Lighter、Aster)
14
+ - **仅市场数据:** RISEx、GRVT、Extended、Variational、TxFlow(非官方 API)— 另有 Derive 期权数据
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ npm install -g perp-cli
20
+ perp --version
21
+
22
+ # Or without a global install
23
+ npx -y perp-cli --json portfolio
24
+ ```
25
+
26
+ 本软件包附带三个可执行文件:`perp`(CLI 本体)、`perp-mcp`([MCP 服务器](docs/mcp.md),stdio)和 `perp-guardrail`([签名护栏](docs/guardrail.md))。
27
+
28
+ ## 快速开始
29
+
30
+ **1. 先看看 — 无需密钥。**
31
+
32
+ ```bash
33
+ perp -e hl market list # markets, prices, funding
34
+ perp -e hl market book BTC # order book
35
+ perp arb scan --rates # funding rates across venues
36
+ ```
37
+
38
+ **2. 添加钱包。** 设置向导会创建或导入一个加密的 [OWS](https://github.com/open-wallet-standard) vault 钱包,选择交易场所并注册 agent 密钥(每次签名前都会先询问,且绝不会移动资金):
39
+
40
+ ```bash
41
+ perp setup
42
+ perp wallet show
43
+ ```
44
+
45
+ 密钥从隐藏输入提示或 stdin 读取 — 绝不从 argv 或 `~/.perp/.env` 读取。一个 EVM 密钥即可覆盖 Hyperliquid、Lighter 和 Aster。其他方式(单行导入密钥、可脚本化的设置、在交易所网站上创建的密钥)请参见 [docs/setup.md](docs/setup.md)。
46
+
47
+ **3. 查看账户并交易。** 在任意命令后加上 `-n testnet` 即可使用测试网。
48
+
49
+ ```bash
50
+ perp --json portfolio # balances + positions + risk on every venue
51
+ perp --json --dry-run -e hl trade market BTC buy 0.001 # validate only, nothing is sent
52
+ perp --json -e hl trade market BTC buy 0.001 # market order (bounded by the slippage setting)
53
+ perp --json -e hl trade close BTC
54
+ ```
55
+
56
+ ## AI Agent Skill
57
+
58
+ 适用于 Claude Code、Cursor、Codex、Gemini CLI 及其他 agent CLI 的即插即用技能(skill)。内含非交互式命令指南、运维手册、策略框架以及可复用的 shell 脚本(预检、套利监控、校验),让你的 agent 在正确的确认节点停下,并且永远不会卡在交互式向导上。
59
+
60
+ ### 安装
61
+
62
+ ```bash
63
+ # Claude Code — slash command
64
+ /install-skill hypurrquant/perp-cli
65
+
66
+ # Cursor / Codex / generic agent CLIs — via npx
67
+ npx skills add hypurrquant/perp-cli
68
+
69
+ # Manual install (any agent that reads a Skills directory)
70
+ git clone https://github.com/hypurrquant/perp-cli.git
71
+ cp -R perp-cli/skills/perp-cli ~/.claude/skills/ # or your CLI's skills dir
72
+ ```
73
+
74
+ 该技能随 npm 包一同发布(`perp-cli/skills/perp-cli`),因此全局安装后也可在本地使用:
75
+
76
+ ```bash
77
+ npm install -g perp-cli@latest
78
+ ls "$(npm root -g)/perp-cli/skills/perp-cli"
79
+ ```
80
+
81
+ ### 技能包含的内容
82
+
83
+ | 文件 | 用途 |
84
+ |------|---------|
85
+ | `SKILL.md` | 顶层 agent 指南 — 规则、工作流、示例 |
86
+ | `references/commands.md` | 完整的非交互式命令参考(涵盖所有命令组) |
87
+ | `references/agent-operations.md` | 设置流程、幂等性、错误处理、常见错误 |
88
+ | `references/strategies.md` | 资金费率套利决策框架 + 风险管理 |
89
+ | `scripts/preflight.sh` | 安装 + 钱包 + 连通性 + 风险就绪检查 |
90
+ | `scripts/arb-monitor.sh` | 实时监控未平仓的套利持仓 |
91
+ | `scripts/spot-perp-scan.sh` | 结合余额信息扫描现货+永续机会 |
92
+ | `scripts/funding-analysis.sh` | 跨交易所资金费率概览 / 单交易对深入分析 |
93
+ | `scripts/validate-arb.sh` | 执行前校验(价格 / 余额 / 风险 / dry-run) |
94
+
95
+ 完整的 agent 指南请参见 [`skills/perp-cli/SKILL.md`](skills/perp-cli/SKILL.md)。
96
+
97
+ ## 功能特性
98
+
99
+ - **资金费率套利** — 永续-永续和现货-永续扫描,一条命令完成双腿开仓与平仓
100
+ - **投资组合** — 一次调用即可返回所有交易场所的余额、持仓和风险
101
+ - **资金** — 充值、提现、划转、多提供商跨链桥(CCTP / Relay / deBridge)、交易所间再平衡
102
+ - **机器人** — TWAP、网格、DCA、追踪止损等,支持后台任务管理
103
+ - **Agent 优先设计** — `--json`、`--fields`、`--ndjson`、`--dry-run`、运行时 schema 自省
104
+ - **安全** — 交易前校验、每笔市价单都有滑点上限、client-id 去重、响应净化
105
+
106
+ ## 交易场所
107
+
108
+ | 交易场所 | `-e` | 交易 | 链 / 抵押品 |
109
+ |---|---|---|---|
110
+ | Hyperliquid | `hyperliquid` (`hl`) | ✅ | HyperEVM,USDC;HIP-3 dex 通过 `--dex` |
111
+ | Pacifica | `pacifica` (`pac`) | ✅ | Solana,USDC |
112
+ | Lighter | `lighter` (`lt`) | ✅ | Ethereum L2,USDC |
113
+ | Lighter RH | `lighter-rh` (`lrh`) | ✅ | Robinhood chain,USDG |
114
+ | Aster | `aster` (`ast`) | ✅ | BNB Chain,USDT / USDC |
115
+ | Ondo Perps | `ondo` | ✅ | 链下,USDC |
116
+ | Arcus | `arcus` | ✅(访问白名单) | Robinhood chain,USDG |
117
+ | QFEX | `qfex` (`qf`) | ✅(仅限手动交易) | 托管;股票、指数、大宗商品 |
118
+ | RISEx | `risex` | 只读 | RISE chain,USDC |
119
+ | GRVT | `grvt` | 仅市场数据 | USDT |
120
+ | Extended | `extended` (`ext`) | 仅市场数据 | Starknet;加密货币 + 股票 / 外汇 |
121
+ | Variational | `variational` (`var`) | 仅市场数据 | RFQ 交易场所 |
122
+ | TxFlow | `txflow` (`txf`) | 仅市场数据 | **非官方** API |
123
+ | Derive | `derive` | 仅期权数据 | — |
124
+
125
+ - **[支持矩阵及各交易场所说明](docs/venues.md)** — 资金费率、WebSocket、充值 / 提现、按地址查询、限制地区
126
+ - **[主网验证](docs/verification.md)** — 哪些功能已用真实资金运行过
127
+ - **[交易细节](docs/trading.md)** — 订单有效期(TIF)、止盈/止损、止损单、滑点、现货交易、构建者费用
128
+
129
+ ## 常用命令
130
+
131
+ ```bash
132
+ # Market data
133
+ perp --json -e <EX> market list | book <SYM> | funding <SYM> | kline <SYM> 1h
134
+
135
+ # Trading
136
+ perp --json -e <EX> trade buy <SYM> <SIZE> --tp <PX> --sl <PX> # entry protected by TP/SL
137
+ perp --json -e <EX> trade limit <SYM> buy <PX> <SIZE>
138
+ perp --json -e <EX> trade close <SYM> # or: flatten (every position)
139
+
140
+ # Account
141
+ perp --json portfolio
142
+ perp --json -e <EX> account positions
143
+ perp --json account balance --address <ADDR> # any account, keyless
144
+
145
+ # Funding arb
146
+ perp --json arb scan --min 5
147
+ perp --json arb exec <SYM> <longEx> <shortEx> <USD>
148
+
149
+ # Funds
150
+ perp --json funds deposit hyperliquid 100
151
+ perp --json funds bridge send --from solana --to arbitrum --amount 100
152
+ ```
153
+
154
+ 包含所有命令组的完整列表:**[docs/commands.md](docs/commands.md)**。全局参数:`--json`、`--fields <f>`、`--ndjson`、`--dry-run`、`-n testnet`、`-e <venue>`、`--dex <name>`(HIP-3)、`-w, --wallet <name>`。
155
+
156
+ ## 命令分组
157
+
158
+ | 命令组 | 说明 |
159
+ |-------|-------------|
160
+ | `market` | 价格、订单簿、资金费率、K 线、HIP-3 dex |
161
+ | `account` | 余额、持仓、订单、保证金 |
162
+ | `trade` | 市价 / 限价 / 止损单、平仓、分批、拆单执行 |
163
+ | `outcome` | Hyperliquid Outcome(HIP-4)— 二元 / 区间合约,按 outcome 计价(outcomeMeta `quoteToken`,目前为 USDC),无杠杆 |
164
+ | `options` | 期权市场数据,只读(Derive:`-e derive options underlyings / expiries / chain / quote`) |
165
+ | `print` | Pacifica Print — 每个周期赚取权利金的行权价订单:games、quote、positions、history、events、deposit / cancel(`--dry-run`) |
166
+ | `vault` | Pacifica vaults(API 中称 lakes)— list、info、positions、deposit(仅限主钱包)/ withdraw(`--dry-run`) |
167
+ | `arb` | 资金费率套利 — 扫描、执行、平仓、监控(永续-永续 & 现货-永续) |
168
+ | `strategy` | 19 种机器人算法(grid、dca、twap、APEX、REFLECT、预设)+ 嵌套的脚本化计划 |
169
+ | `funds` | 充值、提现、划转、跨链桥(多提供商)、交易所间再平衡 |
170
+ | `risk` | 风险限额、强平距离、护栏 |
171
+ | `wallet` | 多钱包管理、agent 钱包(`wallet agent ...`)、构建者费用(`wallet builder ...`,默认关闭)、保证金模式 / 子账户 / API 密钥(`wallet manage ...`)、链上余额 |
172
+ | `history` | 执行日志、盈亏、绩效拆解 |
173
+ | `portfolio` | 跨交易所统一概览(取代原来的 `account balance` / `status` / `dashboard`;`account balance --address` 可无需密钥读取任意账户的余额) |
174
+ | `health` | 适配器健康检查:ping 每个交易场所的 REST 端点(连通性 + 延迟) |
175
+ | `settings` | CLI 设置(推荐码、默认值、手续费、构建者费用、deBridge affiliate) |
176
+ | `backtest` | 策略回测 |
177
+ | `background` | 后台进程管理器(用于策略、告警等的 tmux 会话) |
178
+ | `alerts` | 资金费率告警(Telegram / Discord),支持后台守护进程 |
179
+ | `setup` | 设置向导:OWS 钱包、交易场所、agent 钱包、滑点、默认交易场所(别名:`init`;脚本中使用 `--non-interactive`) |
180
+
181
+ ## Agent 优先的 CLI 设计
182
+
183
+ 遵循 [agent-first CLI principles](https://justin.poehnelt.com/posts/rewrite-your-cli-for-ai-agents/) 构建:
184
+
185
+ ```bash
186
+ # Every command returns structured JSON envelope
187
+ perp --json portfolio
188
+ # → { "ok": true, "data": {...}, "meta": { "timestamp": "..." } }
189
+
190
+ # Filter output to specific fields (saves tokens)
191
+ perp --json --fields totalEquity,risk portfolio
192
+
193
+ # Stream large lists as NDJSON (one JSON per line)
194
+ perp --json --ndjson -e hl market list
195
+
196
+ # Pre-validate before executing
197
+ perp --json -e hl trade check BTC buy 0.01
198
+ perp --json --dry-run -e hl trade market BTC buy 0.01
199
+
200
+ # Idempotent orders with client ID (Hyperliquid: a 128-bit cloid, 0x + 32 hex; --auto-id generates one)
201
+ perp --json -e hl trade market BTC buy 0.01 --client-id 0x1234567890abcdef1234567890abcdef
202
+ perp --json -e hl trade status c:0x1234567890abcdef1234567890abcdef
203
+ ```
204
+
205
+ 所有响应都会自动净化(去除控制字符,拦截提示词注入模式)。
206
+ 错误中包含 `retryable` 标志 — 仅当其为 `true` 时才重试。
207
+
208
+ ## MCP 服务器
209
+
210
+ [![Glama MCP server](https://glama.ai/mcp/servers/hypurrquant/perp-cli/badges/score.svg)](https://glama.ai/mcp/servers/hypurrquant/perp-cli)
211
+
212
+ `perp-mcp` 为 Claude Desktop、Cursor 及其他 MCP 客户端提供 31 个工具。查询市场数据无需密钥;如需账户和交易工具,请先运行 `perp setup`(服务器读取的是同一个钱包)。切勿在 MCP 配置中放入私钥。
213
+
214
+ ```json
215
+ {
216
+ "mcpServers": {
217
+ "perp-cli": {
218
+ "command": "npx",
219
+ "args": ["-y", "-p", "perp-cli", "perp-mcp"]
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ 测试网、完整工具列表、资源和提示词:**[docs/mcp.md](docs/mcp.md)**。
226
+
227
+ ## 文档
228
+
229
+ | 文档 | 内容 |
230
+ |---|---|
231
+ | [设置](docs/setup.md) | 钱包、agent 密钥、在交易所网站上创建的密钥、机密信息的存放位置 |
232
+ | [交易场所](docs/venues.md) | 支持矩阵、交易场所 id、QFEX / RISEx / GRVT / Extended / Variational / TxFlow / Derive 详情 |
233
+ | [交易](docs/trading.md) | 订单有效期(TIF)、止盈/止损、止损单、改单、滑点、现货交易、构建者费用 |
234
+ | [命令](docs/commands.md) | 所有命令组及示例、Telegram 告警 |
235
+ | [主网验证](docs/verification.md) | 各交易场所哪些功能已用真实资金运行过 |
236
+ | [MCP 服务器](docs/mcp.md) | 配置、测试网、工具、资源、提示词 |
237
+ | [签名护栏](docs/guardrail.md) | `perp-guardrail`,用于 agent 密钥的 OWS 策略可执行文件 |
238
+ | [测试](docs/TESTING.md) | 测试套件和交易场所检查清单 |
239
+ | [CHANGELOG](CHANGELOG.md) | 版本发布说明 |
240
+
241
+ ## 许可证
242
+
243
+ MIT
@@ -17,6 +17,11 @@ export interface LighterFundingEntry {
17
17
  }
18
18
  export declare function fetchLighterOrderBookDetails(): Promise<LighterMarketDetail[]>;
19
19
  export declare function fetchLighterOrderBookDetailsRaw(): Promise<unknown>;
20
+ /**
21
+ * GET /api/v1/funding-rates (Lighter's own rows): the PREDICTED rate × 8 — a fraction per 8 h, NOT
22
+ * the venue basis (per 1 h, the settled rate of the market rows). Not read by perp-cli's funding
23
+ * comparisons (they use fetchVenuesRates); kept for API compatibility. Normalize with 8 h.
24
+ */
20
25
  export declare function fetchLighterFundingRates(): Promise<LighterFundingEntry[]>;
21
26
  export declare function fetchLighterFundingRatesRaw(): Promise<unknown>;
22
27
  export declare function parseLighterRaw(detailsRaw: unknown, fundingRaw: unknown): {
@@ -37,6 +37,11 @@ export function fetchLighterOrderBookDetailsRaw() {
37
37
  return res.json();
38
38
  });
39
39
  }
40
+ /**
41
+ * GET /api/v1/funding-rates (Lighter's own rows): the PREDICTED rate × 8 — a fraction per 8 h, NOT
42
+ * the venue basis (per 1 h, the settled rate of the market rows). Not read by perp-cli's funding
43
+ * comparisons (they use fetchVenuesRates); kept for API compatibility. Normalize with 8 h.
44
+ */
40
45
  export function fetchLighterFundingRates() {
41
46
  // SSOT rule #2: error must propagate.
42
47
  // Row-level: rate is required (it's the field the endpoint exists for); a
@@ -82,7 +82,7 @@ export function fetchPacificaPrices() {
82
82
  const out = [];
83
83
  for (const p of data) {
84
84
  const symbol = String(p.symbol ?? "");
85
- const fundingRaw = p.next_funding ?? p.funding;
85
+ const fundingRaw = p.funding; // last settled hourly rate (the market row / WS `funding`), not next_funding
86
86
  const funding = Number(fundingRaw);
87
87
  const mark = Number(p.mark);
88
88
  if (!symbol || fundingRaw === undefined || !Number.isFinite(funding) || !Number.isFinite(mark) || mark <= 0) {
@@ -113,7 +113,7 @@ export function parsePacificaRaw(raw) {
113
113
  if (!sym)
114
114
  continue;
115
115
  // SSOT rule #2: skip rows missing real funding/mark; never publish 0.
116
- const fundingRaw = p.next_funding ?? p.funding;
116
+ const fundingRaw = p.funding; // last settled hourly rate (the market row / WS `funding`), not next_funding
117
117
  const fundingNumber = Number(fundingRaw);
118
118
  if (fundingRaw !== undefined && Number.isFinite(fundingNumber)) {
119
119
  rates.set(sym, fundingNumber);
@@ -5,7 +5,8 @@
5
5
  * @returns size string that works for both exchanges, or null if not viable
6
6
  */
7
7
  export function computeMatchedSize(sizeUsd, price, longExchange, shortExchange, opts) {
8
- if (price <= 0)
8
+ // NaN / Infinity (e.g. a "-" mark parsed as a number) would size to "NaN": no size.
9
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(sizeUsd))
9
10
  return null;
10
11
  const rawSize = sizeUsd / price;
11
12
  // Use explicitly provided decimals if available, else conservative defaults
@@ -105,7 +106,8 @@ function getMinNotional(exchange) {
105
106
  * Uses spot exchange's size decimals for the spot leg.
106
107
  */
107
108
  export function computeSpotPerpMatchedSize(sizeUsd, price, spotExchange, perpExchange, spotSizeDecimals, perpSizeDecimals) {
108
- if (price <= 0)
109
+ // NaN / Infinity (e.g. a "-" mark parsed as a number) would size to "NaN": no size.
110
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(sizeUsd))
109
111
  return null;
110
112
  // If explicit decimals provided, use them; otherwise derive from exchange
111
113
  const spotDec = spotSizeDecimals ?? getSizeDecimals(`spot:${spotExchange}`);
@@ -23,6 +23,12 @@ export interface ArbPositionState {
23
23
  spotExchange?: string;
24
24
  /** Spot symbol, e.g. "ETH/USDC" (for spot-perp mode) */
25
25
  spotSymbol?: string;
26
+ /**
27
+ * The legs' VENUE symbols when they differ from `symbol` (the canonical key): Hyperliquid
28
+ * kPEPE ↔ Lighter 1000PEPE. Absent = both legs trade as `symbol`.
29
+ */
30
+ longSymbol?: string;
31
+ shortSymbol?: string;
26
32
  }
27
33
  export interface ArbDaemonState {
28
34
  version: 1;
@@ -0,0 +1,39 @@
1
+ import { type Network, type PublicBalanceResult } from "./exchanges/venue.js";
2
+ /**
3
+ * The JSON shape of one venue's answer: `balance` and `spot` always (null = not published / per
4
+ * account), the venue-specific keys only on their venue (Lighter `accounts`, Hyperliquid `dexes` +
5
+ * `accountMode`).
6
+ */
7
+ export type BalanceData = Pick<PublicBalanceResult, "balance" | "spot"> & Partial<{
8
+ accounts: NonNullable<PublicBalanceResult["accounts"]>;
9
+ dexes: NonNullable<PublicBalanceResult["dexes"]>;
10
+ accountMode: string;
11
+ }>;
12
+ export interface AddressBalance {
13
+ venue: string;
14
+ data: BalanceData;
15
+ /** Sub-reads that failed (Hyperliquid HIP-3 dex), keyed "hip3:<dex>". */
16
+ errors: Record<string, {
17
+ code: string;
18
+ message: string;
19
+ }>;
20
+ notes: string[];
21
+ /** One human line per failed sub-read: its balance is unknown, not zero. */
22
+ warnings: string[];
23
+ }
24
+ export interface AddressBalanceOptions {
25
+ accountIndex?: number;
26
+ }
27
+ export declare function readBalanceByAddress(venue: string, net: Network, address: string, opts?: AddressBalanceOptions): Promise<AddressBalance>;
28
+ export interface AddressBalanceFanOut {
29
+ venues: Record<string, BalanceData>;
30
+ /** A venue that failed (`<venue>`) or a failed sub-read of a venue (`<venue>:hip3:<dex>`). */
31
+ errors: Record<string, {
32
+ code: string;
33
+ message: string;
34
+ }>;
35
+ notes: Record<string, string[]>;
36
+ warnings: string[];
37
+ }
38
+ /** Every venue that reads balances by address; a venue that fails (address format included) is an `errors` entry. */
39
+ export declare function readBalanceByAddressAll(net: Network, address: string, opts?: AddressBalanceOptions): Promise<AddressBalanceFanOut>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `perp account balance --address` and MCP get_balance_by_address: one read path over the
3
+ * registry (VenuePublicData.fetchBalanceByAddress). Keyless; nothing is signed.
4
+ */
5
+ import { PerpError } from "./errors.js";
6
+ import { fetchBalanceByAddress, getVenue, listPublicBalanceVenues } from "./exchanges/venue.js";
7
+ function dataOf(r) {
8
+ return {
9
+ balance: r.balance,
10
+ spot: r.spot,
11
+ ...(r.accounts !== null ? { accounts: r.accounts } : {}),
12
+ ...(r.dexes !== null ? { dexes: r.dexes } : {}),
13
+ ...(r.accountMode !== null ? { accountMode: r.accountMode } : {}),
14
+ };
15
+ }
16
+ function warningsOf(venue, errors) {
17
+ return Object.entries(errors).map(([src, e]) => `${venue} ${src} unavailable (${e.code}: ${e.message}) — its balance is missing from this answer (unknown, not zero)`);
18
+ }
19
+ export async function readBalanceByAddress(venue, net, address, opts = {}) {
20
+ const id = getVenue(venue).id;
21
+ const r = await fetchBalanceByAddress(id, net, address, { accountIndex: opts.accountIndex });
22
+ return { venue: id, data: dataOf(r), errors: r.errors, notes: r.notes, warnings: warningsOf(id, r.errors) };
23
+ }
24
+ /** Every venue that reads balances by address; a venue that fails (address format included) is an `errors` entry. */
25
+ export async function readBalanceByAddressAll(net, address, opts = {}) {
26
+ const venues = listPublicBalanceVenues().map(v => v.id);
27
+ const out = { venues: {}, errors: {}, notes: {}, warnings: [] };
28
+ const settled = await Promise.allSettled(venues.map(v => readBalanceByAddress(v, net, address, v === "lighter" || v === "lighter-rh" ? opts : {})));
29
+ settled.forEach((res, i) => {
30
+ const v = venues[i];
31
+ if (res.status === "rejected") {
32
+ const e = res.reason;
33
+ out.errors[v] = e instanceof PerpError ? { code: e.structured.code, message: e.structured.message } : { code: "EXCHANGE_ERROR", message: e instanceof Error ? e.message : String(e) };
34
+ return;
35
+ }
36
+ out.venues[v] = res.value.data;
37
+ for (const [src, e] of Object.entries(res.value.errors))
38
+ out.errors[`${v}:${src}`] = e;
39
+ if (res.value.notes.length > 0)
40
+ out.notes[v] = res.value.notes;
41
+ out.warnings.push(...res.value.warnings);
42
+ });
43
+ return out;
44
+ }
@@ -19,7 +19,7 @@ export async function quoteRulesOf(adapter, symbol) {
19
19
  if (network === null) {
20
20
  throw new PerpError("EXCHANGE_ERROR", `${adapter.name}: the adapter's network is unknown, so the ${symbol} tick / minimums cannot be read; nothing was sent`, { exchange: adapter.name });
21
21
  }
22
- const [markets, stepSize] = await Promise.all([getVenue(adapter.name).publicData.fetchMarkets(network), lotStepOf(adapter, symbol)]);
22
+ const [markets, stepSize] = await Promise.all([getVenue(adapter.name).publicData.fetchMarkets(network, { funding: false }), lotStepOf(adapter, symbol)]);
23
23
  const vm = markets.find(m => symbolMatch(m.symbol, symbol));
24
24
  return { tickSize: vm?.tickSize ?? null, stepSize, minSize: vm?.minSize ?? null, minNotional: vm?.minNotional ?? null };
25
25
  }
@@ -1064,9 +1064,16 @@ export class FundingArbStrategy {
1064
1064
  return false;
1065
1065
  }
1066
1066
  const perpSymbol = getPerpSymbol(opp.symbol, opp.shortExchange);
1067
- const priceEstimate = await getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
1068
- if (priceEstimate <= 0) {
1069
- ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: can't determine price`);
1067
+ let priceEstimate;
1068
+ try {
1069
+ priceEstimate = await getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
1070
+ }
1071
+ catch (err) {
1072
+ ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: market read failed: ${err instanceof Error ? err.message : String(err)}`);
1073
+ return false;
1074
+ }
1075
+ if (priceEstimate === null) {
1076
+ ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: no positive mark price on ${opp.shortExchange}`);
1070
1077
  return false;
1071
1078
  }
1072
1079
  // Liquidity check
@@ -58,8 +58,12 @@ export declare function cappedLeverage(configured: number, legs: {
58
58
  }[], logUnknown: (msg: string) => void): number;
59
59
  /** Match a symbol against a target, accounting for -PERP suffix variations. */
60
60
  export declare function matchSymbol(s: string, target: string): boolean;
61
- /** Get a price estimate for a symbol from the perp adapter. */
62
- export declare function getPriceEstimate(perpAdapter: ExchangeAdapter, perpSymbol: string, fallbackSymbol: string): Promise<number>;
61
+ /**
62
+ * The perp venue's mark price for a symbol. null = the market is not listed or its mark is not a
63
+ * positive finite number (Pacifica rows carry markPrice "-" when /info/prices has none). A failed
64
+ * market read throws (never a 0 that callers would size against).
65
+ */
66
+ export declare function getPriceEstimate(perpAdapter: ExchangeAdapter, perpSymbol: string, fallbackSymbol: string): Promise<number | null>;
63
67
  export interface FundingScore {
64
68
  symbol: string;
65
69
  exchange: string;
@@ -82,6 +86,14 @@ export declare function isPositionGone(e: unknown): boolean;
82
86
  * points, or the per-symbol funding basis is unknown). A failed read THROWS — callers must not
83
87
  * mistake a 429 / timeout for "history too thin".
84
88
  */
89
+ /**
90
+ * Funding-history rows one request (or the adapter's paging) can serve per venue — a
91
+ * `history_periods` above it fails every scoring call, so strategies validate it at config time.
92
+ * Venues without an entry page without a limit. Measured / documented in docs/VENUE_READ_CAPS.md.
93
+ */
94
+ export declare const FUNDING_HISTORY_CAP: Readonly<Record<string, number>>;
95
+ /** Throws INVALID_PARAMS when `periods` is not a positive integer or exceeds a leg venue's cap. */
96
+ export declare function assertFundingHistoryPeriods(periods: unknown, venues: readonly string[], param?: string): void;
85
97
  export declare function scoreFunding(adapter: ExchangeAdapter, symbol: string, exchangeName: string, periods: number, cache?: Map<string, {
86
98
  score: FundingScore;
87
99
  ts: number;
@@ -133,18 +133,20 @@ export function matchSymbol(s, target) {
133
133
  const t = target.toUpperCase();
134
134
  return u === t || u === t + "-PERP" || u.replace(/-PERP$/, "") === t;
135
135
  }
136
- /** Get a price estimate for a symbol from the perp adapter. */
136
+ /**
137
+ * The perp venue's mark price for a symbol. null = the market is not listed or its mark is not a
138
+ * positive finite number (Pacifica rows carry markPrice "-" when /info/prices has none). A failed
139
+ * market read throws (never a 0 that callers would size against).
140
+ */
137
141
  export async function getPriceEstimate(perpAdapter, perpSymbol, fallbackSymbol) {
138
- try {
139
- const markets = await perpAdapter.getMarkets();
140
- const market = markets.find(m => m.symbol.toUpperCase() === perpSymbol.toUpperCase() ||
141
- m.symbol.toUpperCase() === fallbackSymbol.toUpperCase() ||
142
- m.symbol.toUpperCase() === `${fallbackSymbol.toUpperCase()}-PERP`);
143
- return market ? parseFloat(market.markPrice) : 0;
144
- }
145
- catch {
146
- return 0;
147
- }
142
+ const markets = await perpAdapter.getMarkets();
143
+ const market = markets.find(m => m.symbol.toUpperCase() === perpSymbol.toUpperCase() ||
144
+ m.symbol.toUpperCase() === fallbackSymbol.toUpperCase() ||
145
+ m.symbol.toUpperCase() === `${fallbackSymbol.toUpperCase()}-PERP`);
146
+ if (market === undefined)
147
+ return null;
148
+ const px = Number(market.markPrice);
149
+ return Number.isFinite(px) && px > 0 ? px : null;
148
150
  }
149
151
  /**
150
152
  * true when a reduce-only close was rejected because there is no position left to reduce.
@@ -187,6 +189,29 @@ async function marketFundingHours(adapter, exchangeName, symbol) {
187
189
  * points, or the per-symbol funding basis is unknown). A failed read THROWS — callers must not
188
190
  * mistake a 429 / timeout for "history too thin".
189
191
  */
192
+ /**
193
+ * Funding-history rows one request (or the adapter's paging) can serve per venue — a
194
+ * `history_periods` above it fails every scoring call, so strategies validate it at config time.
195
+ * Venues without an entry page without a limit. Measured / documented in docs/VENUE_READ_CAPS.md.
196
+ */
197
+ export const FUNDING_HISTORY_CAP = {
198
+ // hyperliquid: fundingHistory pages forward, but `-l` above 5000 is refused (HL_FUNDING_HISTORY_MAX_LIMIT).
199
+ // pacifica: cursor paging, 4000 per page × 10 pages (PACIFICA_FUNDING_PAGE × PACIFICA_MAX_PAGES).
200
+ hyperliquid: 5000, pacifica: 40_000, lighter: 750, "lighter-rh": 750, arcus: 1000, aster: 1000, grvt: 10_000, extended: 10_000,
201
+ };
202
+ /** Throws INVALID_PARAMS when `periods` is not a positive integer or exceeds a leg venue's cap. */
203
+ export function assertFundingHistoryPeriods(periods, venues, param = "history_periods") {
204
+ if (typeof periods !== "number" || !Number.isInteger(periods) || periods < 1) {
205
+ throw new PerpError("INVALID_PARAMS", `${param} must be a positive integer (got ${String(periods)})`, { remediation: `Set ${param} to e.g. 6` });
206
+ }
207
+ const over = venues.map(v => v.toLowerCase()).filter(v => FUNDING_HISTORY_CAP[v] !== undefined && periods > FUNDING_HISTORY_CAP[v]);
208
+ if (over.length > 0) {
209
+ const caps = over.map(v => `${v} ${FUNDING_HISTORY_CAP[v]}`).join(", ");
210
+ throw new PerpError("INVALID_PARAMS", `${param} ${periods} exceeds the funding-history cap of ${caps}`, {
211
+ remediation: `Set ${param} to at most ${Math.min(...over.map(v => FUNDING_HISTORY_CAP[v]))}`,
212
+ });
213
+ }
214
+ }
190
215
  export async function scoreFunding(adapter, symbol, exchangeName, periods, cache) {
191
216
  const key = `${exchangeName}:${symbol}`;
192
217
  const cached = cache?.get(key);
@@ -197,15 +222,23 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
197
222
  if (fH === null)
198
223
  return null; // per-symbol basis unknown — the hourly rate would be a guess
199
224
  const history = await adapter.getFundingHistory(symbol, periods);
200
- if (history.length < 2)
225
+ // A print without a rate is unknown, not 0, and a closed-market window (QFEX outside
226
+ // market_hours: rate null, marketClosed) carries no funding: both left out of the average.
227
+ const known = history.filter((h) => h.rate !== null && h.marketClosed !== true);
228
+ if (known.length < 2)
201
229
  return null;
202
- const rates = history.map(h => parseFloat(h.rate));
230
+ const rates = known.map(h => parseFloat(h.rate));
203
231
  if (rates.some(r => !Number.isFinite(r))) {
204
232
  throw new PerpError("EXCHANGE_ERROR", `${exchangeName} ${symbol}: funding history has a non-numeric rate`, { exchange: exchangeName });
205
233
  }
206
- const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
234
+ // Every print normalized by its own interval (Lighter /fundings prints are per 1 h while the
235
+ // venue's quoted rate is per 8 h); intervalHours null → the market basis fH.
236
+ const hourly = known.map((h, i) => rates[i] / (typeof h.intervalHours === "number" ? h.intervalHours : fH));
237
+ const avgHourly = hourly.reduce((s, r) => s + r, 0) / hourly.length;
238
+ /** Average rate per the market's own interval (fH), from interval-normalized prints. */
239
+ const avgRate = avgHourly * fH;
207
240
  const consistency = rates.filter(r => r > 0).length / rates.length;
208
- const annualized = annualizeHourlyRate(avgRate / fH);
241
+ const annualized = annualizeHourlyRate(avgHourly);
209
242
  const score = { symbol, exchange: exchangeName, avgRate, consistency, annualized, payments: rates.length };
210
243
  cache?.set(key, { score, ts: Date.now() });
211
244
  return score;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Spot-Perp Arbitrage Strategy
3
3
  *
4
- * Focused strategy: buy spot (HL or Lighter) + short perp on the BEST of 4 exchanges.
4
+ * Focused strategy: buy spot (HL or Lighter) + short perp on the best of the configured perp exchanges (`exchanges`).
5
5
  * No perp-perp logic. History-based entry/exit with short rotation.
6
6
  *
7
7
  * Strategy layer handles ONLY decisions (opportunity scanning, scoring, close/rotation decisions).
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Spot-Perp Arbitrage Strategy
3
3
  *
4
- * Focused strategy: buy spot (HL or Lighter) + short perp on the BEST of 4 exchanges.
4
+ * Focused strategy: buy spot (HL or Lighter) + short perp on the best of the configured perp exchanges (`exchanges`).
5
5
  * No perp-perp logic. History-based entry/exit with short rotation.
6
6
  *
7
7
  * Strategy layer handles ONLY decisions (opportunity scanning, scoring, close/rotation decisions).
@@ -12,7 +12,7 @@ import { registerStrategy } from "../strategy-registry.js";
12
12
  import { getFundingHours } from "../../funding.js";
13
13
  import { annualizeHourlyRate } from "../../funding/normalize.js";
14
14
  import { pruneExecutionLog } from "../../execution-log.js";
15
- import { buildAdapterMap, getPerpSymbol, findRate, cappedLeverage, matchSymbol, fetchRates, scoreFunding, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
15
+ import { buildAdapterMap, getPerpSymbol, findRate, cappedLeverage, matchSymbol, fetchRates, scoreFunding, assertFundingHistoryPeriods, botSpotPerpMatches, assertFundingArbLegs, cancelBotSymbols, collectFundingSince } from "./funding-arb-utils.js";
16
16
  import { SpotPerpExecutor } from "./spot-perp-executor.js";
17
17
  import { findSpotQuoteBalance, getBotSpotAdapter, nonQuoteSpotBalances, spotNetworkOf, spotSharesPerpMargin, spotTokenBase } from "../../exchanges/spot-factory.js";
18
18
  import { getVenue } from "../../exchanges/venue.js";
@@ -80,6 +80,8 @@ export class SpotPerpArbStrategy {
80
80
  catch { /* non-critical */ }
81
81
  this._config = ctx.config;
82
82
  const p = this.params;
83
+ // Config-time check: a history_periods above a leg venue's cap would fail every tick.
84
+ assertFundingHistoryPeriods(p.history_periods, p.exchanges);
83
85
  ctx.state.set("arbRunning", true);
84
86
  ctx.state.set("spaPositions", []);
85
87
  ctx.state.set("arbPositions", 0);
@@ -65,8 +65,8 @@ export class SpotPerpExecutor {
65
65
  }
66
66
  // Price estimate
67
67
  const price = await getPriceEstimate(perpAdapter, perpSymbol, symbol);
68
- if (price <= 0)
69
- return { success: false, error: "price <= 0" };
68
+ if (price === null)
69
+ return { success: false, error: `no positive mark price for ${perpSymbol} on ${perpExchange}` };
70
70
  // Liquidity check
71
71
  const liq = await checkSpotPerpLiquidity(spotAdapter, perpAdapter, symbol, perpSymbol, sizeUsd, 0.5, (msg) => log(` ${msg}`));
72
72
  if (!liq.viable)
@@ -111,7 +111,7 @@ export function buildProgram(opts) {
111
111
  .option("-e, --exchange <exchange>", `Exchange: ${listVenues().map(v => v.id).join(", ")} (default: ${defaultExchange})`, defaultExchange)
112
112
  .option("-k, --private-key <key>", "REJECTED — a raw key on argv leaks via ps / shell history. Import it instead: perp wallet set <exchange> (stdin)")
113
113
  .option("--json", "Output raw JSON (for piping)")
114
- .option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. totalEquity,positions)")
114
+ .option("--fields <fields>", "Comma-separated fields to include in JSON output (dot paths allowed, e.g. totals.equity,exchanges)")
115
115
  .option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
116
116
  .option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
117
117
  .option("--ows <name>", "Use an OWS (Open Wallet Standard) wallet by name")