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,74 @@
1
+ import { closeSync, existsSync, mkdirSync, openSync, statSync, unlinkSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { PerpError } from "../errors.js";
4
+ /** How long to wait for another process's lock, and when a lock left by a dead process is stale. */
5
+ export const FILE_LOCK_WAIT_MS = 5_000;
6
+ export const FILE_LOCK_STALE_MS = 30_000;
7
+ /** Locks this process holds (path → depth): a nested load-modify-save in the same process never self-deadlocks. */
8
+ const held = new Map();
9
+ /**
10
+ * Run `fn` holding the advisory lock file `<path>.lock` (O_EXCL create) — for a load → modify →
11
+ * save of a state file shared by several perp processes (bots, daemons, CLI), so one process
12
+ * never overwrites another's concurrent write. A lock older than FILE_LOCK_STALE_MS (its holder
13
+ * died) is removed; a live one not released within FILE_LOCK_WAIT_MS → LOCK_HELD (nothing is
14
+ * written on top of an unknown concurrent write).
15
+ */
16
+ export function withFileLock(path, what, fn) {
17
+ const lock = `${path}.lock`;
18
+ const depth = held.get(lock) ?? 0;
19
+ if (depth > 0) {
20
+ held.set(lock, depth + 1);
21
+ try {
22
+ return fn();
23
+ }
24
+ finally {
25
+ held.set(lock, depth);
26
+ }
27
+ }
28
+ const dir = resolve(path, "..");
29
+ if (!existsSync(dir))
30
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
31
+ const deadline = Date.now() + FILE_LOCK_WAIT_MS;
32
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
33
+ for (;;) {
34
+ try {
35
+ closeSync(openSync(lock, "wx", 0o600));
36
+ break;
37
+ }
38
+ catch (e) {
39
+ if (e.code !== "EEXIST")
40
+ throw e;
41
+ let stale = false;
42
+ try {
43
+ stale = Date.now() - statSync(lock).mtimeMs > FILE_LOCK_STALE_MS;
44
+ }
45
+ catch {
46
+ continue; // released between the open and the stat: try again
47
+ }
48
+ if (stale) {
49
+ try {
50
+ unlinkSync(lock);
51
+ }
52
+ catch { /* another process removed it */ }
53
+ continue;
54
+ }
55
+ if (Date.now() > deadline) {
56
+ throw new PerpError("LOCK_HELD", `${what} is locked by another perp process (${lock})`, {
57
+ remediation: `Retry shortly; if no other perp process is running, delete ${lock}`,
58
+ });
59
+ }
60
+ Atomics.wait(sleeper, 0, 0, 10);
61
+ }
62
+ }
63
+ held.set(lock, 1);
64
+ try {
65
+ return fn();
66
+ }
67
+ finally {
68
+ held.delete(lock);
69
+ try {
70
+ unlinkSync(lock);
71
+ }
72
+ catch { /* already gone */ }
73
+ }
74
+ }
@@ -1,7 +1,7 @@
1
- export { computeMatchedSize, computeSpotPerpMatchedSize, reconcileArbFills } from "./sizing.js";
1
+ export { computeMatchedSize, computeSpotPerpMatchedSize } from "./sizing.js";
2
2
  export { SETTLEMENT_SCHEDULES, getLastSettlement, getMinutesSinceSettlement, aggressiveSettleBoost, estimateFundingUntilSettlement, computeBasisRisk, formatNotifyMessage, sendNotification, notifyIfEnabled, } from "./utils.js";
3
3
  export type { SettleStrategy, BasisRisk, ArbNotifyEvent } from "./utils.js";
4
- export { setStateFilePath, resetStateFilePath, loadArbState, saveArbState, addPosition, removePosition, updatePosition, getPositions, createInitialState, } from "./state.js";
4
+ export { setStateFilePath, resetStateFilePath, loadArbState, saveArbState, addPosition, removePosition, updatePosition, removePositionEntry, updatePositionEntry, positionKey, getPositions, createInitialState, } from "./state.js";
5
5
  export type { ArbPositionState, ArbDaemonState } from "./state.js";
6
6
  export { computeEnhancedStats } from "./history-stats.js";
7
7
  export type { ArbTradeForStats, ExchangePairPerf, TimeOfDayPerf, EnhancedHistoryStats } from "./history-stats.js";
package/dist/arb/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // arb/ barrel — public API
2
- export { computeMatchedSize, computeSpotPerpMatchedSize, reconcileArbFills } from "./sizing.js";
2
+ export { computeMatchedSize, computeSpotPerpMatchedSize } from "./sizing.js";
3
3
  export { SETTLEMENT_SCHEDULES, getLastSettlement, getMinutesSinceSettlement, aggressiveSettleBoost, estimateFundingUntilSettlement, computeBasisRisk, formatNotifyMessage, sendNotification, notifyIfEnabled, } from "./utils.js";
4
- export { setStateFilePath, resetStateFilePath, loadArbState, saveArbState, addPosition, removePosition, updatePosition, getPositions, createInitialState, } from "./state.js";
4
+ export { setStateFilePath, resetStateFilePath, loadArbState, saveArbState, addPosition, removePosition, updatePosition, removePositionEntry, updatePositionEntry, positionKey, getPositions, createInitialState, } from "./state.js";
5
5
  export { computeEnhancedStats } from "./history-stats.js";
@@ -0,0 +1,32 @@
1
+ import type { Network } from "../exchanges/venue-types.js";
2
+ /**
3
+ * Pending-entry records of the arb daemons (`arb auto`, `arb auto --hip3`) — PER NETWORK, next to
4
+ * the arb state (`arb-state.json` → `arb-state.pending.json`, testnet → `arb-state.testnet.pending.json`).
5
+ *
6
+ * A record is written BEFORE the first leg of a pair entry and removed once the pair is in the
7
+ * arb state / in memory, or rolled back / quarantined. A record that is still there when a daemon
8
+ * of the same source starts means the previous process died (crash, kill) between two legs: what
9
+ * is open is unknown → the symbol is quarantined (never re-entered, never "flat").
10
+ */
11
+ export interface PendingEntry {
12
+ /** Daemon that wrote it: "arb-auto" | "arb-dex-auto". */
13
+ source: string;
14
+ symbol: string;
15
+ longExchange: string;
16
+ shortExchange: string;
17
+ /** Requested base size per leg. */
18
+ size: string;
19
+ startedAt: string;
20
+ pid: number;
21
+ }
22
+ export declare function pendingEntryFilePath(network: Network): string;
23
+ /** Record an entry about to send its first leg. Throws when it cannot be persisted (the caller must not trade). */
24
+ export declare function writePendingEntry(network: Network, e: Omit<PendingEntry, "startedAt" | "pid">): void;
25
+ /** The entry's outcome is recorded (state / memory / rollback / quarantine): drop its pending record. */
26
+ export declare function clearPendingEntry(network: Network, source: string, symbol: string): void;
27
+ /**
28
+ * Pending records of `source` left by a process that is no longer running (crashed / killed
29
+ * mid-entry). Read-only: the caller quarantines each one, then clears it (clearPendingEntry).
30
+ * Records of a live process (another daemon still entering) are not returned.
31
+ */
32
+ export declare function orphanedPendingEntries(network: Network, source: string): PendingEntry[];
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
2
+ import { PerpError } from "../errors.js";
3
+ import { withFileLock } from "./file-lock.js";
4
+ import { arbStateFilePath } from "./state.js";
5
+ export function pendingEntryFilePath(network) {
6
+ const state = arbStateFilePath(network);
7
+ return state.endsWith(".json") ? `${state.slice(0, -5)}.pending.json` : `${state}.pending`;
8
+ }
9
+ function keyOf(source, symbol) {
10
+ return `${source}:${symbol.toUpperCase()}`;
11
+ }
12
+ /** A missing file = no pending entry; an unreadable / malformed one throws (never "none pending"). */
13
+ function load(path) {
14
+ if (!existsSync(path))
15
+ return { version: 1, entries: {} };
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
19
+ }
20
+ catch (e) {
21
+ throw new PerpError("INVALID_PARAMS", `Arb pending-entry file is unreadable: ${path} (${e instanceof Error ? e.message : String(e)})`, {
22
+ remediation: `Check the symbols it names on the venues (perp arb status), then fix or remove ${path}`,
23
+ });
24
+ }
25
+ const f = parsed;
26
+ if (f === null || f.version !== 1 || typeof f.entries !== "object" || f.entries === null) {
27
+ throw new PerpError("INVALID_PARAMS", `Arb pending-entry file has an unexpected shape: ${path}`, {
28
+ remediation: `Check the symbols it names on the venues (perp arb status), then fix or remove ${path}`,
29
+ });
30
+ }
31
+ return f;
32
+ }
33
+ function save(path, f) {
34
+ const tmp = `${path}.tmp`;
35
+ writeFileSync(tmp, JSON.stringify(f, null, 2), { mode: 0o600 });
36
+ renameSync(tmp, path);
37
+ }
38
+ function modify(network, fn) {
39
+ const path = pendingEntryFilePath(network);
40
+ return withFileLock(path, `Arb pending-entry file (${network})`, () => {
41
+ const f = load(path);
42
+ const out = fn(f);
43
+ save(path, f);
44
+ return out;
45
+ });
46
+ }
47
+ /** Record an entry about to send its first leg. Throws when it cannot be persisted (the caller must not trade). */
48
+ export function writePendingEntry(network, e) {
49
+ modify(network, (f) => {
50
+ f.entries[keyOf(e.source, e.symbol)] = { ...e, startedAt: new Date().toISOString(), pid: process.pid };
51
+ });
52
+ }
53
+ /** The entry's outcome is recorded (state / memory / rollback / quarantine): drop its pending record. */
54
+ export function clearPendingEntry(network, source, symbol) {
55
+ modify(network, (f) => { delete f.entries[keyOf(source, symbol)]; });
56
+ }
57
+ function alive(pid) {
58
+ if (pid === process.pid)
59
+ return false;
60
+ try {
61
+ process.kill(pid, 0);
62
+ return true;
63
+ }
64
+ catch (e) {
65
+ // EPERM: the process exists (another user) — treat as alive.
66
+ return e.code === "EPERM";
67
+ }
68
+ }
69
+ /**
70
+ * Pending records of `source` left by a process that is no longer running (crashed / killed
71
+ * mid-entry). Read-only: the caller quarantines each one, then clears it (clearPendingEntry).
72
+ * Records of a live process (another daemon still entering) are not returned.
73
+ */
74
+ export function orphanedPendingEntries(network, source) {
75
+ const f = load(pendingEntryFilePath(network));
76
+ return Object.values(f.entries).filter(e => e.source === source && !alive(e.pid));
77
+ }
@@ -1,4 +1,3 @@
1
- import type { ExchangeAdapter } from "../exchanges/index.js";
2
1
  /**
3
2
  * Calculate the exact same size for both legs of an arb position,
4
3
  * respecting both exchanges' minimum order and size precision constraints.
@@ -9,6 +8,12 @@ export declare function computeMatchedSize(sizeUsd: number, price: number, longE
9
8
  longSizeDecimals?: number;
10
9
  shortSizeDecimals?: number;
11
10
  lotSize?: number;
11
+ /**
12
+ * The pair's minimum order value in USD from the venues' published market info (the larger of
13
+ * the two legs; 0 = neither venue publishes one). When given it is the ONLY minimum applied —
14
+ * the per-venue table below is for callers that have no market info at hand.
15
+ */
16
+ minNotional?: number;
12
17
  }): {
13
18
  size: string;
14
19
  notional: number;
@@ -21,15 +26,3 @@ export declare function computeSpotPerpMatchedSize(sizeUsd: number, price: numbe
21
26
  size: string;
22
27
  notional: number;
23
28
  } | null;
24
- /**
25
- * After both legs are submitted, verify actual fills match.
26
- * If there's a size mismatch, place a correction order on the larger side.
27
- *
28
- * @returns the corrected size, or null if already matched
29
- */
30
- export declare function reconcileArbFills(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, log?: (msg: string) => void): Promise<{
31
- matched: boolean;
32
- longSize: number;
33
- shortSize: number;
34
- correction?: string;
35
- }>;
@@ -24,7 +24,9 @@ export function computeMatchedSize(sizeUsd, price, longExchange, shortExchange,
24
24
  return null;
25
25
  // Verify notional meets minimum for both exchanges
26
26
  const notional = roundedSize * price;
27
- const minNotional = Math.max(getMinNotional(longExchange), getMinNotional(shortExchange));
27
+ const minNotional = opts?.minNotional !== undefined
28
+ ? opts.minNotional
29
+ : Math.max(getMinNotional(longExchange), getMinNotional(shortExchange));
28
30
  if (notional < minNotional) {
29
31
  // Try rounding UP instead
30
32
  let roundedUp;
@@ -36,11 +38,14 @@ export function computeMatchedSize(sizeUsd, price, longExchange, shortExchange,
36
38
  roundedUp = Math.ceil(rawSize * factor) / factor;
37
39
  }
38
40
  const notionalUp = roundedUp * price;
39
- // Only round up if the increase is small (< 20% over requested)
40
- if (notionalUp <= sizeUsd * 1.2) {
41
+ // Round up only if it both (a) clears the venue minimum and (b) does not
42
+ // overshoot the requested size by more than 20%. Pre-fix this checked only
43
+ // (b), so a ceil that still fell short of minNotional was returned and then
44
+ // rejected by the venue — SSOT Rule #2: fail honestly with null instead.
45
+ if (notionalUp >= minNotional && notionalUp <= sizeUsd * 1.2) {
41
46
  return { size: formatSize(roundedUp, szDecimals, opts?.lotSize), notional: notionalUp };
42
47
  }
43
- return null; // Can't meet minimum
48
+ return null; // Can't meet minimum within tolerance
44
49
  }
45
50
  return { size: formatSize(roundedSize, szDecimals, opts?.lotSize), notional };
46
51
  }
@@ -94,56 +99,12 @@ export function computeSpotPerpMatchedSize(sizeUsd, price, spotExchange, perpExc
94
99
  if (notional < minNotional) {
95
100
  const roundedUp = Math.ceil(rawSize * factor) / factor;
96
101
  const notionalUp = roundedUp * price;
97
- if (notionalUp <= sizeUsd * 1.2) {
102
+ // Same minNotional re-check as computeMatchedSize (SSOT Rule #2): a ceil
103
+ // that is still under the venue minimum must fail, not be returned.
104
+ if (notionalUp >= minNotional && notionalUp <= sizeUsd * 1.2) {
98
105
  return { size: roundedUp.toFixed(szDecimals), notional: notionalUp };
99
106
  }
100
107
  return null;
101
108
  }
102
109
  return { size: roundedSize.toFixed(szDecimals), notional };
103
110
  }
104
- /**
105
- * After both legs are submitted, verify actual fills match.
106
- * If there's a size mismatch, place a correction order on the larger side.
107
- *
108
- * @returns the corrected size, or null if already matched
109
- */
110
- export async function reconcileArbFills(longAdapter, shortAdapter, symbol, log) {
111
- const [longPositions, shortPositions] = await Promise.all([
112
- longAdapter.getPositions(),
113
- shortAdapter.getPositions(),
114
- ]);
115
- const longPos = longPositions.find(p => p.symbol.replace("-PERP", "").toUpperCase() === symbol.toUpperCase() && p.side === "long");
116
- const shortPos = shortPositions.find(p => p.symbol.replace("-PERP", "").toUpperCase() === symbol.toUpperCase() && p.side === "short");
117
- const longSize = longPos ? Math.abs(Number(longPos.size)) : 0;
118
- const shortSize = shortPos ? Math.abs(Number(shortPos.size)) : 0;
119
- if (longSize === 0 && shortSize === 0) {
120
- return { matched: true, longSize: 0, shortSize: 0 };
121
- }
122
- const diff = Math.abs(longSize - shortSize);
123
- const maxSize = Math.max(longSize, shortSize);
124
- const diffPct = maxSize > 0 ? (diff / maxSize) * 100 : 0;
125
- // Allow 1% tolerance (rounding differences)
126
- if (diffPct <= 1) {
127
- return { matched: true, longSize, shortSize };
128
- }
129
- log?.(`[ARB] Size mismatch: long ${longSize} vs short ${shortSize} (diff: ${diff.toFixed(4)}, ${diffPct.toFixed(1)}%)`);
130
- // Correct the larger side by reducing it
131
- const correctionSize = diff.toFixed(6);
132
- try {
133
- if (longSize > shortSize) {
134
- log?.(`[ARB] Correcting: sell ${correctionSize} ${symbol} on ${longAdapter.name} to match`);
135
- await longAdapter.marketOrder(symbol, "sell", correctionSize);
136
- }
137
- else {
138
- log?.(`[ARB] Correcting: buy ${correctionSize} ${symbol} on ${shortAdapter.name} to match`);
139
- await shortAdapter.marketOrder(symbol, "buy", correctionSize);
140
- }
141
- log?.(`[ARB] Sizes reconciled`);
142
- return { matched: true, longSize: Math.min(longSize, shortSize), shortSize: Math.min(longSize, shortSize), correction: correctionSize };
143
- }
144
- catch (err) {
145
- const msg = err instanceof Error ? err.message : String(err);
146
- log?.(`[ARB] Correction failed: ${msg}`);
147
- return { matched: false, longSize, shortSize };
148
- }
149
- }
@@ -1,5 +1,8 @@
1
+ import type { Network } from "../exchanges/venue-types.js";
1
2
  export declare function setStateFilePath(path: string): void;
2
3
  export declare function resetStateFilePath(): void;
4
+ /** Absolute state-file path for `network` (mainnet = the pre-network path). */
5
+ export declare function arbStateFilePath(network: Network): string;
3
6
  export interface ArbPositionState {
4
7
  id: string;
5
8
  symbol: string;
@@ -9,8 +12,9 @@ export interface ArbPositionState {
9
12
  shortSize: number;
10
13
  entryTime: string;
11
14
  entrySpread: number;
12
- entryLongPrice: number;
13
- entryShortPrice: number;
15
+ /** null = no verified fill price was reported for the leg (a strategy-bot entry; never a guess). */
16
+ entryLongPrice: number | null;
17
+ entryShortPrice: number | null;
14
18
  accumulatedFunding: number;
15
19
  lastCheckTime: string;
16
20
  /** "perp-perp" (default) or "spot-perp" */
@@ -22,6 +26,8 @@ export interface ArbPositionState {
22
26
  }
23
27
  export interface ArbDaemonState {
24
28
  version: 1;
29
+ /** Network the positions live on. Absent only in files written before per-network state (= mainnet). */
30
+ network?: Network;
25
31
  lastStartTime: string;
26
32
  lastScanTime: string;
27
33
  lastSuccessfulScanTime: string;
@@ -37,21 +43,48 @@ export interface ArbDaemonState {
37
43
  notifyUrl?: string;
38
44
  };
39
45
  }
40
- /** Load daemon state from disk. Falls back to .tmp if main file is corrupted. */
41
- export declare function loadArbState(): ArbDaemonState | null;
46
+ /** Load the daemon state for `network`. Falls back to .tmp if main file is corrupted. */
47
+ export declare function loadArbState(network: Network, opts?: {
48
+ readOnly: boolean;
49
+ }): ArbDaemonState | null;
42
50
  /**
43
51
  * Save daemon state to disk using atomic write pattern.
44
52
  * Writes to .tmp first, then renames to main file.
45
53
  * If crash occurs mid-write, .tmp is partial but main file is intact.
46
54
  */
47
- export declare function saveArbState(state: ArbDaemonState): void;
48
- /** Add a position to the persisted state. Auto-initializes state if missing. */
49
- export declare function addPosition(pos: ArbPositionState): void;
50
- /** Remove a position by symbol from the persisted state. */
51
- export declare function removePosition(symbol: string): void;
55
+ export declare function saveArbState(network: Network, state: ArbDaemonState): void;
56
+ /** Identity of a state entry: symbol + long venue + short venue + mode (absent mode = perp-perp). */
57
+ export declare function positionKey(p: Pick<ArbPositionState, "symbol" | "longExchange" | "shortExchange" | "mode">): string;
58
+ /**
59
+ * Put `pos` into `positions`: an entry with the same key is replaced (a re-record of the same
60
+ * pair); an entry of the same symbol on OTHER venues / another mode is never overwritten —
61
+ * RISK_VIOLATION (the state tracks one pair per symbol; overwriting would drop a live pair).
62
+ */
63
+ export declare function upsertPosition(positions: ArbPositionState[], pos: ArbPositionState): ArbPositionState[];
64
+ /** Add a position to the persisted state for `network`. Auto-initializes state if missing. Throws RISK_VIOLATION for a conflicting entry (upsertPosition). */
65
+ export declare function addPosition(network: Network, pos: ArbPositionState): void;
66
+ /** Remove a position by symbol from the persisted state for `network` (one entry per symbol: upsertPosition). */
67
+ export declare function removePosition(network: Network, symbol: string): void;
68
+ /** Remove exactly the entry with `entry`'s key (symbol + venues + mode); another pair of the symbol is left alone. */
69
+ export declare function removePositionEntry(network: Network, entry: Pick<ArbPositionState, "symbol" | "longExchange" | "shortExchange" | "mode">): void;
52
70
  /** Update a position by symbol with partial updates. */
53
- export declare function updatePosition(symbol: string, updates: Partial<ArbPositionState>): void;
54
- /** Get all persisted positions. */
55
- export declare function getPositions(): ArbPositionState[];
71
+ export declare function updatePosition(network: Network, symbol: string, updates: Partial<ArbPositionState>): void;
72
+ /** Update exactly the entry with `entry`'s key (symbol + venues + mode). */
73
+ export declare function updatePositionEntry(network: Network, entry: Pick<ArbPositionState, "symbol" | "longExchange" | "shortExchange" | "mode">, updates: Partial<ArbPositionState>): void;
74
+ /** Get all persisted positions for `network`. */
75
+ export declare function getPositions(network: Network): ArbPositionState[];
56
76
  /** Create a default empty daemon state with the given config. */
57
77
  export declare function createInitialState(config: ArbDaemonState["config"]): ArbDaemonState;
78
+ /** State access for the `arb auto` daemon (see createDaemonStateStore). */
79
+ export interface DaemonStateStore {
80
+ load(): ArbDaemonState | null;
81
+ save(next: ArbDaemonState): void;
82
+ add(pos: ArbPositionState): void;
83
+ remove(symbol: string): void;
84
+ }
85
+ /**
86
+ * Live: the per-network state file. Dry-run (review N3): an in-memory copy seeded READ-ONLY
87
+ * from the real state — nothing is ever written to disk, so a simulated entry is never picked
88
+ * up by a live run and a simulated close never drops a real position from tracking.
89
+ */
90
+ export declare function createDaemonStateStore(network: Network, dryRun: boolean, config: ArbDaemonState["config"]): DaemonStateStore;