perp-cli 0.12.17 → 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 +247 -27
  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 +70 -92
  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 +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  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 +142 -55
  348. package/skills/perp-cli/references/agent-operations.md +110 -56
  349. package/skills/perp-cli/references/commands.md +266 -71
  350. package/skills/perp-cli/references/strategies.md +34 -31
  351. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  352. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  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 +31 -15
  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,7 @@
1
+ import type { ExchangeAdapter } from "../exchanges/index.js";
2
+ /**
3
+ * Filled base size the venue reported for a placed order: the adapter's venue-final IOC / market
4
+ * outcome (iocOutcomeOf), else a TradingV2 OrderResult's `filledSize`. null = not reported (a
5
+ * resting order, or an unrecognized result) — the caller does not count it as a fill.
6
+ */
7
+ export declare function filledOf(adapter: ExchangeAdapter, raw: unknown): number | null;
@@ -0,0 +1,18 @@
1
+ import { isIocOutcomeCapable } from "../exchanges/ioc-outcome.js";
2
+ /**
3
+ * Filled base size the venue reported for a placed order: the adapter's venue-final IOC / market
4
+ * outcome (iocOutcomeOf), else a TradingV2 OrderResult's `filledSize`. null = not reported (a
5
+ * resting order, or an unrecognized result) — the caller does not count it as a fill.
6
+ */
7
+ export function filledOf(adapter, raw) {
8
+ const outcome = isIocOutcomeCapable(adapter) ? adapter.iocOutcomeOf(raw) : null;
9
+ if (outcome !== null)
10
+ return Number(outcome.filledSize);
11
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
12
+ return null;
13
+ const r = raw;
14
+ if (typeof r.status !== "string" || typeof r.filledSize !== "string")
15
+ return null;
16
+ const n = Number(r.filledSize);
17
+ return Number.isFinite(n) ? n : null;
18
+ }
@@ -2,7 +2,7 @@ export { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, } from "
2
2
  export type { ConditionType, Condition, RiskConfig, GridStrategyParams, DCAStrategyParams, FundingArbStrategyParams, GenericStrategyParams, StrategyParams, BotConfig, } from "./config.js";
3
3
  export { runBot } from "./engine.js";
4
4
  export type { BotLog, BotOutputMode } from "./engine.js";
5
- export { PRESETS, getPreset, getPresetsByStrategy } from "./presets.js";
5
+ export { PRESETS, getPreset, getPresetsByStrategy, withPresetSize } from "./presets.js";
6
6
  export type { Preset } from "./presets.js";
7
7
  export { evaluateCondition, evaluateAllConditions, getMarketSnapshot, calculateRSI } from "./conditions.js";
8
8
  export type { MarketSnapshot } from "./conditions.js";
package/dist/bot/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // bot/ barrel — public API
2
2
  export { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, } from "./config.js";
3
3
  export { runBot } from "./engine.js";
4
- export { PRESETS, getPreset, getPresetsByStrategy } from "./presets.js";
4
+ export { PRESETS, getPreset, getPresetsByStrategy, withPresetSize } from "./presets.js";
5
5
  export { evaluateCondition, evaluateAllConditions, getMarketSnapshot, calculateRSI } from "./conditions.js";
6
6
  export * from "./indicators.js";
7
7
  export * from "./strategy-types.js";
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Verified perp market order for bot / arb legs (design §12.3).
3
+ *
4
+ * A resolved `marketOrder()` promise does not prove a fill: Lighter returns success
5
+ * when its /tx poll times out ("assuming executed") or the IOC filled nothing, and a
6
+ * transport failure after the request leaves the outcome unknown. Every bot perp leg
7
+ * that hedges (or un-hedges) another leg goes through placePerpVerified():
8
+ *
9
+ * - the position on the venue is read BEFORE the order (failure → ORDER_REJECTED,
10
+ * nothing sent);
11
+ * - the order call throws a TYPED definite rejection (isDefiniteOrderFailure) → rethrown
12
+ * unchanged;
13
+ * - otherwise — a resolved call, or ANY other thrown value (untyped Error, transport
14
+ * failure, HL "HL exchange API error (5xx)", Pacifica "POST … failed (5xx)", any
15
+ * non-definite PerpError) — the fill is MEASURED as the position delta (uncached
16
+ * reads, polled): moved ≥ 80% of the size (or a reduce-only order flattened the
17
+ * position) → filled; no such movement, or an unreadable position → ORDER_PENDING
18
+ * (do NOT resubmit, the caller quarantines). An untyped error is never read as a
19
+ * rejection. The one measured exception: a reduce-only order that threw a plain venue
20
+ * message while the position was flat before AND after cannot have done anything →
21
+ * ORDER_REJECTED carrying the venue message and details.reduceOnlyFlat (isPositionGone()).
22
+ * Likewise a plain (non-transport, non-5xx) "not a multiple of lot size N" rejection with
23
+ * the position measured unchanged → ORDER_REJECTED with details.lotSize (rejectedLotSize()).
24
+ *
25
+ * Venue-final IOC outcome (design §13.10 #4): when the adapter is IocOutcomeCapable and
26
+ * iocOutcomeOf(raw) reports the order's FINAL filled size, that report is authoritative
27
+ * (no 80% rule) once cross-checked against the measured position delta (tolerance one lot
28
+ * step, taken from the precision of the order size / reported fill):
29
+ * - final 0 and the position did not move → ORDER_REJECTED (definite, nothing filled);
30
+ * the position DID move → the measured delta is returned (discrepancy logged to stderr);
31
+ * - final 0 < filled ≤ size and the delta agrees → filledSize = the venue-reported size
32
+ * (a final partial fill — callers size the 2nd leg / trim / residual-close from it);
33
+ * the delta never agrees (or the position is unreadable) → ORDER_PENDING.
34
+ * No outcome (adapter not capable, or iocOutcomeOf → null) → the measurement path above.
35
+ */
36
+ import type { ExchangeAdapter } from "../exchanges/index.js";
37
+ import { type ResolvedSlippage } from "../exchanges/slippage.js";
38
+ export interface PerpFill {
39
+ /** Base units filled, > 0: the measured position delta, or the venue-final IOC fill cross-checked against it. */
40
+ filledSize: number;
41
+ /** The adapter's raw result (null when the call threw but the position moved). */
42
+ raw: unknown;
43
+ }
44
+ /** Net signed position (long > 0, short < 0) for `symbol`, read uncached. Throws on a bad read. */
45
+ export declare function signedPerpPosition(adapter: ExchangeAdapter, symbol: string): Promise<number>;
46
+ export declare function placePerpVerified(adapter: ExchangeAdapter, venue: string, symbol: string, side: "buy" | "sell", size: string, opts?: {
47
+ reduceOnly?: boolean;
48
+ /**
49
+ * Sends the order instead of adapter.marketOrder (e.g. an IOC limit from smartOrder).
50
+ * It must place exactly `side` `size` `symbol` (reduce-only when opts.reduceOnly);
51
+ * the fill is measured the same way.
52
+ */
53
+ place?: () => Promise<unknown>;
54
+ /** Slippage bound for adapter.marketOrder; omitted → resolveSlippage(venue, null) (bot/arb paths). */
55
+ slippage?: ResolvedSlippage;
56
+ /** Market lot step when known: fill comparisons tolerate half a lot (else float-noise epsilon). */
57
+ stepSize?: string | null;
58
+ }): Promise<PerpFill>;
59
+ /** Lot size from a definite lot-size rejection (placePerpVerified ORDER_REJECTED details.lotSize), else null. */
60
+ export declare function rejectedLotSize(e: unknown): number | null;
61
+ /** true when `e` says the order outcome is unknown (quarantine, never retry / roll back blindly). */
62
+ export declare function isOutcomeUnknown(e: unknown): boolean;
63
+ /**
64
+ * Second-leg residual correction (design §12.3): the second leg is sized to the first
65
+ * leg's VERIFIED fill but may itself fill less (≥ 80% measured, or any venue-final partial fill). Reduce the first leg by exactly
66
+ * this trade's excess (firstFilled − secondFilled, floored to `decimals`) — reduce-only,
67
+ * verified, never the venue's whole position. `firstSide` is the side the first leg
68
+ * opened with. Returns the excess closed ("0" when the legs already match). Throws when
69
+ * the correction was rejected or is unverified: the residual is unhedged → the caller
70
+ * quarantines.
71
+ */
72
+ export declare function trimFirstLegToHedge(adapter: ExchangeAdapter, venue: string, symbol: string, firstSide: "buy" | "sell", firstFilled: number, secondFilled: number, decimals: number): Promise<string>;
73
+ export interface PerpCloseResult {
74
+ /** Base units this call closed (measured, both orders). */
75
+ closed: number;
76
+ /** Base units of the requested close still open on the venue (0 = done). > 0 → the caller quarantines. */
77
+ residual: number;
78
+ /** Why a residual remains (residual close failed / position unreadable), or null. */
79
+ note: string | null;
80
+ }
81
+ /**
82
+ * Verified reduce-only close of `size` with residual handling (design §12.3, §13.10 #4). placePerpVerified
83
+ * accepts a ≥ 80% measured fill or any venue-final partial IOC fill (cross-checked); a close must
84
+ * not leave the rest silently open, so:
85
+ * 1. reduce-only close of `size` (opts.place, e.g. a smartOrder IOC, or a market order);
86
+ * 2. if it filled less than `size`, re-read the position: the residual is
87
+ * min(size − closed, open size in the closed direction), floored to `decimals`;
88
+ * 3. residual > 0 → ONE more verified reduce-only MARKET close of the residual;
89
+ * 4. re-read; whatever is still open is returned as `residual` (never retried again).
90
+ * Errors of the FIRST close propagate unchanged (definite → nothing happened; ORDER_PENDING →
91
+ * unknown). Once the first close moved the position, a failed / unknown residual close or an
92
+ * unreadable position is reported as `residual` (> 0) + `note` — the caller quarantines.
93
+ */
94
+ export declare function closePerpVerified(adapter: ExchangeAdapter, venue: string, symbol: string, side: "buy" | "sell", size: string, opts?: {
95
+ place?: () => Promise<unknown>;
96
+ decimals?: number;
97
+ slippage?: ResolvedSlippage;
98
+ stepSize?: string | null;
99
+ }): Promise<PerpCloseResult>;
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Verified perp market order for bot / arb legs (design §12.3).
3
+ *
4
+ * A resolved `marketOrder()` promise does not prove a fill: Lighter returns success
5
+ * when its /tx poll times out ("assuming executed") or the IOC filled nothing, and a
6
+ * transport failure after the request leaves the outcome unknown. Every bot perp leg
7
+ * that hedges (or un-hedges) another leg goes through placePerpVerified():
8
+ *
9
+ * - the position on the venue is read BEFORE the order (failure → ORDER_REJECTED,
10
+ * nothing sent);
11
+ * - the order call throws a TYPED definite rejection (isDefiniteOrderFailure) → rethrown
12
+ * unchanged;
13
+ * - otherwise — a resolved call, or ANY other thrown value (untyped Error, transport
14
+ * failure, HL "HL exchange API error (5xx)", Pacifica "POST … failed (5xx)", any
15
+ * non-definite PerpError) — the fill is MEASURED as the position delta (uncached
16
+ * reads, polled): moved ≥ 80% of the size (or a reduce-only order flattened the
17
+ * position) → filled; no such movement, or an unreadable position → ORDER_PENDING
18
+ * (do NOT resubmit, the caller quarantines). An untyped error is never read as a
19
+ * rejection. The one measured exception: a reduce-only order that threw a plain venue
20
+ * message while the position was flat before AND after cannot have done anything →
21
+ * ORDER_REJECTED carrying the venue message and details.reduceOnlyFlat (isPositionGone()).
22
+ * Likewise a plain (non-transport, non-5xx) "not a multiple of lot size N" rejection with
23
+ * the position measured unchanged → ORDER_REJECTED with details.lotSize (rejectedLotSize()).
24
+ *
25
+ * Venue-final IOC outcome (design §13.10 #4): when the adapter is IocOutcomeCapable and
26
+ * iocOutcomeOf(raw) reports the order's FINAL filled size, that report is authoritative
27
+ * (no 80% rule) once cross-checked against the measured position delta (tolerance one lot
28
+ * step, taken from the precision of the order size / reported fill):
29
+ * - final 0 and the position did not move → ORDER_REJECTED (definite, nothing filled);
30
+ * the position DID move → the measured delta is returned (discrepancy logged to stderr);
31
+ * - final 0 < filled ≤ size and the delta agrees → filledSize = the venue-reported size
32
+ * (a final partial fill — callers size the 2nd leg / trim / residual-close from it);
33
+ * the delta never agrees (or the position is unreadable) → ORDER_PENDING.
34
+ * No outcome (adapter not capable, or iocOutcomeOf → null) → the measurement path above.
35
+ */
36
+ import { invalidateCache } from "../cache.js";
37
+ import { resolveAdapterSlippage } from "../exchanges/slippage.js";
38
+ import { PerpError, extractErrorMessage, isDefiniteOrderFailure, isTransportFailure, isUnknownOutcomeServerError, orderNotSent, orderOutcomeUnknown } from "../errors.js";
39
+ import { fillSizeString, sizeDecimalsOf } from "../exchanges/spot-result.js";
40
+ import { iocLotTolerance, isIocOutcomeCapable } from "../exchanges/ioc-outcome.js";
41
+ import { matchSymbol } from "./strategies/funding-arb-utils.js";
42
+ import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
43
+ /** Share of the requested size the position must move by to count as filled (measured path only — not for venue-final outcomes). */
44
+ const MIN_FILL_RATIO = 0.8;
45
+ /**
46
+ * Position polls after the order (ms between reads; ~10 s in total). Venue account reads can
47
+ * lag the fill (Lighter testnet: /account showed the position a few seconds after the tx
48
+ * executed), so the window is bounded but long enough; a match ends the polling early.
49
+ */
50
+ const POLL_DELAYS_MS = [0, 1000, 1500, 2000, 2500, 3000];
51
+ /**
52
+ * HIP-3 dex coin key ("xyz:TSLA"): the order symbol and the position's coin go through
53
+ * the SAME resolution the HL adapter uses for dex markets (resolveSymbol + dex prefix),
54
+ * so "TSLA", "xyz:TSLA" and "XYZ:TSLA" all name one position.
55
+ */
56
+ function hlDexCoinKey(adapter, sym) {
57
+ const resolved = adapter.resolveSymbol(sym);
58
+ return (resolved.includes(":") ? resolved : `${adapter.dex}:${resolved}`).toUpperCase();
59
+ }
60
+ /** Net signed position (long > 0, short < 0) for `symbol`, read uncached. Throws on a bad read. */
61
+ export async function signedPerpPosition(adapter, symbol) {
62
+ invalidateCache("acct");
63
+ let matches = (posSym) => matchSymbol(posSym, symbol);
64
+ if (adapter instanceof HyperliquidAdapter && adapter.dex !== "") {
65
+ const target = hlDexCoinKey(adapter, symbol);
66
+ matches = (posSym) => hlDexCoinKey(adapter, posSym) === target;
67
+ }
68
+ let net = 0;
69
+ for (const p of await adapter.getPositions()) {
70
+ if (!matches(p.symbol))
71
+ continue;
72
+ const n = Math.abs(Number(p.size));
73
+ if (!Number.isFinite(n))
74
+ throw new PerpError("EXCHANGE_ERROR", `${adapter.name} position size for ${p.symbol} is not a number: ${p.size}`);
75
+ net += p.side === "long" ? n : -n;
76
+ }
77
+ return net;
78
+ }
79
+ /** First HL-style per-order status in a raw result, or null. */
80
+ function firstVenueStatus(raw) {
81
+ const statuses = raw?.response?.data?.statuses;
82
+ return Array.isArray(statuses) && statuses.length > 0 ? statuses[0] : null;
83
+ }
84
+ /** HL-style per-order status in a raw result: error → definite rejection; 0 fill → definite. */
85
+ function checkVenueStatus(venue, what, raw) {
86
+ const st = firstVenueStatus(raw);
87
+ if (st === null)
88
+ return;
89
+ if (st.error)
90
+ throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${String(st.error)}`, { exchange: venue });
91
+ const filled = st.filled;
92
+ if (st.resting === undefined && filled !== undefined && Number(filled.totalSz ?? 0) === 0) {
93
+ throw new PerpError("ORDER_REJECTED", `${venue} ${what}: 0 fill`, { exchange: venue });
94
+ }
95
+ }
96
+ export async function placePerpVerified(adapter, venue, symbol, side, size, opts = {}) {
97
+ const what = `perp ${side} ${size} ${symbol}${opts.reduceOnly ? " (reduce-only)" : ""}`;
98
+ const requested = Number(size);
99
+ if (!(requested > 0))
100
+ throw new PerpError("INVALID_PARAMS", `${what}: size must be positive`);
101
+ let before;
102
+ try {
103
+ before = await signedPerpPosition(adapter, symbol);
104
+ }
105
+ catch (e) {
106
+ throw orderNotSent(venue, `${what} (pre-order position read failed)`, e);
107
+ }
108
+ /** Base units moved in the order's direction (null when no fill is observable yet) and the last read position. */
109
+ const measure = async () => {
110
+ let last = before;
111
+ for (const delay of POLL_DELAYS_MS) {
112
+ if (delay > 0)
113
+ await new Promise(r => setTimeout(r, delay));
114
+ last = await signedPerpPosition(adapter, symbol);
115
+ const moved = side === "buy" ? last - before : before - last;
116
+ if (moved >= requested * MIN_FILL_RATIO)
117
+ return { moved: Math.min(moved, requested), last };
118
+ if (opts.reduceOnly && moved > 0 && Math.abs(last) < requested * 1e-6)
119
+ return { moved, last }; // flattened a smaller position
120
+ }
121
+ return { moved: null, last };
122
+ };
123
+ let raw;
124
+ try {
125
+ raw = opts.place !== undefined
126
+ ? await opts.place()
127
+ : await adapter.marketOrder(symbol, side, size, { reduceOnly: opts.reduceOnly === true, slippage: opts.slippage ?? resolveAdapterSlippage(adapter, null) });
128
+ }
129
+ catch (e) {
130
+ if (isDefiniteOrderFailure(e)) {
131
+ if (opts.reduceOnly === true && e instanceof PerpError && e.structured.code === "POSITION_NOT_FOUND") {
132
+ // The adapter read the position, found none and sent nothing (QFEX; details.sent === false):
133
+ // the same typed "nothing to reduce" as below (isPositionGone()) — no poll, no quarantine.
134
+ throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${e.message} (no ${symbol} position to reduce)`, { exchange: venue, reduceOnlyFlat: true });
135
+ }
136
+ throw e;
137
+ }
138
+ // Not a typed rejection → the order may have been placed: measure, never assume.
139
+ let m;
140
+ try {
141
+ m = await measure();
142
+ }
143
+ catch (readErr) {
144
+ throw orderOutcomeUnknown(venue, what, e, `Do NOT resubmit — check the ${venue} ${symbol} position (position read also failed: ${readErr instanceof Error ? readErr.message : String(readErr)})`);
145
+ }
146
+ if (m.moved !== null)
147
+ return { filledSize: m.moved, raw: null };
148
+ const postSend = e instanceof PerpError || isTransportFailure(e) || isUnknownOutcomeServerError(e);
149
+ const flat = (n) => Math.abs(n) < requested * 1e-6;
150
+ const lot = !postSend ? /not a multiple of lot size (\d+(?:\.\d+)?)/.exec(extractErrorMessage(e)) : null;
151
+ if (lot !== null && flat(m.last - before)) {
152
+ // Venue lot-size validation rejection (e.g. Pacifica 4xx) and the position did not move: not placed.
153
+ throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${extractErrorMessage(e)}`, { exchange: venue, lotSize: Number(lot[1]) });
154
+ }
155
+ if (opts.reduceOnly && !postSend && flat(before) && flat(m.last)) {
156
+ // Reduce-only cannot open a position: flat before and after = nothing happened (position already gone).
157
+ throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${extractErrorMessage(e)} (no ${symbol} position to reduce)`, { exchange: venue, reduceOnlyFlat: true });
158
+ }
159
+ throw orderOutcomeUnknown(venue, what, e, `Do NOT resubmit — the order may still execute. Check the ${venue} ${symbol} position`);
160
+ }
161
+ // Venue-final IOC outcome (§13.10 #4): the venue reported what this order filled; nothing more can fill.
162
+ const outcome = isIocOutcomeCapable(adapter) ? adapter.iocOutcomeOf(raw) : null;
163
+ if (outcome !== null) {
164
+ const reported = Number(outcome.filledSize);
165
+ if (!(Number.isFinite(reported) && reported >= 0)) {
166
+ throw orderOutcomeUnknown(venue, what, new Error(`venue reported an unusable final fill "${outcome.filledSize}"`));
167
+ }
168
+ const tol = iocLotTolerance(size, outcome.filledSize, opts.stepSize);
169
+ if (reported > requested + tol) {
170
+ throw orderOutcomeUnknown(venue, what, new Error(`venue reported a final fill of ${outcome.filledSize}, more than the ${size} ordered`));
171
+ }
172
+ let measured = 0;
173
+ let last = before;
174
+ try {
175
+ for (const delay of POLL_DELAYS_MS) {
176
+ if (delay > 0)
177
+ await new Promise(r => setTimeout(r, delay));
178
+ last = await signedPerpPosition(adapter, symbol);
179
+ measured = side === "buy" ? last - before : before - last;
180
+ // 0 reported: keep polling the whole schedule so a lagging fill is still seen.
181
+ if (reported > 0 ? Math.abs(measured - reported) <= tol : measured > tol)
182
+ break;
183
+ }
184
+ }
185
+ catch (readErr) {
186
+ throw orderOutcomeUnknown(venue, what, new Error(`venue reported a final fill of ${outcome.filledSize} but the position could not be read: ${readErr instanceof Error ? readErr.message : String(readErr)}`));
187
+ }
188
+ if (reported === 0) {
189
+ if (!(measured > tol)) {
190
+ const venueError = firstVenueStatus(raw)?.error;
191
+ // Reduce-only with the position flat before and after: nothing to reduce (isPositionGone()).
192
+ const flat = (n) => Math.abs(n) < requested * 1e-6;
193
+ const gone = opts.reduceOnly === true && flat(before) && flat(last);
194
+ throw new PerpError("ORDER_REJECTED", `${venue} ${what}: ${venueError ? String(venueError) : "IOC filled nothing"} (venue-final, position unchanged)${gone ? ` (no ${symbol} position to reduce)` : ""}`, { exchange: venue, ...(gone ? { reduceOnlyFlat: true } : {}) });
195
+ }
196
+ process.stderr.write(`[warn] ${venue} ${what}: venue reported a final fill of 0 but the position moved ${measured} — using the measured fill\n`);
197
+ return { filledSize: Math.min(measured, requested), raw };
198
+ }
199
+ if (Math.abs(measured - reported) > tol) {
200
+ throw orderOutcomeUnknown(venue, what, new Error(`venue reported a final fill of ${outcome.filledSize} but the ${symbol} position moved ${measured} (before ${before})`), `Do NOT resubmit — check the ${venue} ${symbol} position`);
201
+ }
202
+ return { filledSize: Math.min(reported, requested), raw };
203
+ }
204
+ checkVenueStatus(venue, what, raw);
205
+ let moved;
206
+ try {
207
+ moved = (await measure()).moved;
208
+ }
209
+ catch (readErr) {
210
+ throw orderOutcomeUnknown(venue, what, new Error(`order acknowledged but the position could not be read: ${readErr instanceof Error ? readErr.message : String(readErr)}`));
211
+ }
212
+ if (moved === null) {
213
+ throw orderOutcomeUnknown(venue, what, new Error(`order acknowledged but the ${symbol} position did not move (before ${before})`), `Do NOT resubmit — the order may be unconfirmed. Check the ${venue} ${symbol} position`);
214
+ }
215
+ return { filledSize: moved, raw };
216
+ }
217
+ /** Lot size from a definite lot-size rejection (placePerpVerified ORDER_REJECTED details.lotSize), else null. */
218
+ export function rejectedLotSize(e) {
219
+ if (!(e instanceof PerpError) || e.structured.code !== "ORDER_REJECTED")
220
+ return null;
221
+ const lot = e.structured.details?.lotSize;
222
+ return typeof lot === "number" && lot > 0 ? lot : null;
223
+ }
224
+ /** true when `e` says the order outcome is unknown (quarantine, never retry / roll back blindly). */
225
+ export function isOutcomeUnknown(e) {
226
+ return e instanceof PerpError && e.structured.code === "ORDER_PENDING";
227
+ }
228
+ /**
229
+ * Second-leg residual correction (design §12.3): the second leg is sized to the first
230
+ * leg's VERIFIED fill but may itself fill less (≥ 80% measured, or any venue-final partial fill). Reduce the first leg by exactly
231
+ * this trade's excess (firstFilled − secondFilled, floored to `decimals`) — reduce-only,
232
+ * verified, never the venue's whole position. `firstSide` is the side the first leg
233
+ * opened with. Returns the excess closed ("0" when the legs already match). Throws when
234
+ * the correction was rejected or is unverified: the residual is unhedged → the caller
235
+ * quarantines.
236
+ */
237
+ export async function trimFirstLegToHedge(adapter, venue, symbol, firstSide, firstFilled, secondFilled, decimals) {
238
+ const excess = fillSizeString(firstFilled - secondFilled, decimals);
239
+ if (!(Number(excess) > 0))
240
+ return "0";
241
+ const c = await closePerpVerified(adapter, venue, symbol, firstSide === "buy" ? "sell" : "buy", excess, { decimals });
242
+ if (c.residual > 0) {
243
+ throw new PerpError("ORDER_PARTIAL", `${venue} trim of ${excess} ${symbol} closed ${c.closed}; ${c.residual} left unhedged${c.note !== null ? ` (${c.note})` : ""}`, { exchange: venue });
244
+ }
245
+ return excess;
246
+ }
247
+ /**
248
+ * Verified reduce-only close of `size` with residual handling (design §12.3, §13.10 #4). placePerpVerified
249
+ * accepts a ≥ 80% measured fill or any venue-final partial IOC fill (cross-checked); a close must
250
+ * not leave the rest silently open, so:
251
+ * 1. reduce-only close of `size` (opts.place, e.g. a smartOrder IOC, or a market order);
252
+ * 2. if it filled less than `size`, re-read the position: the residual is
253
+ * min(size − closed, open size in the closed direction), floored to `decimals`;
254
+ * 3. residual > 0 → ONE more verified reduce-only MARKET close of the residual;
255
+ * 4. re-read; whatever is still open is returned as `residual` (never retried again).
256
+ * Errors of the FIRST close propagate unchanged (definite → nothing happened; ORDER_PENDING →
257
+ * unknown). Once the first close moved the position, a failed / unknown residual close or an
258
+ * unreadable position is reported as `residual` (> 0) + `note` — the caller quarantines.
259
+ */
260
+ export async function closePerpVerified(adapter, venue, symbol, side, size, opts = {}) {
261
+ const requested = Number(size);
262
+ const dec = opts.decimals ?? sizeDecimalsOf(size);
263
+ const slip = { ...(opts.slippage !== undefined ? { slippage: opts.slippage } : {}), ...(opts.stepSize !== undefined ? { stepSize: opts.stepSize } : {}) };
264
+ const first = await placePerpVerified(adapter, venue, symbol, side, size, { reduceOnly: true, ...slip, ...(opts.place !== undefined ? { place: opts.place } : {}) });
265
+ let closed = first.filledSize;
266
+ /** Residual of this close still open on the venue (closing a long = sell → the long part). */
267
+ const openResidual = async () => {
268
+ const pos = await signedPerpPosition(adapter, symbol);
269
+ const openInDir = side === "sell" ? Math.max(pos, 0) : Math.max(-pos, 0);
270
+ return Number(fillSizeString(Math.min(requested - closed, openInDir), dec));
271
+ };
272
+ if (closed >= requested * (1 - 1e-6))
273
+ return { closed, residual: 0, note: null };
274
+ let residual;
275
+ try {
276
+ residual = await openResidual();
277
+ }
278
+ catch (e) {
279
+ return { closed, residual: Number(fillSizeString(requested - closed, dec)), note: `position unreadable after a partial close: ${extractErrorMessage(e)}` };
280
+ }
281
+ if (!(residual > 0))
282
+ return { closed, residual: 0, note: null };
283
+ const resStr = fillSizeString(residual, dec);
284
+ try {
285
+ closed += (await placePerpVerified(adapter, venue, symbol, side, resStr, { reduceOnly: true, ...slip })).filledSize;
286
+ }
287
+ catch (e) {
288
+ return { closed, residual, note: `residual close of ${resStr} ${isOutcomeUnknown(e) ? "unconfirmed" : "failed"}: ${extractErrorMessage(e)}` };
289
+ }
290
+ if (closed >= requested * (1 - 1e-6))
291
+ return { closed, residual: 0, note: null };
292
+ try {
293
+ residual = await openResidual();
294
+ }
295
+ catch (e) {
296
+ return { closed, residual: Number(fillSizeString(requested - closed, dec)), note: `position unreadable after the residual close: ${extractErrorMessage(e)}` };
297
+ }
298
+ return { closed, residual, note: residual > 0 ? `residual close of ${resStr} filled partially` : null };
299
+ }
@@ -7,5 +7,12 @@ export interface Preset {
7
7
  buildConfig: (exchange: string, symbol: string) => BotConfig;
8
8
  }
9
9
  export declare const PRESETS: Preset[];
10
+ /**
11
+ * `perp strategy preset --size`: replaces the preset's size field — grid `size` (total base
12
+ * size, split across the grids), DCA `amount` (base size per order), arb `size_usd` ($ per leg).
13
+ */
14
+ export declare function withPresetSize<T extends {
15
+ strategy: unknown;
16
+ }>(config: T, size: string): T;
10
17
  export declare function getPreset(name: string): Preset | undefined;
11
18
  export declare function getPresetsByStrategy(strategy: string): Preset[];
@@ -1,3 +1,4 @@
1
+ import { PerpError } from "../errors.js";
1
2
  const DEFAULT_RISK = {
2
3
  max_position_usd: 1000,
3
4
  max_daily_loss: 100,
@@ -289,6 +290,22 @@ export const PRESETS = [
289
290
  arbStandard,
290
291
  arbAggressive,
291
292
  ];
293
+ /**
294
+ * `perp strategy preset --size`: replaces the preset's size field — grid `size` (total base
295
+ * size, split across the grids), DCA `amount` (base size per order), arb `size_usd` ($ per leg).
296
+ */
297
+ export function withPresetSize(config, size) {
298
+ const n = Number(size);
299
+ if (!Number.isFinite(n) || n <= 0) {
300
+ throw new PerpError("INVALID_PARAMS", `--size must be a positive number (got ${JSON.stringify(size)})`);
301
+ }
302
+ const s = config.strategy;
303
+ const field = "size_usd" in s ? "size_usd" : "amount" in s ? "amount" : "size" in s ? "size" : null;
304
+ if (field === null) {
305
+ throw new PerpError("INVALID_PARAMS", `preset strategy ${String(s.type)} has no size field to override`);
306
+ }
307
+ return { ...config, strategy: { ...s, [field]: n } };
308
+ }
292
309
  export function getPreset(name) {
293
310
  return PRESETS.find(p => p.name === name);
294
311
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Position quarantine (design §12.3) — GLOBAL per symbol.
3
+ *
4
+ * A spot leg whose outcome is unknown (ORDER_PENDING), a perp leg that could not be
5
+ * confirmed, a close whose post-trade verification failed, or a position left unhedged
6
+ * by a failed rollback must NOT be retried automatically: a retry can double-buy,
7
+ * double-sell or open a naked perp. Such a symbol is quarantined here, persisted in
8
+ * ~/.perp/bot-quarantine.json, and skipped by EVERY automated path — the bot strategies
9
+ * (funding-arb, funding-arb-v2, spot-perp-arb, funding-auto), `perp arb exec` (perp-perp
10
+ * and spot:<venue>) and the `perp arb auto` / `arb dex-auto` daemons — no entry, no close,
11
+ * no rotation — until a human clears it (`perp arb close` refuses it without --force):
12
+ *
13
+ * perp strategy quarantine # list
14
+ * perp strategy quarantine-clear <symbol> [--strategy <source>]
15
+ *
16
+ * Entries are stored per source ("<source>:<KEY>", source = strategy name or CLI path)
17
+ * but CHECKED by the canonical key alone (quarantineKey: HIP-3 dex prefix and "-PERP"
18
+ * stripped, DENOMINATION_ALIASES applied, uppercased — "xyz:TSLA" ≡ "TSLA", "KPEPE" ≡
19
+ * "1000PEPE"): one venue's unknown outcome blocks the underlying for all strategies,
20
+ * venues and processes. `entry.symbol` keeps the symbol as the source reported it. The file is re-read on every check, so a clear
21
+ * from the CLI takes effect in running bots on their next tick (no restart). Only an
22
+ * entry whose persist failed is held in process memory (logged CRITICAL); it holds
23
+ * until that process stops.
24
+ *
25
+ * Network scope: an entry records the network it was set on (`network`); a process that called
26
+ * setQuarantineNetwork(n) sees only entries of n plus entries of unknown network (null — set by
27
+ * a process that never declared its network: they apply to EVERY network, fail closed). Entries
28
+ * written before network scoping have no tag and are MAINNET (like the arb state's legacy file).
29
+ * A process that never declared its network sees every entry (fail closed). Mainnet / unknown
30
+ * entries keep the legacy key "<source>:<KEY>"; testnet ones are keyed "testnet:<source>:<KEY>".
31
+ * Every load → modify → save of the file holds the cross-process lock (withFileLock).
32
+ */
33
+ import type { Network } from "../exchanges/venue-types.js";
34
+ export declare const QUARANTINE_FILE: string;
35
+ export interface QuarantineEntry {
36
+ /** Who quarantined it: a strategy name or a CLI path ("arb-exec"). */
37
+ strategy: string;
38
+ /** The symbol as the source reported it (display); matched via quarantineKey(). */
39
+ symbol: string;
40
+ /** What happened (the CRITICAL log line). */
41
+ reason: string;
42
+ /** What the human must check before clearing. */
43
+ remediation: string;
44
+ time: string;
45
+ /** Network the entry was set on; null = unknown (applies to every network). Legacy untagged entries load as "mainnet". */
46
+ network: Network | null;
47
+ }
48
+ /**
49
+ * Canonical quarantine key for a symbol — the ONLY way a symbol is compared here (set,
50
+ * check, clear): uppercased, HIP-3 dex prefix ("xyz:") and "-PERP" suffix stripped,
51
+ * DENOMINATION_ALIASES applied (KPEPE → 1000PEPE, UBTC → BTC).
52
+ */
53
+ export declare function quarantineKey(symbol: string): string;
54
+ /** Declare the network this process trades on: entries are set on it and checked against it. */
55
+ export declare function setQuarantineNetwork(network: Network): void;
56
+ /** Test hook: back to "network not declared". */
57
+ export declare function resetQuarantineNetwork(): void;
58
+ /**
59
+ * Quarantine `symbol` (for every strategy / CLI path) and log CRITICAL with the
60
+ * remediation. `source` records who did it. If persisting fails the entry is held in
61
+ * this process's memory and the failure is logged loudly.
62
+ */
63
+ export declare function quarantineSymbol(source: string, symbol: string, reason: string, remediation: string, log: (msg: string) => void): void;
64
+ /**
65
+ * The quarantine entry blocking `symbol` (any source), or null. Re-reads the file on
66
+ * every call. An unreadable quarantine file fails CLOSED (a synthetic entry, logged) —
67
+ * never "not quarantined".
68
+ */
69
+ export declare function quarantineEntryFor(symbol: string, log: (msg: string) => void): QuarantineEntry | null;
70
+ /** true when `symbol` is quarantined by any strategy / CLI path (see quarantineEntryFor). */
71
+ export declare function isQuarantined(symbol: string, log: (msg: string) => void): boolean;
72
+ /** Entries that apply on this process's network (every entry when it never declared one). */
73
+ export declare function listQuarantine(): QuarantineEntry[];
74
+ /** Remove entries (that apply on this process's network) whose quarantineKey matches symbol's (one source, or every source when null). Returns the removed entries. */
75
+ export declare function clearQuarantine(symbol: string, strategy: string | null): QuarantineEntry[];