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
@@ -1,7 +1,16 @@
1
1
  import { registerStrategy } from "../strategy-registry.js";
2
+ import { isQuarantined, quarantineKey, quarantineSymbol } from "../quarantine.js";
3
+ import { PerpError } from "../../errors.js";
4
+ import { closePerpVerified, isOutcomeUnknown, placePerpVerified, trimFirstLegToHedge } from "../perp-order.js";
5
+ import { fillSizeString, sizeDecimalsOf } from "../../exchanges/spot-result.js";
6
+ import { isPositionGone, assertFundingArbLegs, cancelBotSymbols } from "./funding-arb-utils.js";
7
+ /** Consecutive failed closes before the symbol is quarantined. */
8
+ const MAX_CLOSE_FAILURES = 3;
2
9
  export class FundingArbV2Strategy {
3
10
  name = "funding-arb-v2";
4
11
  _config = {};
12
+ /** Symbols this bot entered or recovered this run — the only symbols its stop cancels orders on. */
13
+ _traded = new Set();
5
14
  describe() {
6
15
  return {
7
16
  description: "Enhanced multi-exchange funding rate arbitrage with auto-close",
@@ -20,11 +29,68 @@ export class FundingArbV2Strategy {
20
29
  get sizeUsd() { return Number(this._config.sizeUsd ?? 50); }
21
30
  get scanIntervalSec() { return Number(this._config.scanIntervalSec ?? 60); }
22
31
  async init(ctx, _snapshot) {
32
+ assertFundingArbLegs(ctx);
23
33
  this._config = ctx.config;
24
- ctx.state.set("positions", []);
34
+ // A restart must never re-enter (or ignore) what is already held: rebuild the pairs from the venues.
35
+ const recovered = await this.recoverPositions(this.getAdapters(ctx), ctx);
36
+ for (const p of recovered)
37
+ this._traded.add(p.symbol);
38
+ ctx.state.set("positions", recovered);
25
39
  ctx.state.set("lastScan", 0);
40
+ if (recovered.length > 0)
41
+ ctx.log(` [ARB-V2] Recovered ${recovered.length} pair(s): ${recovered.map(p => `${p.symbol} ${p.longExchange}↔${p.shortExchange} ${p.size}`).join(", ")}`);
26
42
  ctx.log(` [ARB-V2] Ready | minSpread=${this.minSpread}% closeSpread=${this.closeSpread}% maxPos=${this.maxPositions} size=$${this.sizeUsd}`);
27
43
  }
44
+ /**
45
+ * Pairs held on the venues: a long on one venue + a short of the same symbol and EXACTLY the
46
+ * same size on another. A single leg or a size-mismatched long/short is quarantined (both
47
+ * sizes recorded) — never tracked, never stacked on. A venue whose positions cannot be read
48
+ * throws (the start fails and is retried): unknown exposure is never "flat".
49
+ */
50
+ async recoverPositions(adapters, ctx) {
51
+ const legs = [];
52
+ for (const [venue, a] of adapters) {
53
+ let list;
54
+ try {
55
+ list = await a.getPositions();
56
+ }
57
+ catch (e) {
58
+ throw new PerpError("EXCHANGE_ERROR", `[ARB-V2] ${venue} positions unreadable at start (${e instanceof Error ? e.message : String(e)}) — not started (held pairs unknown)`, {
59
+ exchange: venue, remediation: `Check ${venue} connectivity (perp -e ${venue} account positions); the bot retries the start`,
60
+ });
61
+ }
62
+ for (const p of list) {
63
+ const size = Math.abs(Number(p.size));
64
+ if (size > 0)
65
+ legs.push({ venue, symbol: p.symbol.toUpperCase(), side: p.side, size, raw: p.size.replace(/^-/, "") });
66
+ }
67
+ }
68
+ const used = new Set();
69
+ const out = [];
70
+ for (const l of legs) {
71
+ if (l.side !== "long" || used.has(l))
72
+ continue;
73
+ const s = legs.find(x => !used.has(x) && x.side === "short" && x.symbol === l.symbol && x.venue !== l.venue && x.size === l.size);
74
+ if (s === undefined)
75
+ continue;
76
+ used.add(l);
77
+ used.add(s);
78
+ out.push({ symbol: l.symbol, longExchange: l.venue, shortExchange: s.venue, entrySpread: 0, entryTime: 0, sizeUsd: 0, size: l.raw, longClosed: false, closeFailures: 0 });
79
+ }
80
+ const leftover = new Map();
81
+ for (const l of legs)
82
+ if (!used.has(l))
83
+ leftover.set(quarantineKey(l.symbol), [...(leftover.get(quarantineKey(l.symbol)) ?? []), l]);
84
+ for (const [key, group] of leftover) {
85
+ if (isQuarantined(key, ctx.log))
86
+ continue;
87
+ const desc = group.map(l => `${l.side} ${l.raw} on ${l.venue}`).join(", ");
88
+ const venues = [...new Set(group.map(l => l.venue))];
89
+ const mismatched = group.some(l => l.side === "long") && group.some(l => l.side === "short");
90
+ quarantineSymbol(this.name, key, mismatched ? `recovery: ${key} long/short sizes do not match (${desc}) — not tracked as a pair` : `recovery: unpaired ${key} perp leg (${desc}) — no hedge found, not tracked`, `Check ${key} on ${venues.join(" and ")} (${venues.map(v => `perp -e ${v} positions`).join("; ")}); close or re-hedge by hand, then: perp strategy quarantine-clear ${key}`, ctx.log);
91
+ }
92
+ return out;
93
+ }
28
94
  async onTick(ctx, _snapshot) {
29
95
  const positions = ctx.state.get("positions");
30
96
  const lastScan = ctx.state.get("lastScan");
@@ -54,14 +120,17 @@ export class FundingArbV2Strategy {
54
120
  }
55
121
  ratesByExchange.set(name, map);
56
122
  }
57
- catch {
58
- // Skip unavailable exchange
123
+ catch (err) {
124
+ // Logged; this venue is left out of this scan (no close decision, no entry on it).
125
+ ctx.log(` [ARB-V2] ${name} markets unavailable: ${err instanceof Error ? err.message : String(err)}`);
59
126
  }
60
127
  }
61
128
  // Phase 1: Check existing positions for auto-close
62
129
  const toClose = [];
63
130
  for (let i = 0; i < positions.length; i++) {
64
131
  const pos = positions[i];
132
+ if (isQuarantined(pos.symbol, ctx.log))
133
+ continue;
65
134
  const longRates = ratesByExchange.get(pos.longExchange);
66
135
  const shortRates = ratesByExchange.get(pos.shortExchange);
67
136
  if (!longRates || !shortRates)
@@ -73,26 +142,8 @@ export class FundingArbV2Strategy {
73
142
  const currentSpread = this.annualizeSpread(shortInfo.rate, longInfo.rate);
74
143
  if (currentSpread <= this.closeSpread) {
75
144
  ctx.log(` [ARB-V2] Auto-close ${pos.symbol}: spread ${currentSpread.toFixed(1)}% <= ${this.closeSpread}% (entry was ${pos.entrySpread.toFixed(1)}%)`);
76
- // Close both legs
77
- const longAdapter = adapters.get(pos.longExchange);
78
- const shortAdapter = adapters.get(pos.shortExchange);
79
- if (longAdapter && shortAdapter) {
80
- const price = longInfo.price;
81
- const size = price > 0 ? (pos.sizeUsd / price).toFixed(6) : "0";
82
- try {
83
- await Promise.all([
84
- longAdapter.marketOrder(pos.symbol, "sell", size),
85
- shortAdapter.marketOrder(pos.symbol, "buy", size),
86
- ]);
87
- ctx.log(` [ARB-V2] Closed ${pos.symbol} (long ${pos.longExchange}, short ${pos.shortExchange})`);
88
- }
89
- catch (err) {
90
- const msg = err instanceof Error ? err.message : String(err);
91
- ctx.log(` [ARB-V2] Close failed for ${pos.symbol}: ${msg}`);
92
- continue; // Don't mark as closed
93
- }
94
- }
95
- toClose.push(i);
145
+ if (await this.closePosition(ctx, pos, adapters))
146
+ toClose.push(i);
96
147
  }
97
148
  }
98
149
  // Remove closed positions (reverse order to preserve indices)
@@ -103,6 +154,11 @@ export class FundingArbV2Strategy {
103
154
  // Phase 2: Scan for new opportunities
104
155
  if (positions.length >= this.maxPositions)
105
156
  return [];
157
+ // Engine: equity unknown → no new entries (the closes above still ran).
158
+ if (ctx.state.get("equityUnknown") === true) {
159
+ ctx.log(` [ARB-V2] Equity unknown — no new entries this scan`);
160
+ return positions.length > 0 ? [{ type: "noop" }] : [];
161
+ }
106
162
  const exchangeNames = [...ratesByExchange.keys()];
107
163
  const allSymbols = new Set();
108
164
  for (const [, map] of ratesByExchange) {
@@ -115,6 +171,8 @@ export class FundingArbV2Strategy {
115
171
  for (const sym of allSymbols) {
116
172
  if (activeSymbols.has(sym))
117
173
  continue;
174
+ if (isQuarantined(sym, ctx.log))
175
+ continue;
118
176
  let minRate = Infinity, maxRate = -Infinity;
119
177
  let minExchange = "", maxExchange = "";
120
178
  let bestPrice = 0;
@@ -149,18 +207,19 @@ export class FundingArbV2Strategy {
149
207
  // Open new positions
150
208
  const slotsAvailable = this.maxPositions - positions.length;
151
209
  const toOpen = opportunities.slice(0, slotsAvailable);
210
+ const live = new Map();
152
211
  for (const opp of toOpen) {
153
212
  const longAdapter = adapters.get(opp.longExchange);
154
213
  const shortAdapter = adapters.get(opp.shortExchange);
155
214
  if (!longAdapter || !shortAdapter || opp.price <= 0)
156
215
  continue;
216
+ if (await this.heldUntracked(opp.symbol, [[opp.longExchange, longAdapter], [opp.shortExchange, shortAdapter]], live, ctx))
217
+ continue;
218
+ this._traded.add(opp.symbol);
157
219
  const size = (this.sizeUsd / opp.price).toFixed(6);
158
- try {
159
- ctx.log(` [ARB-V2] Opening ${opp.symbol}: spread ${opp.spread.toFixed(1)}% (long ${opp.longExchange}, short ${opp.shortExchange})`);
160
- await Promise.all([
161
- longAdapter.marketOrder(opp.symbol, "buy", size),
162
- shortAdapter.marketOrder(opp.symbol, "sell", size),
163
- ]);
220
+ ctx.log(` [ARB-V2] Opening ${opp.symbol}: spread ${opp.spread.toFixed(1)}% (long ${opp.longExchange}, short ${opp.shortExchange})`);
221
+ const opened = await this.openPosition(ctx, opp, size, longAdapter, shortAdapter);
222
+ if (opened !== null) {
164
223
  positions.push({
165
224
  symbol: opp.symbol,
166
225
  longExchange: opp.longExchange,
@@ -168,39 +227,191 @@ export class FundingArbV2Strategy {
168
227
  entrySpread: opp.spread,
169
228
  entryTime: now,
170
229
  sizeUsd: this.sizeUsd,
230
+ size: opened,
231
+ longClosed: false,
232
+ closeFailures: 0,
171
233
  });
172
234
  ctx.log(` [ARB-V2] Position opened! (${positions.length}/${this.maxPositions})`);
173
235
  }
174
- catch (err) {
175
- const msg = err instanceof Error ? err.message : String(err);
176
- ctx.log(` [ARB-V2] Execution failed: ${msg}`);
177
- }
178
236
  }
179
237
  ctx.state.set("positions", positions);
180
238
  // Return noop — arb executes directly via multi-adapter coordination
181
239
  return positions.length > 0 || toOpen.length > 0 ? [{ type: "noop" }] : [];
182
240
  }
241
+ /**
242
+ * Close every tracked position (reduce-only, verified). A position that cannot be
243
+ * closed is never dropped silently: it is quarantined (persisted with what is open)
244
+ * so no bot / arb path touches it until a human resolves it.
245
+ */
183
246
  async onStop(ctx) {
184
247
  const positions = ctx.state.get("positions");
185
- if (positions.length === 0)
186
- return [];
187
248
  const adapters = this.getAdapters(ctx);
249
+ const remaining = [];
188
250
  for (const pos of positions) {
189
- const longAdapter = adapters.get(pos.longExchange);
190
- const shortAdapter = adapters.get(pos.shortExchange);
191
- if (longAdapter && shortAdapter) {
192
- try {
193
- // Best-effort close all legs
194
- await Promise.all([
195
- longAdapter.cancelAllOrders(pos.symbol),
196
- shortAdapter.cancelAllOrders(pos.symbol),
197
- ]);
251
+ if (isQuarantined(pos.symbol, ctx.log)) {
252
+ ctx.log(` [ARB-V2] CRITICAL: ${pos.symbol} is quarantined — left open on stop; resolve manually (perp strategy quarantine)`);
253
+ remaining.push(pos);
254
+ continue;
255
+ }
256
+ ctx.log(` [ARB-V2] Closing ${pos.symbol} on stop (${pos.size})`);
257
+ if (!(await this.closePosition(ctx, pos, adapters))) {
258
+ remaining.push(pos);
259
+ if (!isQuarantined(pos.symbol, ctx.log)) {
260
+ this.quarantine(pos, `still open when the bot stopped (long ${pos.longClosed ? "closed" : `${pos.size} on ${pos.longExchange}`}, short ${pos.size} on ${pos.shortExchange})`, ctx);
261
+ }
262
+ }
263
+ }
264
+ ctx.state.set("positions", remaining);
265
+ // Only the bot's own symbols, on each venue it used — never an account-wide cancel.
266
+ await cancelBotSymbols(adapters, this._traded, "[ARB-V2]", ctx.log);
267
+ return [];
268
+ }
269
+ /** A live position of `symbol` on one of the venues that the bot does not track (or an unreadable venue): no entry on top of it. */
270
+ async heldUntracked(symbol, venues, cache, ctx) {
271
+ const key = quarantineKey(symbol);
272
+ for (const [v, a] of venues) {
273
+ if (!cache.has(v))
274
+ cache.set(v, a.getPositions());
275
+ let list;
276
+ try {
277
+ list = await cache.get(v);
278
+ }
279
+ catch (e) {
280
+ ctx.log(` [ARB-V2] Skip ${symbol}: ${v} positions unreadable (${e instanceof Error ? e.message : String(e)}) — no entry on unknown exposure`);
281
+ return true;
282
+ }
283
+ if (list.some(p => quarantineKey(p.symbol) === key && Number(p.size) !== 0)) {
284
+ ctx.log(` [ARB-V2] Skip ${symbol}: a ${symbol} position on ${v} is not one of the bot's pairs — no entry on top of it`);
285
+ return true;
286
+ }
287
+ }
288
+ return false;
289
+ }
290
+ quarantine(pos, reason, ctx) {
291
+ quarantineSymbol(this.name, pos.symbol, reason, `Check ${pos.symbol} on ${pos.longExchange} and ${pos.shortExchange} (perp -e ${pos.longExchange} positions; perp -e ${pos.shortExchange} positions); close or re-hedge by hand, then: perp strategy quarantine-clear ${pos.symbol}`, ctx.log);
292
+ }
293
+ /**
294
+ * Long then short, each verified; the short is sized to the long's verified fill and
295
+ * any excess the short did not cover is trimmed off the long (reduce-only, verified).
296
+ * A definite short failure rolls the long back reduce-only. Returns the hedged size
297
+ * (both legs), or null when nothing is open / the symbol was quarantined.
298
+ */
299
+ async openPosition(ctx, opp, size, longAdapter, shortAdapter) {
300
+ const errText = (e) => e instanceof Error ? e.message : String(e);
301
+ const dec = sizeDecimalsOf(size);
302
+ let longFilled;
303
+ try {
304
+ longFilled = (await placePerpVerified(longAdapter, opp.longExchange, opp.symbol, "buy", size)).filledSize;
305
+ }
306
+ catch (err) {
307
+ if (isOutcomeUnknown(err))
308
+ this.quarantine(opp, `long leg ${size} on ${opp.longExchange} outcome unknown (${errText(err)}) — no short placed`, ctx);
309
+ else
310
+ ctx.log(` [ARB-V2] Execution failed (long leg): ${errText(err)}`);
311
+ return null;
312
+ }
313
+ const longOpen = fillSizeString(longFilled, dec);
314
+ let shortFilled;
315
+ try {
316
+ shortFilled = (await placePerpVerified(shortAdapter, opp.shortExchange, opp.symbol, "sell", longOpen)).filledSize;
317
+ }
318
+ catch (err) {
319
+ const msg = errText(err);
320
+ if (isOutcomeUnknown(err)) {
321
+ // The short may exist: closing the long could leave a naked short.
322
+ this.quarantine(opp, `short leg ${longOpen} on ${opp.shortExchange} outcome unknown (${msg}); long ${longOpen} open on ${opp.longExchange} — not rolled back`, ctx);
323
+ return null;
324
+ }
325
+ ctx.log(` [ARB-V2] Execution failed (short leg): ${msg} — rolling back the long (reduce-only)`);
326
+ try {
327
+ const rb = await closePerpVerified(longAdapter, opp.longExchange, opp.symbol, "sell", longOpen, { decimals: dec });
328
+ if (rb.residual > 0) {
329
+ this.quarantine(opp, `short leg failed (${msg}); the long rollback closed ${fillSizeString(rb.closed, dec)} of ${longOpen}, ${fillSizeString(rb.residual, dec)} still open${rb.note !== null ? ` (${rb.note})` : ""} — orphaned long on ${opp.longExchange}`, ctx);
330
+ return null;
198
331
  }
199
- catch { /* best effort */ }
332
+ ctx.log(` [ARB-V2] Rollback OK: closed long ${longOpen} ${opp.symbol} on ${opp.longExchange}`);
333
+ }
334
+ catch (rbErr) {
335
+ this.quarantine(opp, `short leg failed (${msg}) and the long rollback failed or is unconfirmed (${errText(rbErr)}) — orphaned long ${longOpen} on ${opp.longExchange}`, ctx);
336
+ }
337
+ return null;
338
+ }
339
+ const hedged = fillSizeString(shortFilled, dec);
340
+ try {
341
+ const trimmed = await trimFirstLegToHedge(longAdapter, opp.longExchange, opp.symbol, "buy", longFilled, shortFilled, dec);
342
+ if (trimmed !== "0")
343
+ ctx.log(` [ARB-V2] Short filled ${hedged} of ${longOpen} — trimmed long by ${trimmed} (reduce-only)`);
344
+ }
345
+ catch (trimErr) {
346
+ this.quarantine(opp, `short filled ${hedged} of ${longOpen}; trimming the long's excess on ${opp.longExchange} failed or is unconfirmed (${errText(trimErr)}) — long ${longOpen} vs short ${hedged}`, ctx);
347
+ return null;
348
+ }
349
+ return hedged;
350
+ }
351
+ /**
352
+ * Close both legs reduce-only with the opened size, verified. Reduce-only makes a
353
+ * retry safe (a leg already flat is rejected, never re-opened), so a definite failure
354
+ * of the LONG close keeps the (still hedged) position for the next tick; MAX_CLOSE_FAILURES
355
+ * in a row, an unknown outcome, or a short close that definitely failed once the long is
356
+ * closed (naked short) quarantines it. Returns true when both legs are flat.
357
+ */
358
+ async closePosition(ctx, pos, adapters) {
359
+ const longAdapter = adapters.get(pos.longExchange);
360
+ const shortAdapter = adapters.get(pos.shortExchange);
361
+ if (!longAdapter || !shortAdapter) {
362
+ ctx.log(` [ARB-V2] Close ${pos.symbol}: adapter missing (${pos.longExchange}/${pos.shortExchange})`);
363
+ return false;
364
+ }
365
+ const errText = (e) => e instanceof Error ? e.message : String(e);
366
+ const failed = (why) => {
367
+ pos.closeFailures++;
368
+ ctx.log(` [ARB-V2] Close failed for ${pos.symbol}: ${why} (${pos.closeFailures}/${MAX_CLOSE_FAILURES})`);
369
+ if (pos.closeFailures >= MAX_CLOSE_FAILURES)
370
+ this.quarantine(pos, `close failed ${pos.closeFailures} times in a row (last: ${why})`, ctx);
371
+ return false;
372
+ };
373
+ if (!pos.longClosed) {
374
+ try {
375
+ const lc = await closePerpVerified(longAdapter, pos.longExchange, pos.symbol, "sell", pos.size);
376
+ if (lc.residual > 0) {
377
+ this.quarantine(pos, `close of long on ${pos.longExchange} closed ${lc.closed} of ${pos.size}, ${lc.residual} still open${lc.note !== null ? ` (${lc.note})` : ""}; short ${pos.size} on ${pos.shortExchange} not closed`, ctx);
378
+ return false;
379
+ }
380
+ pos.longClosed = true;
381
+ }
382
+ catch (err) {
383
+ const msg = errText(err);
384
+ if (isOutcomeUnknown(err)) {
385
+ this.quarantine(pos, `close of long ${pos.size} on ${pos.longExchange} outcome unknown (${msg})`, ctx);
386
+ return false;
387
+ }
388
+ if (!isPositionGone(err))
389
+ return failed(`long leg: ${msg}`);
390
+ pos.longClosed = true; // already flat
391
+ }
392
+ }
393
+ try {
394
+ const sc = await closePerpVerified(shortAdapter, pos.shortExchange, pos.symbol, "buy", pos.size);
395
+ if (sc.residual > 0) {
396
+ this.quarantine(pos, `the long is closed; close of short on ${pos.shortExchange} closed ${sc.closed} of ${pos.size}, ${sc.residual} still open${sc.note !== null ? ` (${sc.note})` : ""} — residual short unhedged`, ctx);
397
+ return false;
398
+ }
399
+ }
400
+ catch (err) {
401
+ const msg = errText(err);
402
+ if (isOutcomeUnknown(err)) {
403
+ this.quarantine(pos, `close of short ${pos.size} on ${pos.shortExchange} outcome unknown (${msg}); the long is closed`, ctx);
404
+ return false;
405
+ }
406
+ if (!isPositionGone(err)) {
407
+ // The long is closed, so the short is naked: quarantine NOW — never carry a naked
408
+ // short across ticks waiting for MAX_CLOSE_FAILURES.
409
+ this.quarantine(pos, `long closed on ${pos.longExchange}; close of short ${pos.size} on ${pos.shortExchange} failed (${msg}) — naked short`, ctx);
410
+ return false;
200
411
  }
201
412
  }
202
- ctx.state.set("positions", []);
203
- return [{ type: "cancel_all" }];
413
+ ctx.log(` [ARB-V2] Closed ${pos.symbol} (long ${pos.longExchange}, short ${pos.shortExchange})`);
414
+ return true;
204
415
  }
205
416
  getAdapters(ctx) {
206
417
  const adapters = new Map();