perp-cli 0.12.15 → 0.14.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 (361) hide show
  1. package/README.md +278 -31
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +286 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +74 -84
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +286 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +939 -245
  103. package/dist/commands/history.js +72 -45
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +316 -337
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +219 -6
  166. package/dist/exchanges/aster.js +1023 -153
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +166 -0
  200. package/dist/exchanges/ondo/adapter.js +835 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +122 -0
  206. package/dist/exchanges/ondo/funds.js +290 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +288 -4
  218. package/dist/exchanges/pacifica.js +1013 -76
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1383 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +34 -0
  270. package/dist/exchanges/venues/ondo.js +140 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +28 -0
  274. package/dist/exchanges/venues/qfex.js +132 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +157 -682
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +14 -0
  287. package/dist/landing.js +36 -0
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +820 -402
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/pacifica/client.d.ts +41 -10
  293. package/dist/pacifica/client.js +167 -63
  294. package/dist/pacifica/constants.d.ts +0 -1
  295. package/dist/pacifica/constants.js +0 -5
  296. package/dist/pacifica/deposit.d.ts +30 -3
  297. package/dist/pacifica/deposit.js +119 -2
  298. package/dist/pacifica/faucet.d.ts +29 -0
  299. package/dist/pacifica/faucet.js +100 -0
  300. package/dist/pacifica/sign-tx.d.ts +15 -0
  301. package/dist/pacifica/sign-tx.js +41 -0
  302. package/dist/pacifica/types/account.d.ts +28 -4
  303. package/dist/pacifica/types/lake.d.ts +20 -5
  304. package/dist/pacifica/types/lake.js +4 -0
  305. package/dist/pacifica/types/market.d.ts +5 -0
  306. package/dist/pacifica/types/order.d.ts +4 -1
  307. package/dist/pacifica/types/position.d.ts +9 -1
  308. package/dist/plan-executor.d.ts +10 -5
  309. package/dist/plan-executor.js +165 -54
  310. package/dist/position-history.js +13 -0
  311. package/dist/rebalance.d.ts +26 -2
  312. package/dist/rebalance.js +86 -7
  313. package/dist/relay-swap.d.ts +232 -0
  314. package/dist/relay-swap.js +630 -0
  315. package/dist/risk.js +5 -1
  316. package/dist/secret-env.d.ts +15 -0
  317. package/dist/secret-env.js +36 -0
  318. package/dist/settings.d.ts +169 -6
  319. package/dist/settings.js +219 -38
  320. package/dist/signer/ows-evm.d.ts +13 -0
  321. package/dist/signer/ows-evm.js +22 -0
  322. package/dist/smart-order.d.ts +9 -0
  323. package/dist/smart-order.js +53 -8
  324. package/dist/strategies/dca.d.ts +6 -2
  325. package/dist/strategies/dca.js +66 -26
  326. package/dist/strategies/funding-arb.d.ts +11 -1
  327. package/dist/strategies/funding-arb.js +202 -89
  328. package/dist/strategies/grid.d.ts +31 -0
  329. package/dist/strategies/grid.js +89 -11
  330. package/dist/strategies/pending-stop.d.ts +8 -0
  331. package/dist/strategies/pending-stop.js +12 -0
  332. package/dist/strategies/split-order.d.ts +45 -17
  333. package/dist/strategies/split-order.js +157 -91
  334. package/dist/strategies/trailing-stop.d.ts +2 -1
  335. package/dist/strategies/trailing-stop.js +52 -6
  336. package/dist/strategies/twap.d.ts +6 -1
  337. package/dist/strategies/twap.js +39 -11
  338. package/dist/trade-validator.d.ts +22 -0
  339. package/dist/trade-validator.js +119 -15
  340. package/dist/utils/numeric.d.ts +46 -0
  341. package/dist/utils/numeric.js +79 -0
  342. package/dist/utils.d.ts +19 -0
  343. package/dist/utils.js +96 -19
  344. package/dist/withdraw-gate.d.ts +49 -0
  345. package/dist/withdraw-gate.js +131 -0
  346. package/package.json +15 -6
  347. package/skills/perp-cli/SKILL.md +338 -134
  348. package/skills/perp-cli/references/agent-operations.md +181 -92
  349. package/skills/perp-cli/references/commands.md +475 -132
  350. package/skills/perp-cli/references/strategies.md +44 -38
  351. package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
  352. package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
  353. package/skills/perp-cli/scripts/preflight.sh +21 -14
  354. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  355. package/skills/perp-cli/scripts/validate-arb.sh +33 -16
  356. package/vendor/lighter-signer/SHA256SUMS +2 -0
  357. package/vendor/lighter-signer/VERSION +8 -0
  358. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  359. package/vendor/lighter-signer/wasm_exec.js +575 -0
  360. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  361. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -0,0 +1,835 @@
1
+ /**
2
+ * Ondo Perps adapter — native TradingV2 + legacy ExchangeAdapter shim
3
+ * (design §10.7, §10.10).
4
+ *
5
+ * Signer: HMAC API key only (OndoAgentMeta → ~/.perp/ondo-agents/<keyId>.json).
6
+ * There is no master-key signing path for trading; read-only adapters serve
7
+ * public endpoints only.
8
+ *
9
+ * Order entry:
10
+ * - market orders: IOC LIMIT at the protection price
11
+ * alignProtectionPrice(slippagePrice(mark, side, bps), quoteIncrement, side),
12
+ * bps = the caller's resolveSlippage() value (design §13.2);
13
+ * - TIF (design §13.3): GTC | IOC | ALO, where ALO = GTC + postOnly; FOK is
14
+ * not offered (uniform CAPABILITY_UNSUPPORTED, never emulated); a limit order
15
+ * without a TIF is GTC;
16
+ * - limit orders without a caller clientOrderId get a generated one that
17
+ * encodes post-only (`pcli-p-<hex>` / `pcli-n-<hex>`, see ondoClientOrderId)
18
+ * because the API never reports postOnly back;
19
+ * - TP/SL attached to the entry via takeProfit/stopLoss;
20
+ * - stopMarket = position-level stop (POST /v1/perps/stop_order), reduce-only,
21
+ * mark-triggered; stopLimit is not offered by the venue;
22
+ * - amend = cancel, then place (design §13.10 #10, #11). `size` is the NEW OPEN
23
+ * size. The replacement keeps side / reduceOnly / TIF; post-only comes from
24
+ * the original's generated clientOrderId, or the caller's explicit TIF — an
25
+ * order whose post-only flag is not decodable (placed elsewhere / caller id)
26
+ * and no TIF given is refused before anything is cancelled. Fills that race
27
+ * the cancel reduce the replacement: replace = size − (filledAtCancel −
28
+ * filledAtRead); ≤ 0 (or below the step) → no replacement (reported). Cancel
29
+ * outcome unknown → ORDER_PENDING (nothing placed); cancel ok + replacement
30
+ * refused → ORDER_PARTIAL { cancelled, placed: null }.
31
+ * - cancel-all cancels resting ("open") orders and untriggered stops (position-level
32
+ * and entry-attached TP/SL) — see listWorkingOrders.
33
+ * - a reduce-only limit order must be IOC (capabilities.reduceOnlyTif); a resting one is
34
+ * refused before sending.
35
+ * Limit prices and TP/SL triggers must be on tick (INVALID_TICK); sizes are
36
+ * floored to the step (SIZE_TOO_SMALL when that reaches zero).
37
+ */
38
+ import { randomUUID } from "node:crypto";
39
+ import { PerpError, orderNotSent, orderOutcomeUnknown } from "../../errors.js";
40
+ import { alignProtectionPrice, floorSize, formatComputedPrice, requireOnTick, slippagePrice, subDecimal } from "../precision.js";
41
+ import { unsupported, validateLegacyTif, validateOrderRequest } from "../trading-v2.js";
42
+ import { networkConfig, requireCapabilities } from "../venue.js";
43
+ import { parseOrderRef, toLegacyMarketInfo } from "../venue-types.js";
44
+ import { ONDO_WITHDRAW_NETWORK } from "../venues/ondo.js";
45
+ import { builderGateOf, builderRejectionOf, NO_BUILDER, requireFeeBps } from "../builder-fee.js";
46
+ import { loadOndoCredentials, ONDO_APPROVE_REMEDIATION } from "./agent.js";
47
+ import { OndoClient } from "./client.js";
48
+ import { fetchOndoTradingPairs, ondoPlainSymbol, ondoPublicData, ondoVenueSymbol, parseOndoTime, strOrNull, } from "./public.js";
49
+ const ORDERS_PAGE_LIMIT = 100;
50
+ const DEPTH_LEVELS = 50;
51
+ const PAIRS_TTL_MS = 5 * 60_000;
52
+ const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
53
+ /** Ondo (Go) encodes an empty list as JSON null; that is "no rows", not an unknown value. */
54
+ function listOf(v) {
55
+ return v === null ? [] : v;
56
+ }
57
+ function isPositive(s) {
58
+ return Number(s) > 0;
59
+ }
60
+ /**
61
+ * Generated clientOrderId carrying the post-only flag (design §13.10 #11): the
62
+ * Ondo API never returns postOnly, so amend recovers it from the id instead of
63
+ * a local file. `pcli-p-` = post-only, `pcli-n-` = not post-only; 39 chars of
64
+ * [A-Za-z0-9_-] (venue rule: 1–64).
65
+ */
66
+ const GENERATED_CLOID_RE = /^pcli-([pn])-[0-9a-f]{32}$/;
67
+ function ondoClientOrderId(postOnly) {
68
+ return `pcli-${postOnly ? "p" : "n"}-${randomUUID().replace(/-/g, "")}`;
69
+ }
70
+ /** Post-only flag of a generated clientOrderId; null = not generated here (attributes unknown). */
71
+ function postOnlyOf(clientOrderId) {
72
+ if (clientOrderId === undefined)
73
+ return null;
74
+ const m = GENERATED_CLOID_RE.exec(clientOrderId);
75
+ return m === null ? null : m[1] === "p";
76
+ }
77
+ /** Ondo wire TIF for a resolved common TIF: ALO = GTC + postOnly. FOK is never sent. */
78
+ function wireTif(tif) {
79
+ if (tif === "ALO")
80
+ return { timeInForce: "GTC", postOnly: true, label: "GTC+postOnly" };
81
+ if (tif === "GTC" || tif === "IOC")
82
+ return { timeInForce: tif, postOnly: false, label: tif };
83
+ throw new PerpError("INVALID_PARAMS", `unreachable: Ondo TIF ${tif} passed validation`);
84
+ }
85
+ /** All-null execution record with the given method (fields set by the caller). */
86
+ function execution(method, partial) {
87
+ return {
88
+ method,
89
+ slippageBps: partial.slippageBps ?? null,
90
+ slippageSource: partial.slippageSource ?? null,
91
+ protectionPrice: partial.protectionPrice ?? null,
92
+ referencePrice: partial.referencePrice ?? null,
93
+ tifRequested: partial.tifRequested ?? null,
94
+ tifSent: partial.tifSent ?? null,
95
+ expiresAt: partial.expiresAt ?? null,
96
+ builderFee: partial.builderFee ?? null,
97
+ notes: partial.notes ?? [],
98
+ };
99
+ }
100
+ /** Execution record for an order read back from the venue (status / amend paths). */
101
+ function readExecution(o) {
102
+ const postOnly = postOnlyOf(o.clientOrderId);
103
+ const method = o.type === "limit" ? "limit" : o.type === "market" ? "market" : "trigger";
104
+ const tifSent = o.timeInForce === undefined ? null : postOnly === true ? "GTC+postOnly" : o.timeInForce;
105
+ return execution(method, {
106
+ tifSent,
107
+ notes: postOnly === null && o.type === "limit" ? ["post-only flag not reported by the venue (order not placed with a perp-cli generated client id)"] : [],
108
+ });
109
+ }
110
+ /** Thrown by an amend's DELETE: a venue answer (HTTP < 500 / rate limit) means the cancel was refused, anything else is unknown. */
111
+ function cancelDefinitelyRefused(e) {
112
+ if (!(e instanceof PerpError))
113
+ return false;
114
+ if (e.structured.code === "RATE_LIMITED")
115
+ return true;
116
+ const http = e.structured.details?.httpStatus;
117
+ return typeof http === "number" && http < 500;
118
+ }
119
+ /** Thrown by a POST: definitely not placed (venue refusal with HTTP < 500 or a pre-send failure). */
120
+ function placementDefinitelyRefused(e) {
121
+ if (!(e instanceof PerpError) || e.structured.code === "ORDER_PENDING")
122
+ return false;
123
+ const http = e.structured.details?.httpStatus;
124
+ return typeof http === "number" ? http < 500 : e.structured.code !== "EXCHANGE_ERROR";
125
+ }
126
+ function mapStatus(o) {
127
+ switch (o.status) {
128
+ case "pending": return "accepted";
129
+ case "untriggered": return "open";
130
+ case "open": return isPositive(o.filledSize) ? "partiallyFilled" : "open";
131
+ case "fullyfilled": return "filled";
132
+ // A cancelled order that filled part of its size is a partial fill (its remainder cancelled).
133
+ case "canceled": return isPositive(o.filledSize) ? "partiallyFilled" : "cancelled";
134
+ }
135
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: unknown order status "${String(o.status)}"`);
136
+ }
137
+ function refPath(ref) {
138
+ if (!ID_RE.test(ref.value)) {
139
+ throw new PerpError("INVALID_PARAMS", `invalid order reference "${ref.value}" (allowed: A-Z a-z 0-9 _ -, max 64)`);
140
+ }
141
+ return ref.kind === "orderId" ? `/v1/perps/orders/${ref.value}` : `/v1/perps/orders/client:${ref.value}`;
142
+ }
143
+ /** CLI kline interval → Ondo resolution ("15m" → "15", "4h" → "240", "1d" → "1D", "1w" → "1W", "1M" → "1M"). */
144
+ function toResolution(interval) {
145
+ const m = /^(\d+)([mhdwM])$/.exec(interval.trim());
146
+ if (!m) {
147
+ throw new PerpError("INVALID_PARAMS", `Unsupported kline interval "${interval}"`, {
148
+ remediation: "Use e.g. 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w, 1M",
149
+ });
150
+ }
151
+ const n = Number(m[1]);
152
+ switch (m[2]) {
153
+ case "m": return String(n);
154
+ case "h": return String(n * 60);
155
+ case "d": return `${n}D`;
156
+ case "w": return `${n}W`;
157
+ default: return `${n}M`;
158
+ }
159
+ }
160
+ export class OndoAdapter {
161
+ name = "ondo";
162
+ aliases = ["ondo-perps"];
163
+ venue;
164
+ network;
165
+ caps;
166
+ client;
167
+ pairsCache = null;
168
+ /**
169
+ * Builder-fee gate (settings builder.ondo; Ondo has no approval step); null = not
170
+ * configured. Set by the venue definition's create — the adapter never reads settings.
171
+ */
172
+ builderFee = null;
173
+ /**
174
+ * GET /v1/perps/candles needs the account's API key (docs.ondoperps.xyz rest-spec.json:
175
+ * `security: [{"BearerAuth": []}, {"ApiKeyAuth": []}]` — no anonymous `{}` entry, unlike
176
+ * /v1/perps/depth). `market kline` therefore uses the credentialed adapter (klinesNeedAuth).
177
+ */
178
+ klinesRequireAuth = true;
179
+ constructor(def, ctx) {
180
+ this.venue = def;
181
+ this.network = ctx.network;
182
+ this.caps = requireCapabilities(def);
183
+ let credentials = null;
184
+ if (!ctx.readOnly) {
185
+ if (ctx.noAgent) {
186
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Ondo Perps signs only with a registered API key; --no-agent leaves no signer", {
187
+ remediation: `Drop --no-agent, or register a key: ${ONDO_APPROVE_REMEDIATION}`,
188
+ });
189
+ }
190
+ const agent = ctx.credentials.agent;
191
+ if (agent === null || !("kind" in agent) || agent.kind !== "ondo") {
192
+ throw new PerpError("NO_SIGNER_AVAILABLE", "No Ondo Perps API key registered", {
193
+ remediation: `Register one: ${ONDO_APPROVE_REMEDIATION}`,
194
+ });
195
+ }
196
+ const meta = agent;
197
+ if (meta.network !== ctx.network) {
198
+ throw new PerpError("AGENT_NOT_REGISTERED", `Ondo Perps API key ${meta.keyId} was approved for ${String(meta.network)}, not ${ctx.network}`, {
199
+ remediation: `Register a ${ctx.network} key: ${ONDO_APPROVE_REMEDIATION}${ctx.network === "testnet" ? " -n testnet" : ""}`,
200
+ });
201
+ }
202
+ credentials = loadOndoCredentials(meta.keyId);
203
+ }
204
+ this.client = new OndoClient({
205
+ restUrl: networkConfig(def, ctx.network).restUrl,
206
+ network: ctx.network,
207
+ rateLimit: def.rateLimit,
208
+ credentials,
209
+ });
210
+ }
211
+ /** Raw REST client (same host + HMAC key) — shared with OndoSpotAdapter. */
212
+ get restClient() {
213
+ return this.client;
214
+ }
215
+ /** Account id from GET /v1/account — the `from.id`/`to.id` of withdrawals and spot↔perp transfers. */
216
+ async accountId() {
217
+ const account = await this.client.get("/v1/account", null);
218
+ return account.accountID;
219
+ }
220
+ /** Withdrawal address book (GET /v1/wallet/address_book) — withdrawals go only to these addresses. */
221
+ async addressBook() {
222
+ return (await import("./funds.js")).ondoAddressBook(this.client);
223
+ }
224
+ // ── Helpers ──
225
+ async pairOf(symbol) {
226
+ const market = ondoVenueSymbol(symbol);
227
+ if (this.pairsCache === null || Date.now() - this.pairsCache.at > PAIRS_TTL_MS) {
228
+ const pairs = await fetchOndoTradingPairs(this.client);
229
+ this.pairsCache = { at: Date.now(), byMarket: new Map(pairs.map(p => [p.market, p])) };
230
+ }
231
+ const pair = this.pairsCache.byMarket.get(market);
232
+ if (pair === undefined) {
233
+ throw new PerpError("SYMBOL_NOT_FOUND", `Ondo Perps has no market ${market}`, {
234
+ remediation: "List markets with: perp market list -e ondo",
235
+ });
236
+ }
237
+ return pair;
238
+ }
239
+ /**
240
+ * An order the venue returned as "pending" (not yet open / filled) is read back by id
241
+ * (GET /v1/perps/orders/{id}, bounded 5 × 500 ms) until the venue reports a final status;
242
+ * still pending → the result stays pending (never assumed open).
243
+ */
244
+ async settlePending(r) {
245
+ if (r.finality === "final" || r.orderId === null)
246
+ return r;
247
+ for (let attempt = 0; attempt < 5; attempt++) {
248
+ await new Promise(res => setTimeout(res, 500));
249
+ let order;
250
+ try {
251
+ order = await this.client.get(refPath({ kind: "orderId", value: r.orderId }), null);
252
+ }
253
+ catch {
254
+ continue; // read retried; still unknown after the loop → pending
255
+ }
256
+ const read = this.toOrderResult(order, order, r.execution);
257
+ if (read.finality === "final")
258
+ return { ...read, venueRaw: { ack: r.venueRaw, order } };
259
+ }
260
+ r.execution.notes.push(`order ${r.orderId} still "pending" at the venue after the read-back — verify: perp account orders -e ondo`);
261
+ return r;
262
+ }
263
+ toOrderResult(o, venueRaw, exec) {
264
+ const status = mapStatus(o);
265
+ const filled = isPositive(o.filledSize);
266
+ return {
267
+ venue: "ondo",
268
+ symbol: ondoPlainSymbol(o.market),
269
+ status,
270
+ finality: status === "accepted" ? "pending" : "final",
271
+ orderId: o.orderId,
272
+ clientOrderId: strOrNull(o.clientOrderId),
273
+ filledSize: o.filledSize,
274
+ avgPrice: filled ? formatComputedPrice(Number(o.filledCost) / Number(o.filledSize)) : null,
275
+ execution: exec,
276
+ venueRaw,
277
+ };
278
+ }
279
+ toExchangeOrder(o) {
280
+ return {
281
+ orderId: o.orderId,
282
+ symbol: ondoPlainSymbol(o.market),
283
+ side: o.side,
284
+ price: o.price,
285
+ size: o.size,
286
+ filled: o.filledSize,
287
+ status: o.status,
288
+ type: o.type,
289
+ triggerPrice: o.triggerPrice !== undefined && Number(o.triggerPrice) > 0 ? o.triggerPrice : null,
290
+ };
291
+ }
292
+ async listOrders(query) {
293
+ const out = [];
294
+ let cursor = null;
295
+ do {
296
+ const page = await this.client.getPage("/v1/perps/orders", { ...query, limit: ORDERS_PAGE_LIMIT, cursor });
297
+ out.push(...listOf(page.result));
298
+ cursor = page.nextCursor;
299
+ } while (cursor !== null);
300
+ return out;
301
+ }
302
+ /**
303
+ * Resting orders plus untriggered stop orders. The documented `status` filter enum is open /
304
+ * canceled / fullyfilled; `untriggered` is a documented order status and the filter accepts it
305
+ * (live mainnet 2026-10-05) — every returned row's own status is still checked.
306
+ */
307
+ async listWorkingOrders(market) {
308
+ const open = await this.listOrders({ status: "open", market });
309
+ const untriggered = (await this.listOrders({ status: "untriggered", market })).filter(o => o.status === "untriggered");
310
+ return [...open, ...untriggered];
311
+ }
312
+ async markPriceOf(market) {
313
+ const marks = await this.client.publicGet("/v1/perps/mark_prices", null);
314
+ const m = marks[market];
315
+ if (m === undefined)
316
+ throw new PerpError("PRICE_STALE", `Ondo Perps: no mark price for ${market}`);
317
+ return m.markPrice;
318
+ }
319
+ /**
320
+ * `builderCode: { code, feeRateBpsFractional }` for the next order, recorded on `exec`
321
+ * (builderFee + the reason a configured fee was not attached). Every order body carries it
322
+ * explicitly — position stops do not inherit it (Ondo integration guide).
323
+ */
324
+ async attachBuilderCode(body, exec) {
325
+ const gate = builderGateOf(this);
326
+ const d = gate === null ? NO_BUILDER : await gate.decide({ hip3: false });
327
+ exec.builderFee = d.applied;
328
+ if (d.note !== null)
329
+ exec.notes.push(d.note);
330
+ if (d.applied !== null)
331
+ body.builderCode = { code: d.applied.identity, feeRateBpsFractional: requireFeeBps(d.applied) };
332
+ }
333
+ /** stopMarket → position-level stop (POST /v1/perps/stop_order). */
334
+ async placePositionStop(req, triggerPrice) {
335
+ if (!req.reduceOnly) {
336
+ throw unsupported(this.venue.displayName, "openingStop", "stop orders are position-level and always reduce-only", "Pass --reduce-only for stop orders on ondo");
337
+ }
338
+ if (req.clientOrderId !== null) {
339
+ throw unsupported(this.venue.displayName, "stopClientOrderId", "position stops do not take a client order id", "Omit the client order id for stop orders");
340
+ }
341
+ if (req.tpsl !== null)
342
+ throw new PerpError("INVALID_PARAMS", "tpsl cannot be attached to a stop order");
343
+ if (req.triggerSource !== null && req.triggerSource !== "mark") {
344
+ throw unsupported(this.venue.displayName, "triggerSource", `stops trigger on the mark price only (got ${req.triggerSource})`, "Use --trigger-type mark or omit it");
345
+ }
346
+ const pair = await this.pairOf(req.symbol);
347
+ const trigger = requireOnTick(triggerPrice, pair.quoteIncrement);
348
+ const quantity = floorSize(req.size, pair.baseIncrement);
349
+ // A sell stop closes a long; a buy stop closes a short.
350
+ const positionDirection = req.side === "sell" ? "long" : "short";
351
+ const mark = Number(await this.markPriceOf(pair.market));
352
+ const t = Number(trigger);
353
+ if (t === mark) {
354
+ throw new PerpError("INVALID_PARAMS", `Trigger ${trigger} equals the current mark price; cannot tell stop-loss from take-profit`);
355
+ }
356
+ const lossSide = positionDirection === "long" ? t < mark : t > mark;
357
+ const type = lossSide ? "stopLoss" : "takeProfit";
358
+ const exec = execution("trigger", {
359
+ referencePrice: trigger,
360
+ notes: [`position-level ${type} on the mark price; executes as a venue market order (no protection price)`],
361
+ });
362
+ const stopBody = { market: pair.market, positionDirection, type, triggerPrice: trigger, quantity };
363
+ await this.attachBuilderCode(stopBody, exec);
364
+ // Order type per kind, live mainnet 2026-10-05: stopLoss → "stopMarket", takeProfit → "takeProfitMarket".
365
+ const orderType = type === "stopLoss" ? "stopMarket" : "takeProfitMarket";
366
+ const sameStop = (o) => o.status === "untriggered" && o.type === orderType && o.stopOrderType === type
367
+ && o.triggerPrice !== undefined && Number(o.triggerPrice) === t && subDecimal(o.size, quantity) === "0";
368
+ // The stop endpoint returns no order id ({market, positionDirection, stopLoss?, takeProfit?}),
369
+ // so the order ids already listed BEFORE the POST are excluded afterwards — an older identical
370
+ // stop is never reported as the new one. Pre-send: a failed read sends nothing.
371
+ let before;
372
+ try {
373
+ const prior = await this.client.getPage("/v1/perps/orders", { market: pair.market, limit: ORDERS_PAGE_LIMIT });
374
+ before = new Set(listOf(prior.result).map(o => o.orderId));
375
+ }
376
+ catch (e) {
377
+ throw orderNotSent("ondo", `${type} ${req.side} ${quantity} ${pair.market} @ ${trigger}`, e);
378
+ }
379
+ const stopRaw = await this.client.post("/v1/perps/stop_order", stopBody);
380
+ // Locate the resulting untriggered order. A stop set with `quantity` is a PARTIAL stop
381
+ // (set-stop-order: "Sets a partial stop order for this quantity of the position instead of the
382
+ // full position"), so it carries `size`, not closePosition (live mainnet 2026-10-05:
383
+ // {type:"stopMarket", status:"untriggered", size:"0.005"}). The POST succeeded: a failed read
384
+ // or no / several new candidates leave the stop's state unknown — ORDER_PENDING, never resubmit.
385
+ const pendingRemediation = "Do NOT resubmit — the stop may be set. Check with: perp account orders -e ondo";
386
+ let recent;
387
+ try {
388
+ recent = await this.client.getPage("/v1/perps/orders", { market: pair.market, limit: ORDERS_PAGE_LIMIT });
389
+ }
390
+ catch (e) {
391
+ throw orderOutcomeUnknown("ondo", `${type} at ${trigger}: accepted, but the order list could not be read`, e, pendingRemediation);
392
+ }
393
+ const fresh = listOf(recent.result).filter(o => !before.has(o.orderId) && sameStop(o));
394
+ if (fresh.length !== 1) {
395
+ throw new PerpError("ORDER_PENDING", `Ondo Perps accepted the ${type} at ${trigger} but ${fresh.length === 0 ? "the new order is not visible in the order list" : `${fresh.length} new matching orders are listed (${fresh.map(o => o.orderId).join(", ")})`}`, {
396
+ exchange: "ondo", stopOrders: stopRaw, remediation: pendingRemediation,
397
+ });
398
+ }
399
+ const order = fresh[0];
400
+ return this.toOrderResult(order, subDecimal(req.size, quantity) === "0"
401
+ ? { stopOrder: stopRaw, order }
402
+ : { stopOrder: stopRaw, order, requestedSize: req.size, placedSize: quantity }, exec);
403
+ }
404
+ // ── TradingV2 ──
405
+ async getVenueMarkets() {
406
+ return ondoPublicData.fetchMarkets(this.network);
407
+ }
408
+ async placeOrder(input) {
409
+ try {
410
+ return await this.placeOrderOnVenue(input);
411
+ }
412
+ catch (e) {
413
+ // A builder-related rejection drops the cached approval and says how to proceed; never retried.
414
+ throw builderRejectionOf(this, e);
415
+ }
416
+ }
417
+ async placeOrderOnVenue(input) {
418
+ // §13.3: GTC is the uniform default for a limit order without a TIF.
419
+ const tifRequested = input.tif;
420
+ const req = input.type === "limit" && input.tif === null ? { ...input, tif: "GTC" } : input;
421
+ if (req.tif === "FOK") {
422
+ throw unsupported(this.venue.displayName, "fok", "fill-or-kill is not offered and is never emulated (a partial IOC fill cannot be undone)", "Use --tif IOC (partial fills possible) or GTC / ALO");
423
+ }
424
+ validateOrderRequest(this.venue, this.caps, req);
425
+ if (req.type === "stopLimit") {
426
+ throw unsupported(this.venue.displayName, "stopLimit", "the venue offers position stops (market) only", "Use a stop-market (position stop) instead");
427
+ }
428
+ if (req.type === "stopMarket") {
429
+ if (req.triggerPrice === null)
430
+ throw new PerpError("INVALID_PARAMS", "stopMarket order requires a trigger price");
431
+ return this.placePositionStop(req, req.triggerPrice);
432
+ }
433
+ const pair = await this.pairOf(req.symbol);
434
+ const size = floorSize(req.size, pair.baseIncrement);
435
+ const body = {
436
+ market: pair.market,
437
+ side: req.side,
438
+ size,
439
+ reduceOnly: req.reduceOnly,
440
+ };
441
+ let exec;
442
+ if (req.type === "limit") {
443
+ if (req.price === null || req.tif === null)
444
+ throw new PerpError("INVALID_PARAMS", "limit order requires price and tif");
445
+ const w = wireTif(req.tif);
446
+ body.type = "limit";
447
+ body.price = requireOnTick(req.price, pair.quoteIncrement);
448
+ body.timeInForce = w.timeInForce;
449
+ if (w.postOnly)
450
+ body.postOnly = true;
451
+ // The API never returns postOnly: encode it in a generated client id so amend can recover it.
452
+ if (req.clientOrderId === null)
453
+ body.clientOrderId = ondoClientOrderId(w.postOnly);
454
+ exec = execution("limit", {
455
+ tifRequested, tifSent: w.label,
456
+ notes: tifRequested === null ? ["no time-in-force given; GTC (uniform default)"] : [],
457
+ });
458
+ }
459
+ else {
460
+ // validateOrderRequest guarantees the resolved bound for market orders.
461
+ const slippage = req.slippage;
462
+ // Slippage-bounded market: IOC limit at the protection price (see header).
463
+ const mark = await this.markPriceOf(pair.market);
464
+ const protectionPrice = alignProtectionPrice(slippagePrice(mark, req.side, slippage.bps), pair.quoteIncrement, req.side);
465
+ body.type = "limit";
466
+ body.price = protectionPrice;
467
+ body.timeInForce = "IOC";
468
+ const appliedBps = Math.abs(Number(protectionPrice) - Number(mark)) / Number(mark) * 10_000;
469
+ exec = execution("protectedIoc", {
470
+ slippageBps: Number(appliedBps.toFixed(4)), slippageSource: slippage.source,
471
+ protectionPrice, referencePrice: mark, tifSent: "IOC",
472
+ notes: [`resolved bound ${slippage.bps} bps (${slippage.source}); protection price aligned to tick ${pair.quoteIncrement} on the mark price`],
473
+ });
474
+ }
475
+ if (req.clientOrderId !== null) {
476
+ if (!ID_RE.test(req.clientOrderId)) {
477
+ throw new PerpError("INVALID_PARAMS", `clientOrderId "${req.clientOrderId}" must be 1-64 chars of A-Z a-z 0-9 _ -`);
478
+ }
479
+ // A caller id in the generated format must not misstate post-only (amend trusts it).
480
+ const flag = postOnlyOf(req.clientOrderId);
481
+ if (flag !== null && flag !== (body.postOnly === true)) {
482
+ throw new PerpError("INVALID_PARAMS", `clientOrderId "${req.clientOrderId}" uses the reserved pcli-${flag ? "p" : "n"}- prefix but the order is ${body.postOnly === true ? "" : "not "}post-only`);
483
+ }
484
+ body.clientOrderId = req.clientOrderId;
485
+ }
486
+ if (req.tpsl !== null) {
487
+ if (req.tpsl.tp !== null)
488
+ body.takeProfit = { triggerPrice: requireOnTick(req.tpsl.tp, pair.quoteIncrement) };
489
+ if (req.tpsl.sl !== null)
490
+ body.stopLoss = { triggerPrice: requireOnTick(req.tpsl.sl, pair.quoteIncrement) };
491
+ exec.notes.push("TP/SL attached to the entry (venue takeProfit/stopLoss)");
492
+ }
493
+ await this.attachBuilderCode(body, exec);
494
+ const order = await this.client.post("/v1/perps/orders", body);
495
+ // Size flooring is surfaced, never silent (L2).
496
+ return this.settlePending(this.toOrderResult(order, subDecimal(req.size, size) === "0" ? order : { requestedSize: req.size, placedSize: size, response: order }, exec));
497
+ }
498
+ async amendOrder(req) {
499
+ const pair = await this.pairOf(req.symbol);
500
+ const original = await this.client.get(refPath(req.ref), null);
501
+ if (original.market !== pair.market) {
502
+ throw new PerpError("INVALID_PARAMS", `Order ${req.ref.value} is on ${original.market}, not ${pair.market}`);
503
+ }
504
+ if (original.type !== "limit" || original.status !== "open") {
505
+ throw unsupported(this.venue.displayName, "amend", `only open limit orders can be amended (order is ${original.type}/${original.status})`, `Cancel it (perp trade cancel ${req.symbol} ${original.orderId} -e ondo) and place a new order`);
506
+ }
507
+ if (original.timeInForce === undefined) {
508
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps returned no timeInForce for limit order ${original.orderId}`);
509
+ }
510
+ // Replacement TIF: explicit caller TIF, else the original's (post-only from the generated client id).
511
+ const postOnly = postOnlyOf(original.clientOrderId);
512
+ let tif;
513
+ if (req.tif !== null) {
514
+ if (req.tif === "FOK") {
515
+ throw unsupported(this.venue.displayName, "fok", "fill-or-kill is not offered and is never emulated", "Use --tif GTC, IOC or ALO");
516
+ }
517
+ tif = validateLegacyTif(this.venue, this.caps, req.tif);
518
+ if (postOnly !== null && (tif === "ALO") !== postOnly && req.forceTif !== true) {
519
+ throw new PerpError("INVALID_PARAMS", `--tif ${tif} would make order ${original.orderId} ${postOnly ? "no longer post-only" : "post-only"} (its client id encodes ${postOnly ? "" : "not "}post-only)`, {
520
+ remediation: `Omit --tif to keep the order's own TIF, or pass --force-tif to change it`,
521
+ });
522
+ }
523
+ }
524
+ else if (postOnly === null) {
525
+ throw unsupported(this.venue.displayName, "amend", "original order attributes unknown", `pass --tif: perp trade edit ${req.symbol} ${original.orderId} <price> <size> --tif <GTC|IOC|ALO> -e ondo (ALO keeps post-only)`);
526
+ }
527
+ else {
528
+ tif = postOnly ? "ALO" : original.timeInForce;
529
+ }
530
+ const w = wireTif(tif);
531
+ // The replacement keeps reduceOnly: a reduce-only limit must be one of caps.reduceOnlyTif (IOC
532
+ // — see header), so a reduce-only replacement at another TIF would be refused only AFTER the
533
+ // original is cancelled. Refuse up front; nothing is cancelled.
534
+ if (original.reduceOnly === true && this.caps.reduceOnlyTif !== null && !this.caps.reduceOnlyTif.includes(tif)) {
535
+ throw unsupported(this.venue.displayName, "amend", `order ${original.orderId} is reduce-only and a reduce-only limit can only be ${this.caps.reduceOnlyTif.join(" / ")} (replacement TIF ${tif}); nothing was cancelled`, `Cancel it (perp trade cancel ${req.symbol} ${original.orderId} -e ondo) and place a new order`);
536
+ }
537
+ // Validate the replacement BEFORE cancelling so a bad price/size never loses the original.
538
+ const price = requireOnTick(req.price, pair.quoteIncrement);
539
+ floorSize(req.size, pair.baseIncrement);
540
+ const filledAtRead = original.filledSize;
541
+ let cancelled;
542
+ try {
543
+ cancelled = await this.client.delete(`/v1/perps/orders/${original.orderId}`, null);
544
+ }
545
+ catch (e) {
546
+ // A venue answer means the cancel was refused (original untouched); anything else is unknown.
547
+ if (cancelDefinitelyRefused(e))
548
+ throw e;
549
+ throw orderOutcomeUnknown("ondo", `amend: cancel of ${original.orderId}`, e, `No replacement was placed. Check the original: perp trade status ${original.orderId} --symbol ${req.symbol} -e ondo`);
550
+ }
551
+ const notes = [
552
+ req.tif !== null ? `TIF ${tif} (from --tif)` : `TIF ${tif} kept (post-only flag from the order's generated client id)`,
553
+ `cancel→replace: filledAtRead ${filledAtRead}, filledAtCancel ${cancelled.filledSize}`,
554
+ ];
555
+ const amendExec = (extra, sent) => execution("cancelReplace", { tifRequested: req.tif, tifSent: sent, notes: [...notes, ...extra] });
556
+ if (cancelled.status === "fullyfilled") {
557
+ return this.toOrderResult(cancelled, { cancelled, placed: null }, amendExec([`order ${original.orderId} filled completely before the cancel — no replacement placed`], null));
558
+ }
559
+ if (cancelled.status !== "canceled") {
560
+ throw new PerpError("ORDER_PENDING", `Ondo Perps amend: cancel of ${original.orderId} returned status "${cancelled.status}", not "canceled"; no replacement was placed`, {
561
+ exchange: "ondo",
562
+ cancelled,
563
+ remediation: `Do NOT resubmit. Check the order: perp trade status ${original.orderId} --symbol ${req.symbol} -e ondo`,
564
+ });
565
+ }
566
+ // req.size = new OPEN size; fills that raced the cancel reduce the replacement (§13.10 #10).
567
+ const racedFill = subDecimal(cancelled.filledSize, filledAtRead);
568
+ const replace = subDecimal(req.size, racedFill.startsWith("-") ? "0" : racedFill);
569
+ let size = null;
570
+ if (!replace.startsWith("-") && isPositive(replace)) {
571
+ try {
572
+ size = floorSize(replace, pair.baseIncrement);
573
+ }
574
+ catch (e) {
575
+ if (!(e instanceof PerpError) || e.structured.code !== "SIZE_TOO_SMALL")
576
+ throw e;
577
+ }
578
+ }
579
+ if (size === null) {
580
+ return this.toOrderResult(cancelled, { cancelled, placed: null }, amendExec([`${racedFill} filled between read and cancel; replacement ${replace} is not above the step ${pair.baseIncrement} — no replacement placed`], null));
581
+ }
582
+ const body = {
583
+ market: pair.market,
584
+ side: original.side,
585
+ type: "limit",
586
+ price,
587
+ size,
588
+ reduceOnly: original.reduceOnly === true,
589
+ timeInForce: w.timeInForce,
590
+ clientOrderId: ondoClientOrderId(w.postOnly),
591
+ };
592
+ if (w.postOnly)
593
+ body.postOnly = true;
594
+ const replaceExec = amendExec([], w.label);
595
+ await this.attachBuilderCode(body, replaceExec);
596
+ let placed;
597
+ try {
598
+ placed = await this.client.post("/v1/perps/orders", body);
599
+ }
600
+ catch (e) {
601
+ const msg = e instanceof Error ? e.message : String(e);
602
+ if (placementDefinitelyRefused(e)) {
603
+ throw new PerpError("ORDER_PARTIAL", `Ondo Perps amend: original order ${original.orderId} was CANCELLED but the replacement was refused: ${msg}`, {
604
+ exchange: "ondo",
605
+ cancelled,
606
+ placed: null,
607
+ remediation: "The original order is gone; place a new order manually",
608
+ });
609
+ }
610
+ throw new PerpError("ORDER_PENDING", `Ondo Perps amend: original order ${original.orderId} was CANCELLED; the replacement outcome is unknown: ${msg}`, {
611
+ exchange: "ondo",
612
+ cancelled,
613
+ replacementClientOrderId: body.clientOrderId,
614
+ remediation: `Do NOT resubmit. Check: perp trade status c:${String(body.clientOrderId)} --symbol ${req.symbol} -e ondo`,
615
+ });
616
+ }
617
+ const extra = [
618
+ `filledBeforeCancel ${cancelled.filledSize}, replacedSize ${size}`,
619
+ ...(subDecimal(req.size, size) === "0" ? [] : [`replacement size ${size} (requested open size ${req.size})`]),
620
+ ];
621
+ replaceExec.notes.push(...extra);
622
+ return this.settlePending(this.toOrderResult(placed, { cancelled, placed, requestedSize: req.size, placedSize: size }, replaceExec));
623
+ }
624
+ async cancelByRef(_symbol, ref) {
625
+ const order = await this.client.delete(refPath(ref), null);
626
+ return { venue: "ondo", cancelled: [ref], failed: [], venueRaw: order };
627
+ }
628
+ /** Lists resting orders and untriggered stops (listWorkingOrders), then cancels them via the batch endpoint so every result is exact. */
629
+ async cancelAllV2(symbol) {
630
+ const open = await this.listWorkingOrders(symbol === null ? null : ondoVenueSymbol(symbol));
631
+ const result = { venue: "ondo", cancelled: [], failed: [], venueRaw: [] };
632
+ if (this.caps.batchPlaceMax === null)
633
+ throw new Error("ondo capabilities.batchPlaceMax must be set");
634
+ const chunk = this.caps.batchPlaceMax;
635
+ const raws = [];
636
+ for (let i = 0; i < open.length; i += chunk) {
637
+ const ids = open.slice(i, i + chunk).map(o => o.orderId);
638
+ const r = await this.client.delete("/v1/perps/orders/batch", { orderIDs: ids.join(",") });
639
+ raws.push(r);
640
+ for (const o of listOf(r.successfulCancels))
641
+ result.cancelled.push({ kind: "orderId", value: o.orderId });
642
+ for (const f of listOf(r.failedCancels)) {
643
+ result.failed.push({ ref: { kind: "orderId", value: f.orderId }, error: JSON.stringify(f) });
644
+ }
645
+ }
646
+ result.venueRaw = { batches: raws };
647
+ return result;
648
+ }
649
+ async applyLeverage(symbol, leverage, marginMode) {
650
+ if (marginMode === "isolated") {
651
+ throw unsupported(this.venue.displayName, "isolatedMargin", "accounts are cross-margin only", "Omit --isolated (ondo accounts are cross-margin)");
652
+ }
653
+ if (!Number.isInteger(leverage) || leverage < 1) {
654
+ throw new PerpError("INVALID_PARAMS", `Ondo Perps leverage must be a positive integer (got ${leverage})`);
655
+ }
656
+ const market = ondoVenueSymbol(symbol);
657
+ await this.client.postNoResult("/v1/perps/leverage", { market, leverage: String(leverage) });
658
+ return { venue: "ondo", symbol: ondoPlainSymbol(market), leverage, marginMode: "cross", venueRaw: null };
659
+ }
660
+ async getOrderStatus(_symbol, ref) {
661
+ const order = await this.client.get(refPath(ref), null);
662
+ return this.toOrderResult(order, order, readExecution(order));
663
+ }
664
+ // ── Legacy ExchangeAdapter: market data ──
665
+ async getMarkets() {
666
+ return (await this.getVenueMarkets()).map(toLegacyMarketInfo);
667
+ }
668
+ async getOrderbook(symbol) {
669
+ const book = await this.client.publicGet("/v1/perps/depth", { market: ondoVenueSymbol(symbol), depth: DEPTH_LEVELS });
670
+ const lvl = (l) => [l[0], l[1]];
671
+ return { bids: book.bids.map(lvl), asks: book.asks.map(lvl) };
672
+ }
673
+ async getRecentTrades(symbol, limit) {
674
+ const trades = await this.client.publicGet("/v1/perps/trades", {
675
+ market: ondoVenueSymbol(symbol), limit: limit === undefined ? null : limit,
676
+ });
677
+ return listOf(trades).map(t => ({
678
+ time: parseOndoTime(t.time, "trade"),
679
+ symbol: ondoPlainSymbol(t.market),
680
+ side: t.aggressor_side,
681
+ price: t.price,
682
+ size: t.size,
683
+ fee: "0", // public tape: no fee applies to the viewer
684
+ }));
685
+ }
686
+ async getFundingHistory(symbol, limit) {
687
+ const rows = await this.client.publicGet("/v1/perps/funding_rate_history", {
688
+ market: ondoVenueSymbol(symbol), limit: limit === undefined ? null : limit,
689
+ });
690
+ return listOf(rows).map(r => ({ time: parseOndoTime(r.time, "funding"), rate: r.fundingRate, price: null }));
691
+ }
692
+ /** GET /v1/perps/candles — authenticated (see klinesRequireAuth). */
693
+ async getKlines(symbol, interval, startTime, endTime) {
694
+ const candles = await this.client.get("/v1/perps/candles", {
695
+ market: ondoVenueSymbol(symbol),
696
+ resolution: toResolution(interval),
697
+ from: Math.floor(startTime / 1000),
698
+ to: Math.floor(endTime / 1000),
699
+ });
700
+ return listOf(candles).map(c => ({
701
+ time: parseOndoTime(c.startTime, "candle"),
702
+ open: c.open,
703
+ high: c.high,
704
+ low: c.low,
705
+ close: c.close,
706
+ volume: c.volume,
707
+ trades: null, // Ondo publishes no per-candle trade count
708
+ }));
709
+ }
710
+ // ── Legacy ExchangeAdapter: account ──
711
+ async getBalance() {
712
+ const b = await this.client.get("/v1/perps/balance", null);
713
+ return { equity: b.marginBalance, available: b.availableMargin, marginUsed: b.usedMargin, unrealizedPnl: b.unrealizedPnl };
714
+ }
715
+ /**
716
+ * netQuantity is documented as the unsigned position size ("Size of the
717
+ * position in base currency", example "1.5489" on a short) with the side in
718
+ * `direction`. A signed value is still accepted: the magnitude is used and
719
+ * the sign must agree with `direction` (never dropped, never guessed).
720
+ */
721
+ async getPositions() {
722
+ const rows = await this.client.get("/v1/perps/positions", null);
723
+ const out = [];
724
+ for (const p of listOf(rows)) {
725
+ const negative = p.netQuantity.startsWith("-");
726
+ const size = negative ? p.netQuantity.slice(1) : p.netQuantity;
727
+ if (p.direction === "neutral" || !isPositive(size))
728
+ continue;
729
+ if (negative && p.direction !== "short") {
730
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps position ${p.market}: netQuantity ${p.netQuantity} contradicts direction ${p.direction}`);
731
+ }
732
+ out.push({
733
+ symbol: ondoPlainSymbol(p.market),
734
+ side: p.direction === "long" ? "long" : "short",
735
+ size,
736
+ entryPrice: p.averageEntryPrice,
737
+ markPrice: p.markPrice,
738
+ liquidationPrice: strOrNull(p.liquidationPrice),
739
+ unrealizedPnl: p.unrealizedPnl,
740
+ leverage: Number(p.leverage),
741
+ });
742
+ }
743
+ return out;
744
+ }
745
+ async getOpenOrders() {
746
+ return (await this.listWorkingOrders(null)).map(o => this.toExchangeOrder(o));
747
+ }
748
+ async getOrderHistory(limit) {
749
+ const rows = await this.client.get("/v1/perps/orders", { limit: limit === undefined ? null : limit });
750
+ return listOf(rows).map(o => this.toExchangeOrder(o));
751
+ }
752
+ async getTradeHistory(limit) {
753
+ const fills = await this.client.get("/v1/perps/fills", { limit: limit === undefined ? null : limit });
754
+ return listOf(fills).map(f => ({
755
+ time: parseOndoTime(f.time, "fill"),
756
+ symbol: ondoPlainSymbol(f.market),
757
+ side: f.side,
758
+ price: f.price,
759
+ size: f.size,
760
+ fee: f.fee,
761
+ }));
762
+ }
763
+ /** `amount` is positive when the account received funding. */
764
+ async getFundingPayments(limit) {
765
+ const rows = await this.client.get("/v1/perps/funding_fees", { limit: limit === undefined ? null : limit });
766
+ return listOf(rows).map(r => ({ time: parseOndoTime(r.time, "funding fee"), symbol: ondoPlainSymbol(r.market), payment: r.amount }));
767
+ }
768
+ // ── Legacy ExchangeAdapter: trading (shim → TradingV2, design §10.7) ──
769
+ async marketOrder(symbol, side, size, opts) {
770
+ return this.placeOrder({
771
+ symbol, side, type: "market", size, price: null, triggerPrice: null, triggerSource: null, tif: null,
772
+ reduceOnly: opts.reduceOnly, clientOrderId: null, slippage: opts.slippage, tpsl: null,
773
+ });
774
+ }
775
+ async limitOrder(symbol, side, price, size, opts) {
776
+ // §13.3: no TIF → GTC (uniform default); FOK → uniform CAPABILITY_UNSUPPORTED.
777
+ const tifInput = opts?.tif;
778
+ const tif = tifInput === undefined || tifInput === "" ? null : validateLegacyTif(this.venue, this.caps, tifInput);
779
+ return this.placeOrder({
780
+ symbol, side, type: "limit", size, price, triggerPrice: null, triggerSource: null, tif,
781
+ reduceOnly: opts?.reduceOnly === true, clientOrderId: null, slippage: null, tpsl: null,
782
+ });
783
+ }
784
+ async editOrder(symbol, orderId, price, size) {
785
+ // tif null → keep the original's TIF (post-only decoded from the generated client id; refused when unknown).
786
+ return this.amendOrder({ symbol, ref: parseOrderRef(orderId), price, size, tif: null });
787
+ }
788
+ async cancelOrder(symbol, orderId) {
789
+ return this.cancelByRef(symbol, parseOrderRef(orderId));
790
+ }
791
+ async cancelAllOrders(symbol) {
792
+ return this.cancelAllV2(symbol === undefined ? null : symbol);
793
+ }
794
+ async setLeverage(symbol, leverage, marginMode) {
795
+ return this.applyLeverage(symbol, leverage, marginMode === undefined ? null : marginMode);
796
+ }
797
+ async stopOrder(symbol, side, size, triggerPrice, opts) {
798
+ if (opts?.limitPrice !== undefined) {
799
+ throw unsupported(this.venue.displayName, "stopLimit", "the venue offers position stops (market) only", "Omit --limit-price; ondo stops execute as market orders");
800
+ }
801
+ return this.placeOrder({
802
+ symbol, side, type: "stopMarket", size, price: null, triggerPrice,
803
+ triggerSource: opts?.triggerType === undefined ? null : opts.triggerType,
804
+ tif: null, reduceOnly: opts?.reduceOnly === true, clientOrderId: null, slippage: null, tpsl: null,
805
+ });
806
+ }
807
+ // ── Withdraw (capabilities.WithdrawCapable) ──
808
+ /**
809
+ * Withdraw USDC from the perps (margin) wallet to an Ethereum address.
810
+ * Requires an API key with the `transfer` scope. The destination may need to
811
+ * be on the account's address book (venue error bad_withdrawal_address).
812
+ */
813
+ async withdraw(amount, destination, opts) {
814
+ if (opts?.assetId !== undefined || opts?.routeType !== undefined) {
815
+ throw new PerpError("INVALID_PARAMS", "Ondo Perps withdraw takes no assetId/routeType (USDC on Ethereum only)");
816
+ }
817
+ if (!/^0x[0-9a-fA-F]{40}$/.test(destination)) {
818
+ throw new PerpError("INVALID_PARAMS", `Ondo Perps withdraw destination must be an Ethereum address (got "${destination}")`);
819
+ }
820
+ if (!/^\d+(\.\d+)?$/.test(amount) || !isPositive(amount)) {
821
+ throw new PerpError("INVALID_PARAMS", `Withdraw amount must be a positive decimal (got "${amount}")`);
822
+ }
823
+ // AccountInfo.withdrawalFeeUSD (required in the OpenAPI spec) is reported with the result.
824
+ const account = await this.client.get("/v1/account", null);
825
+ const res = await this.client.post("/v1/withdraw", {
826
+ customer_withdrawal_id: randomUUID(),
827
+ symbol: "USDC",
828
+ network: ONDO_WITHDRAW_NETWORK,
829
+ amount,
830
+ address: destination,
831
+ from: { id: account.accountID, wallet: "margin" },
832
+ });
833
+ return { ...res, withdrawalFeeUSD: account.withdrawalFeeUSD };
834
+ }
835
+ }