perp-cli 0.12.17 → 0.14.1

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 (363) 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 +303 -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 +248 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +925 -245
  103. package/dist/commands/history.js +102 -54
  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 +357 -459
  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 +240 -6
  166. package/dist/exchanges/aster.js +1139 -195
  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 +152 -0
  200. package/dist/exchanges/ondo/adapter.js +811 -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 +95 -0
  206. package/dist/exchanges/ondo/funds.js +198 -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 +293 -4
  218. package/dist/exchanges/pacifica.js +1044 -90
  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 +1386 -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 +29 -0
  270. package/dist/exchanges/venues/ondo.js +136 -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 +39 -0
  274. package/dist/exchanges/venues/qfex.js +140 -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 +821 -403
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/onchain-balances.d.ts +40 -0
  293. package/dist/onchain-balances.js +173 -0
  294. package/dist/pacifica/client.d.ts +41 -10
  295. package/dist/pacifica/client.js +167 -63
  296. package/dist/pacifica/constants.d.ts +0 -1
  297. package/dist/pacifica/constants.js +0 -5
  298. package/dist/pacifica/deposit.d.ts +30 -3
  299. package/dist/pacifica/deposit.js +119 -2
  300. package/dist/pacifica/faucet.d.ts +29 -0
  301. package/dist/pacifica/faucet.js +100 -0
  302. package/dist/pacifica/sign-tx.d.ts +15 -0
  303. package/dist/pacifica/sign-tx.js +41 -0
  304. package/dist/pacifica/types/account.d.ts +28 -4
  305. package/dist/pacifica/types/lake.d.ts +20 -5
  306. package/dist/pacifica/types/lake.js +4 -0
  307. package/dist/pacifica/types/market.d.ts +5 -0
  308. package/dist/pacifica/types/order.d.ts +4 -1
  309. package/dist/pacifica/types/position.d.ts +9 -1
  310. package/dist/plan-executor.d.ts +10 -5
  311. package/dist/plan-executor.js +165 -54
  312. package/dist/position-history.js +13 -0
  313. package/dist/rebalance.d.ts +26 -2
  314. package/dist/rebalance.js +86 -7
  315. package/dist/relay-swap.d.ts +232 -0
  316. package/dist/relay-swap.js +630 -0
  317. package/dist/risk.js +5 -1
  318. package/dist/secret-env.d.ts +15 -0
  319. package/dist/secret-env.js +36 -0
  320. package/dist/settings.d.ts +169 -6
  321. package/dist/settings.js +219 -38
  322. package/dist/signer/ows-evm.d.ts +13 -0
  323. package/dist/signer/ows-evm.js +22 -0
  324. package/dist/smart-order.d.ts +9 -0
  325. package/dist/smart-order.js +53 -8
  326. package/dist/strategies/dca.d.ts +6 -2
  327. package/dist/strategies/dca.js +66 -26
  328. package/dist/strategies/funding-arb.d.ts +11 -1
  329. package/dist/strategies/funding-arb.js +202 -89
  330. package/dist/strategies/grid.d.ts +31 -0
  331. package/dist/strategies/grid.js +89 -11
  332. package/dist/strategies/pending-stop.d.ts +8 -0
  333. package/dist/strategies/pending-stop.js +12 -0
  334. package/dist/strategies/split-order.d.ts +45 -17
  335. package/dist/strategies/split-order.js +157 -91
  336. package/dist/strategies/trailing-stop.d.ts +2 -1
  337. package/dist/strategies/trailing-stop.js +52 -6
  338. package/dist/strategies/twap.d.ts +6 -1
  339. package/dist/strategies/twap.js +39 -11
  340. package/dist/trade-validator.d.ts +22 -0
  341. package/dist/trade-validator.js +119 -15
  342. package/dist/utils/numeric.d.ts +46 -0
  343. package/dist/utils/numeric.js +79 -0
  344. package/dist/utils.d.ts +19 -0
  345. package/dist/utils.js +96 -19
  346. package/dist/withdraw-gate.d.ts +49 -0
  347. package/dist/withdraw-gate.js +131 -0
  348. package/package.json +15 -6
  349. package/skills/perp-cli/SKILL.md +159 -55
  350. package/skills/perp-cli/references/agent-operations.md +112 -58
  351. package/skills/perp-cli/references/commands.md +269 -73
  352. package/skills/perp-cli/references/strategies.md +34 -31
  353. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  354. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  355. package/skills/perp-cli/scripts/preflight.sh +21 -14
  356. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  357. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  358. package/vendor/lighter-signer/SHA256SUMS +2 -0
  359. package/vendor/lighter-signer/VERSION +8 -0
  360. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  361. package/vendor/lighter-signer/wasm_exec.js +575 -0
  362. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  363. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -1,5 +1,14 @@
1
1
  import { registerStrategy } from "../strategy-registry.js";
2
- import { toHourlyRate, computeAnnualSpread, annualizeHourlyRate } from "../../funding/normalize.js";
2
+ import { toHourlyRate, annualizeHourlyRate } from "../../funding/normalize.js";
3
+ import { isQuarantined, quarantineSymbol } from "../quarantine.js";
4
+ import { closePerpVerified, isOutcomeUnknown, placePerpVerified, trimFirstLegToHedge } from "../perp-order.js";
5
+ import { fillSizeString, sizeDecimalsOf } from "../../exchanges/spot-result.js";
6
+ import { PerpError } from "../../errors.js";
7
+ import { assertFundingArbLegs } from "./funding-arb-utils.js";
8
+ /** Market basis first (ExchangeMarketInfo.fundingHours), else the venue definition's basis (§10.1). */
9
+ function hourlyOf(info, exchange) {
10
+ return info.fundingHours !== null ? info.rate / info.fundingHours : toHourlyRate(info.rate, exchange);
11
+ }
3
12
  /**
4
13
  * Minimum notional per exchange (USD):
5
14
  * - HL: ~$2 (0.001 ETH)
@@ -8,19 +17,14 @@ import { toHourlyRate, computeAnnualSpread, annualizeHourlyRate } from "../../fu
8
17
  * Use $12 as safe minimum across all exchanges.
9
18
  */
10
19
  const MIN_NOTIONAL_USD = 12;
11
- /** Size precision per asset price range */
20
+ /** Entry size precision per asset price range, FLOORED (never rounds up past the notional). */
12
21
  function formatSizeForPrice(rawSize, price) {
13
- // Higher price → fewer decimals (BTC: 4, ETH: 3, SOL: 1, small coins: 0)
14
- if (price > 10000)
15
- return rawSize.toFixed(4); // BTC
16
- if (price > 1000)
17
- return rawSize.toFixed(3); // ETH
18
- if (price > 10)
19
- return rawSize.toFixed(2); // SOL, AVAX
20
- if (price > 1)
21
- return rawSize.toFixed(1); // DOGE, etc
22
- return rawSize.toFixed(0); // sub-$1 tokens
22
+ // Higher price → fewer decimals (BTC: 4, ETH: 3, SOL: 2, DOGE: 1, sub-$1: 0)
23
+ const decimals = price > 10000 ? 4 : price > 1000 ? 3 : price > 10 ? 2 : price > 1 ? 1 : 0;
24
+ return fillSizeString(rawSize, decimals);
23
25
  }
26
+ /** Consecutive definite close failures before a pair is quarantined. */
27
+ const MAX_CLOSE_FAILURES = 3;
24
28
  /** Normalize exchange symbol to base asset: "ETH-PERP" → "ETH", "ETHUSDT" → "ETH" */
25
29
  function normalizeSymbol(raw) {
26
30
  return raw.toUpperCase().replace(/-PERP$/, "").replace(/USDT$/, "").replace(/USD$/, "").replace(/-USD$/, "");
@@ -41,11 +45,9 @@ class FundingAutoStrategy {
41
45
  name = "funding-auto";
42
46
  _config = {};
43
47
  // Config accessors
44
- get spotPerpRatio() { return Number(this._config.spotPerpRatio ?? 0.7); }
45
48
  get perpPerpRatio() { return Number(this._config.perpPerpRatio ?? 0.3); }
46
49
  get perpPerpMinSpread() { return Number(this._config.perpPerpMinSpread ?? 20); }
47
- get spotPerpMinSpread() { return Number(this._config.spotPerpMinSpread ?? 5); }
48
- get perpPerpCloseSpread() { return Number(this._config.perpPerpCloseSpread ?? 5); }
50
+ get perpPerpCloseSpread() { return Number(this._config.perpPerpCloseSpread ?? 0); }
49
51
  get maxBreakEvenHours() { return Number(this._config.maxBreakEvenHours ?? 48); }
50
52
  get maxPositions() { return Number(this._config.maxPositions ?? 5); }
51
53
  get maxCapitalPct() { return Number(this._config.maxCapitalPct ?? 20); }
@@ -54,8 +56,6 @@ class FundingAutoStrategy {
54
56
  get scanIntervalTicks() { return Number(this._config.scanIntervalTicks ?? 1); }
55
57
  /** Position size: min of sizeUsd (if set) and maxCapitalPct% of total equity */
56
58
  _cachedEquity = 0;
57
- /** Symbols managed by this strategy instance (persists across ticks, prevents adopting unrelated positions) */
58
- _managedSymbols = new Set();
59
59
  getSizeUsd() {
60
60
  const fixedSize = Number(this._config.sizeUsd ?? 0);
61
61
  const capitalLimit = this._cachedEquity * (this.maxCapitalPct / 100);
@@ -67,12 +67,11 @@ class FundingAutoStrategy {
67
67
  }
68
68
  describe() {
69
69
  return {
70
- description: "Automated funding rate trading - perp-perp + spot-perp dual mode",
70
+ description: "Automated funding rate trading - perp-perp (spot-perp mode not supported; use spot-perp-arb)",
71
71
  params: [
72
- { name: "spotPerpRatio", type: "number", required: false, default: 0.7, description: "Capital ratio for spot-perp mode" },
72
+ { name: "spotPerpRatio", type: "number", required: false, default: 0, description: "Spot-perp mode is NOT supported here (a value > 0 fails with CAPABILITY_UNSUPPORTED) — use spot-perp-arb" },
73
73
  { name: "perpPerpRatio", type: "number", required: false, default: 0.3, description: "Capital ratio for perp-perp mode" },
74
74
  { name: "perpPerpMinSpread", type: "number", required: false, default: 20, description: "Min annualized spread % for perp-perp entry" },
75
- { name: "spotPerpMinSpread", type: "number", required: false, default: 5, description: "Min annualized spread % for spot-perp entry" },
76
75
  { name: "perpPerpCloseSpread", type: "number", required: false, default: 0, description: "Close perp-perp when spread reverses below this % (0 = only on reversal)" },
77
76
  { name: "maxBreakEvenHours", type: "number", required: false, default: 48, description: "Max break-even hours to enter" },
78
77
  { name: "maxPositions", type: "number", required: false, default: 5, description: "Max concurrent positions" },
@@ -85,10 +84,16 @@ class FundingAutoStrategy {
85
84
  };
86
85
  }
87
86
  async init(ctx, _snapshot) {
87
+ assertFundingArbLegs(ctx);
88
88
  this._config = ctx.config;
89
+ if (ctx.config.spotPerpRatio !== undefined && Number(ctx.config.spotPerpRatio) > 0) {
90
+ throw new PerpError("CAPABILITY_UNSUPPORTED", "funding-auto does not support spot-perp mode (no spot leg can be held or unwound by this strategy)", {
91
+ remediation: "Remove spotPerpRatio (or set it to 0) to run perp-perp only, or use the spot-perp-arb strategy for spot + perp short",
92
+ });
93
+ }
89
94
  ctx.state.set("lastScan", 0);
90
95
  ctx.state.set("priceCache", new Map());
91
- ctx.log(`[funding-auto] Starting with ${this.spotPerpRatio}/${this.perpPerpRatio} spot-perp/perp-perp split`);
96
+ ctx.log(`[funding-auto] Starting (perp-perp only; spot-perp mode is not supported by this strategy)`);
92
97
  }
93
98
  async onTick(ctx, _snapshot) {
94
99
  // Cache total equity across ALL exchanges for capital-based sizing
@@ -112,7 +117,9 @@ class FundingAutoStrategy {
112
117
  return [];
113
118
  }
114
119
  // ── Discover existing arb pairs from live exchange positions ──
115
- const positions = await this.discoverArbPairs(adapters);
120
+ // A venue whose positions could not be read is never treated as "no positions":
121
+ // no entries touch it this tick (its existing pairs are simply not visible).
122
+ const { pairs: positions, unreadable } = await this.discoverArbPairs(adapters, ctx.log);
116
123
  // Fetch funding rates from all exchanges (price data cached for position sizing)
117
124
  const ratesByExchange = new Map();
118
125
  const priceCache = new Map(); // "exchange:symbol" -> price
@@ -128,6 +135,7 @@ class FundingAutoStrategy {
128
135
  rate: m.fundingRate != null ? parseFloat(m.fundingRate) : NaN,
129
136
  price,
130
137
  exchange: name,
138
+ fundingHours: m.fundingHours,
131
139
  });
132
140
  if (price > 0) {
133
141
  priceCache.set(`${name}:${sym}`, price);
@@ -153,6 +161,11 @@ class FundingAutoStrategy {
153
161
  // ── Phase 1: Monitor existing positions for exit ──
154
162
  const surviving = [];
155
163
  for (const pos of positions) {
164
+ if (isQuarantined(pos.symbol, ctx.log)) {
165
+ // Unknown / unhedged outcome recorded by some bot or arb path: hands off (design §12.3).
166
+ surviving.push(pos);
167
+ continue;
168
+ }
156
169
  if (pos.mode === "perp-perp") {
157
170
  // Check if spread has converged below close threshold
158
171
  const longRates = ratesByExchange.get(pos.longExchange);
@@ -167,12 +180,21 @@ class FundingAutoStrategy {
167
180
  surviving.push(pos);
168
181
  continue;
169
182
  }
170
- const currentSpread = computeAnnualSpread(shortInfo.rate, pos.shortExchange, longInfo.rate, pos.longExchange);
171
- // Calculate real P&L from exchange history
183
+ // SIGNED spread for the held direction: short-venue hourly − long-venue hourly
184
+ // (each normalized by its own funding basis), annualized %. Positive = still
185
+ // earning; ≤ 0 = the pair has reversed and now pays.
186
+ const currentSpread = annualizeHourlyRate(hourlyOf(shortInfo, pos.shortExchange) - hourlyOf(longInfo, pos.longExchange));
187
+ // Calculate real P&L from exchange history. An unreadable history is never "0":
188
+ // no close decision for this pair this tick.
172
189
  const [longPnl, shortPnl] = await Promise.all([
173
- this.calculateRealPnl(adapters.get(pos.longExchange), pos.symbol),
174
- this.calculateRealPnl(adapters.get(pos.shortExchange), pos.symbol),
190
+ this.calculateRealPnl(adapters.get(pos.longExchange), pos.longExchange, pos.symbol, ctx.log),
191
+ this.calculateRealPnl(adapters.get(pos.shortExchange), pos.shortExchange, pos.symbol, ctx.log),
175
192
  ]);
193
+ if (longPnl === null || shortPnl === null) {
194
+ ctx.log(`[funding-auto] ${pos.symbol}: P&L history unreadable — skipping the exit decision this tick`);
195
+ surviving.push(pos);
196
+ continue;
197
+ }
176
198
  const totalFees = longPnl.fees + shortPnl.fees;
177
199
  const totalFunding = longPnl.funding + shortPnl.funding;
178
200
  pos.accumulatedFunding = totalFunding;
@@ -180,45 +202,14 @@ class FundingAutoStrategy {
180
202
  ctx.log(`[funding-auto] ${pos.symbol} spread ${currentSpread.toFixed(1)}% | ` +
181
203
  `fees $${totalFees.toFixed(4)} | funding $${totalFunding.toFixed(4)} | ` +
182
204
  `net $${(totalFunding - totalFees).toFixed(4)} (${pos.longExchange}↔${pos.shortExchange})`);
183
- // Only close when spread REVERSES (≤ 0%) — never close a profitable position
184
- // A spread of 5% or even 2% still earns funding. Closing costs more than keeping.
185
- if (currentSpread <= 0) {
186
- ctx.log(`[funding-auto] Closing perp-perp ${pos.symbol}: spread REVERSED to ${currentSpread.toFixed(1)}%`);
187
- await this.closePosition(ctx, pos, adapters);
188
- }
189
- else {
190
- surviving.push(pos);
191
- }
192
- }
193
- else if (pos.mode === "spot-perp") {
194
- // Spot-perp: exit if perp funding flips negative (we are short perp)
195
- const shortRates = ratesByExchange.get(pos.shortExchange);
196
- if (!shortRates) {
197
- surviving.push(pos);
198
- continue;
199
- }
200
- const shortInfo = shortRates.get(pos.symbol);
201
- if (!shortInfo) {
202
- surviving.push(pos);
203
- continue;
204
- }
205
- const hourlyRate = toHourlyRate(shortInfo.rate, pos.shortExchange);
206
- const annualized = annualizeHourlyRate(Math.abs(hourlyRate));
207
- const pnl = await this.calculateRealPnl(adapters.get(pos.shortExchange), pos.symbol);
208
- pos.accumulatedFunding = pnl.funding;
209
- pos.totalCost = pnl.fees;
210
- ctx.log(`[funding-auto] ${pos.symbol} rate ${annualized.toFixed(1)}% | ` +
211
- `fees $${pnl.fees.toFixed(4)} | funding $${pnl.funding.toFixed(4)} | ` +
212
- `net $${(pnl.funding - pnl.fees).toFixed(4)} (${pos.shortExchange})`);
213
- // Exit if funding flipped (rate went negative — shorts would pay instead of receive)
214
- if (shortInfo.rate < 0) {
215
- ctx.log(`[funding-auto] Closing spot-perp ${pos.symbol}: funding flipped negative (${(shortInfo.rate * 100).toFixed(4)}%)`);
216
- await this.closePosition(ctx, pos, adapters);
205
+ // Close when the signed spread falls to the configured threshold (default 0 =
206
+ // only on reversal). A small positive spread still earns funding; closing costs more.
207
+ if (currentSpread <= this.perpPerpCloseSpread) {
208
+ ctx.log(`[funding-auto] Closing perp-perp ${pos.symbol}: spread ${currentSpread.toFixed(1)}% <= close threshold ${this.perpPerpCloseSpread}%${currentSpread <= 0 ? " (REVERSED)" : ""}`);
209
+ if (!(await this.closePosition(ctx, pos, adapters)))
210
+ surviving.push(pos);
217
211
  }
218
212
  else {
219
- if (annualized < this.spotPerpMinSpread) {
220
- ctx.log(`[funding-auto] Spot-perp ${pos.symbol} annualized rate dropped to ${annualized.toFixed(1)}% — monitoring`);
221
- }
222
213
  surviving.push(pos);
223
214
  }
224
215
  }
@@ -253,7 +244,7 @@ class FundingAutoStrategy {
253
244
  const info = ratesByExchange.get(exName)?.get(sym);
254
245
  if (!info)
255
246
  continue;
256
- const hourly = toHourlyRate(info.rate, exName);
247
+ const hourly = hourlyOf(info, exName);
257
248
  if (hourly < minRate) {
258
249
  minRate = hourly;
259
250
  minExchange = exName;
@@ -268,7 +259,7 @@ class FundingAutoStrategy {
268
259
  // Need raw rates for computeAnnualSpread
269
260
  const longInfo = ratesByExchange.get(minExchange).get(sym);
270
261
  const shortInfo = ratesByExchange.get(maxExchange).get(sym);
271
- const annualSpread = computeAnnualSpread(shortInfo.rate, maxExchange, longInfo.rate, minExchange);
262
+ const annualSpread = Math.abs(hourlyOf(shortInfo, maxExchange) - hourlyOf(longInfo, minExchange)) * 24 * 365 * 100;
272
263
  if (annualSpread < this.perpPerpMinSpread)
273
264
  continue;
274
265
  // Calculate cost and break-even
@@ -293,9 +284,7 @@ class FundingAutoStrategy {
293
284
  });
294
285
  }
295
286
  // ── Spot-Perp opportunities ──
296
- // DISABLED: spot-perp requires SpotAdapter to buy spot (not yet integrated).
297
- // Opening perp short without spot buy creates naked short = directional risk.
298
- // TODO: Enable when SpotAdapter is wired into the strategy context.
287
+ // Not supported in this strategy (CAPABILITY_UNSUPPORTED at init) — see the header.
299
288
  // Sort by break-even hours ascending (best opportunities first)
300
289
  opportunities.sort((a, b) => a.breakEvenHours - b.breakEvenHours);
301
290
  if (opportunities.length > 0) {
@@ -305,22 +294,22 @@ class FundingAutoStrategy {
305
294
  }
306
295
  }
307
296
  else {
308
- ctx.log(`[funding-auto] No opportunities found (perp-perp min ${this.perpPerpMinSpread}%, spot-perp min ${this.spotPerpMinSpread}%, BE max ${this.maxBreakEvenHours}h)`);
297
+ ctx.log(`[funding-auto] No opportunities found (perp-perp min ${this.perpPerpMinSpread}%, BE max ${this.maxBreakEvenHours}h)`);
309
298
  }
310
299
  // ── Phase 3: Enter new positions ──
311
300
  const slotsAvailable = this.maxPositions - surviving.length;
312
301
  // Allocate slots by ratio
313
302
  const perpPerpSlots = Math.max(1, Math.floor(slotsAvailable * this.perpPerpRatio));
314
- const spotPerpSlots = Math.max(1, slotsAvailable - perpPerpSlots);
315
303
  let perpPerpOpened = 0;
316
- let spotPerpOpened = 0;
317
304
  let currentCount = surviving.length;
318
305
  for (const opp of opportunities) {
319
306
  if (currentCount >= this.maxPositions)
320
307
  break;
321
- if (opp.mode === "perp-perp" && perpPerpOpened >= perpPerpSlots)
308
+ if (perpPerpOpened >= perpPerpSlots)
322
309
  continue;
323
- if (opp.mode === "spot-perp" && spotPerpOpened >= spotPerpSlots)
310
+ if (unreadable.has(opp.longExchange) || unreadable.has(opp.shortExchange))
311
+ continue;
312
+ if (isQuarantined(opp.symbol, ctx.log))
324
313
  continue;
325
314
  // Track attempted symbols to prevent re-entry on same tick cycle
326
315
  attempted.add(`${opp.symbol}:${opp.mode}`);
@@ -328,11 +317,7 @@ class FundingAutoStrategy {
328
317
  const success = await this.openPosition(ctx, opp, adapters);
329
318
  if (success) {
330
319
  currentCount++;
331
- this._managedSymbols.add(`${opp.symbol}:${opp.mode}`);
332
- if (opp.mode === "perp-perp")
333
- perpPerpOpened++;
334
- else
335
- spotPerpOpened++;
320
+ perpPerpOpened++;
336
321
  ctx.log(`[funding-auto] Opened ${opp.mode} ${opp.symbol}: spread ${opp.annualSpread.toFixed(1)}% | ` +
337
322
  `BE ${opp.breakEvenHours.toFixed(1)}h | income $${opp.hourlyIncome.toFixed(4)}/h | ` +
338
323
  `long ${opp.longExchange}, short ${opp.shortExchange} (${currentCount}/${this.maxPositions})`);
@@ -342,13 +327,20 @@ class FundingAutoStrategy {
342
327
  }
343
328
  async onStop(ctx) {
344
329
  const adapters = this.getAdapters(ctx);
345
- const positions = await this.discoverArbPairs(adapters);
330
+ const { pairs: positions, unreadable } = await this.discoverArbPairs(adapters, ctx.log);
331
+ if (unreadable.size > 0) {
332
+ ctx.log(`[funding-auto] CRITICAL: positions on ${[...unreadable].join(", ")} could not be read — any pair there is NOT closed on stop; check manually`);
333
+ }
346
334
  if (positions.length === 0) {
347
335
  ctx.log("[funding-auto] No positions to close.");
348
336
  return [];
349
337
  }
350
338
  ctx.log(`[funding-auto] Stopping - closing ${positions.length} positions`);
351
339
  for (const pos of positions) {
340
+ if (isQuarantined(pos.symbol, ctx.log)) {
341
+ ctx.log(`[funding-auto] CRITICAL: ${pos.symbol} is quarantined — left open on stop; resolve manually (perp strategy quarantine)`);
342
+ continue;
343
+ }
352
344
  try {
353
345
  await this.closePosition(ctx, pos, adapters);
354
346
  }
@@ -381,9 +373,10 @@ class FundingAutoStrategy {
381
373
  const price = rawSize > 0 ? this.getSizeUsd() / rawSize : 0;
382
374
  return formatSizeForPrice(rawSize, price);
383
375
  }
384
- /** Discover existing arb pairs from live exchange positions */
385
- async discoverArbPairs(adapters) {
376
+ /** Discover existing arb pairs from live exchange positions; `unreadable` = venues whose positions failed to load. */
377
+ async discoverArbPairs(adapters, log) {
386
378
  const allPositions = new Map();
379
+ const unreadable = new Set();
387
380
  for (const [name, adapter] of adapters) {
388
381
  try {
389
382
  const exchangePositions = await adapter.getPositions();
@@ -395,15 +388,19 @@ class FundingAutoStrategy {
395
388
  exchange: name,
396
389
  side: p.side,
397
390
  size: Math.abs(Number(p.size)),
391
+ sizeRaw: String(p.size).trim().replace(/^[-+]/, ""), // venue's own size string, for closes
398
392
  symbol: p.symbol, // raw for order execution
399
393
  });
400
394
  }
401
395
  }
402
- catch { /* skip unavailable exchange */ }
396
+ catch (e) {
397
+ unreadable.add(name);
398
+ log(`[funding-auto] ${name} positions unreadable (${e instanceof Error ? e.message : String(e)}) — no entries on ${name} this tick`);
399
+ }
403
400
  }
404
- // Find arb pairs: same symbol, opposite sides, different exchanges = perp-perp
401
+ // Find arb pairs: same symbol, opposite sides, different exchanges = perp-perp.
402
+ // An unmatched short is never adopted (spot-perp mode is not supported here).
405
403
  const pairs = [];
406
- const matchedShorts = new Set(); // track shorts already matched to perp-perp
407
404
  for (const [sym, positionList] of allPositions) {
408
405
  const longs = positionList.filter(p => p.side === "long");
409
406
  const shorts = positionList.filter(p => p.side === "short");
@@ -420,43 +417,22 @@ class FundingAutoStrategy {
420
417
  shortSize: short.size,
421
418
  longSymbol: long.symbol,
422
419
  shortSymbol: short.symbol,
420
+ longSizeRaw: long.sizeRaw,
421
+ shortSizeRaw: short.sizeRaw,
423
422
  entrySpread: 0,
424
423
  entryTime: 0,
425
424
  notional: 0,
426
425
  totalCost: 0,
427
426
  accumulatedFunding: 0,
428
427
  });
429
- matchedShorts.add(`${sym}:${short.exchange}`);
430
428
  }
431
429
  }
432
430
  }
433
- // Unmatched shorts = spot-perp ONLY if funding-auto previously entered them
434
- // (prevents adopting unrelated positions like manual PURR short)
435
- for (const short of shorts) {
436
- if (!matchedShorts.has(`${sym}:${short.exchange}`) && this._managedSymbols.has(`${sym}:spot-perp`)) {
437
- pairs.push({
438
- id: `${sym}-spot-${short.exchange}`,
439
- symbol: sym,
440
- mode: "spot-perp",
441
- longExchange: short.exchange,
442
- shortExchange: short.exchange,
443
- longSize: 0,
444
- shortSize: short.size,
445
- longSymbol: "",
446
- shortSymbol: short.symbol,
447
- entrySpread: 0,
448
- entryTime: 0,
449
- notional: 0,
450
- totalCost: 0,
451
- accumulatedFunding: 0,
452
- });
453
- }
454
- }
455
431
  }
456
- return pairs;
432
+ return { pairs, unreadable };
457
433
  }
458
- /** Calculate actual P&L from exchange trade/funding history */
459
- async calculateRealPnl(adapter, symbol) {
434
+ /** Actual fees / funding from exchange trade/funding history; null when the history could not be read (never 0). */
435
+ async calculateRealPnl(adapter, exchange, symbol, log) {
460
436
  try {
461
437
  const [trades, funding] = await Promise.all([
462
438
  adapter.getTradeHistory(50),
@@ -468,105 +444,150 @@ class FundingAutoStrategy {
468
444
  const totalFunding = symFunding.reduce((s, f) => s + Number(f.payment), 0);
469
445
  return { fees: totalFees, funding: totalFunding };
470
446
  }
471
- catch {
472
- return { fees: 0, funding: 0 };
447
+ catch (e) {
448
+ log(`[funding-auto] ${exchange} ${symbol} trade/funding history unreadable: ${e instanceof Error ? e.message : String(e)}`);
449
+ return null;
473
450
  }
474
451
  }
452
+ quarantine(ctx, pos, reason) {
453
+ 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);
454
+ }
455
+ /**
456
+ * Perp-perp entry, sequential and verified: long first, then the short sized to the
457
+ * long's VERIFIED fill; any excess the short did not cover is trimmed off the long
458
+ * (reduce-only, verified). A definite short failure rolls the long back reduce-only.
459
+ * An unknown outcome or a failed rollback / trim quarantines the symbol.
460
+ */
475
461
  async openPosition(ctx, opp, adapters) {
476
- if (opp.mode === "perp-perp") {
477
- const longAdapter = adapters.get(opp.longExchange);
478
- const shortAdapter = adapters.get(opp.shortExchange);
479
- if (!longAdapter || !shortAdapter)
480
- return false;
481
- const price = this.getCachedPrice(ctx, opp.longExchange, opp.symbol)
482
- || this.getCachedPrice(ctx, opp.shortExchange, opp.symbol);
483
- if (price <= 0)
484
- return false;
485
- const notionalUsd = this.getSizeUsd();
486
- const size = this.formatSize(notionalUsd / price, opp.symbol);
487
- // Minimum notional check — skip if below exchange minimums
488
- if (notionalUsd < MIN_NOTIONAL_USD) {
489
- ctx.log(`[funding-auto] Skip ${opp.symbol}: notional $${notionalUsd.toFixed(0)} < min $${MIN_NOTIONAL_USD}`);
490
- return false;
491
- }
492
- try {
493
- await Promise.all([
494
- longAdapter.marketOrder(opp.symbol, "buy", size),
495
- shortAdapter.marketOrder(opp.symbol, "sell", size),
496
- ]);
497
- return true;
498
- }
499
- catch (err) {
500
- const msg = err instanceof Error ? err.message : String(err);
501
- ctx.log(`[funding-auto] Perp-perp entry failed ${opp.symbol}: ${msg}`);
502
- return false;
503
- }
462
+ // Engine: equity unknown → no new entries (closes still run).
463
+ if (ctx.state.get("equityUnknown") === true) {
464
+ ctx.log(`[funding-auto] Skip ${opp.symbol}: equity unknown — no new entries`);
465
+ return false;
504
466
  }
505
- else {
506
- // spot-perp: short perp (spot buy would need spot adapter — use perp adapter for now)
507
- const shortAdapter = adapters.get(opp.shortExchange);
508
- if (!shortAdapter)
509
- return false;
510
- const price = this.getCachedPrice(ctx, opp.shortExchange, opp.symbol);
511
- if (price <= 0)
512
- return false;
513
- const notionalUsd = this.getSizeUsd();
514
- const size = this.formatSize(notionalUsd / price, opp.symbol);
515
- if (notionalUsd < MIN_NOTIONAL_USD) {
516
- ctx.log(`[funding-auto] Skip ${opp.symbol}: notional $${notionalUsd.toFixed(0)} < min $${MIN_NOTIONAL_USD}`);
467
+ const longAdapter = adapters.get(opp.longExchange);
468
+ const shortAdapter = adapters.get(opp.shortExchange);
469
+ if (!longAdapter || !shortAdapter)
470
+ return false;
471
+ const price = this.getCachedPrice(ctx, opp.longExchange, opp.symbol)
472
+ || this.getCachedPrice(ctx, opp.shortExchange, opp.symbol);
473
+ if (price <= 0)
474
+ return false;
475
+ const notionalUsd = this.getSizeUsd();
476
+ const size = this.formatSize(notionalUsd / price, opp.symbol);
477
+ // Minimum notional check — skip if below exchange minimums
478
+ if (notionalUsd < MIN_NOTIONAL_USD) {
479
+ ctx.log(`[funding-auto] Skip ${opp.symbol}: notional $${notionalUsd.toFixed(0)} < min $${MIN_NOTIONAL_USD}`);
480
+ return false;
481
+ }
482
+ if (!(Number(size) > 0)) {
483
+ ctx.log(`[funding-auto] Skip ${opp.symbol}: size rounds to 0 at $${notionalUsd.toFixed(0)} / $${price}`);
484
+ return false;
485
+ }
486
+ const errText = (e) => e instanceof Error ? e.message : String(e);
487
+ const dec = sizeDecimalsOf(size);
488
+ let longFilled;
489
+ try {
490
+ longFilled = (await placePerpVerified(longAdapter, opp.longExchange, opp.symbol, "buy", size)).filledSize;
491
+ }
492
+ catch (err) {
493
+ if (isOutcomeUnknown(err))
494
+ this.quarantine(ctx, opp, `entry long ${size} on ${opp.longExchange} outcome unknown (${errText(err)}) — no short placed`);
495
+ else
496
+ ctx.log(`[funding-auto] Perp-perp entry failed ${opp.symbol} (long leg): ${errText(err)}`);
497
+ return false;
498
+ }
499
+ const longOpen = fillSizeString(longFilled, dec);
500
+ let shortFilled;
501
+ try {
502
+ shortFilled = (await placePerpVerified(shortAdapter, opp.shortExchange, opp.symbol, "sell", longOpen)).filledSize;
503
+ }
504
+ catch (err) {
505
+ const msg = errText(err);
506
+ if (isOutcomeUnknown(err)) {
507
+ // The short may exist: closing the long could leave a naked short.
508
+ this.quarantine(ctx, opp, `entry short ${longOpen} on ${opp.shortExchange} outcome unknown (${msg}); long ${longOpen} open on ${opp.longExchange} — not rolled back`);
517
509
  return false;
518
510
  }
511
+ ctx.log(`[funding-auto] Perp-perp entry failed ${opp.symbol} (short leg): ${msg} — rolling back the long (reduce-only)`);
519
512
  try {
520
- // Short perp leg — spot buy is out-of-scope for perp-only adapters
521
- await shortAdapter.marketOrder(opp.symbol, "sell", size);
522
- ctx.log(`[funding-auto] Spot-perp ${opp.symbol}: perp short opened (spot leg requires manual hedge or spot adapter)`);
523
- return true;
513
+ const rb = await closePerpVerified(longAdapter, opp.longExchange, opp.symbol, "sell", longOpen, { decimals: dec });
514
+ if (rb.residual > 0) {
515
+ this.quarantine(ctx, opp, `entry short 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}`);
516
+ return false;
517
+ }
518
+ ctx.log(`[funding-auto] Rollback OK: closed long ${longOpen} ${opp.symbol} on ${opp.longExchange}`);
524
519
  }
525
- catch (err) {
526
- const msg = err instanceof Error ? err.message : String(err);
527
- ctx.log(`[funding-auto] Spot-perp entry failed ${opp.symbol}: ${msg}`);
528
- return false;
520
+ catch (rbErr) {
521
+ this.quarantine(ctx, opp, `entry short failed (${msg}) and the long rollback failed or is unconfirmed (${errText(rbErr)}) — orphaned long ${longOpen} on ${opp.longExchange}`);
529
522
  }
523
+ return false;
524
+ }
525
+ try {
526
+ const trimmed = await trimFirstLegToHedge(longAdapter, opp.longExchange, opp.symbol, "buy", longFilled, shortFilled, dec);
527
+ if (trimmed !== "0")
528
+ ctx.log(`[funding-auto] Short filled ${fillSizeString(shortFilled, dec)} of ${longOpen} — trimmed long by ${trimmed} (reduce-only)`);
529
+ }
530
+ catch (trimErr) {
531
+ this.quarantine(ctx, opp, `entry short filled ${fillSizeString(shortFilled, dec)} of ${longOpen}; trimming the long's excess on ${opp.longExchange} failed or is unconfirmed (${errText(trimErr)})`);
532
+ return false;
530
533
  }
534
+ return true;
531
535
  }
536
+ /**
537
+ * Close both legs sequentially, reduce-only and verified, each sized from the venue's
538
+ * own position size (never rounded up). Long first: a definite failure there changes
539
+ * nothing, so the pair is retried next tick and quarantined after MAX_CLOSE_FAILURES
540
+ * in a row. Once the long is closed, ANY short failure leaves an unhedged short this
541
+ * strategy can no longer discover as a pair → quarantine. An unknown outcome or a
542
+ * residual that survives one reduce-only residual close quarantines. Returns true when both legs closed.
543
+ */
532
544
  async closePosition(ctx, pos, adapters) {
533
- if (pos.mode === "perp-perp") {
534
- const longAdapter = adapters.get(pos.longExchange);
535
- const shortAdapter = adapters.get(pos.shortExchange);
536
- if (longAdapter && shortAdapter) {
537
- // Use actual sizes from exchange positions, with raw exchange symbols
538
- const longSize = formatSizeForPrice(pos.longSize, pos.longSize > 0 && pos.notional > 0 ? pos.notional / pos.longSize : 0);
539
- const shortSize = formatSizeForPrice(pos.shortSize, pos.shortSize > 0 && pos.notional > 0 ? pos.notional / pos.shortSize : 0);
540
- try {
541
- await Promise.all([
542
- longAdapter.marketOrder(pos.longSymbol, "sell", longSize),
543
- shortAdapter.marketOrder(pos.shortSymbol, "buy", shortSize),
544
- ]);
545
- const netPnl = pos.accumulatedFunding - pos.totalCost;
546
- ctx.log(`[funding-auto] Closed perp-perp ${pos.symbol} | funding $${pos.accumulatedFunding.toFixed(4)} - fees $${pos.totalCost.toFixed(4)} = net $${netPnl.toFixed(4)}`);
547
- }
548
- catch (err) {
549
- const msg = err instanceof Error ? err.message : String(err);
550
- ctx.log(`[funding-auto] Close failed ${pos.symbol}: ${msg}`);
551
- }
552
- }
545
+ const longAdapter = adapters.get(pos.longExchange);
546
+ const shortAdapter = adapters.get(pos.shortExchange);
547
+ if (!longAdapter || !shortAdapter) {
548
+ ctx.log(`[funding-auto] Close ${pos.symbol}: adapter missing (${pos.longExchange}/${pos.shortExchange})`);
549
+ return false;
553
550
  }
554
- else {
555
- // spot-perp: close perp short
556
- const shortAdapter = adapters.get(pos.shortExchange);
557
- if (!shortAdapter)
558
- return;
559
- const shortSize = formatSizeForPrice(pos.shortSize, pos.shortSize > 0 && pos.notional > 0 ? pos.notional / pos.shortSize : 0);
560
- try {
561
- await shortAdapter.marketOrder(pos.shortSymbol, "buy", shortSize);
562
- const netPnl = pos.accumulatedFunding - pos.totalCost;
563
- ctx.log(`[funding-auto] Closed spot-perp ${pos.symbol} | funding $${pos.accumulatedFunding.toFixed(4)} - fees $${pos.totalCost.toFixed(4)} = net $${netPnl.toFixed(4)}`);
564
- }
565
- catch (err) {
566
- const msg = err instanceof Error ? err.message : String(err);
567
- ctx.log(`[funding-auto] Close failed ${pos.symbol}: ${msg}`);
551
+ const errText = (e) => e instanceof Error ? e.message : String(e);
552
+ const failures = ctx.state.get("closeFailures") ?? new Map();
553
+ ctx.state.set("closeFailures", failures);
554
+ let lc;
555
+ try {
556
+ lc = await closePerpVerified(longAdapter, pos.longExchange, pos.longSymbol, "sell", pos.longSizeRaw);
557
+ }
558
+ catch (err) {
559
+ const msg = errText(err);
560
+ if (isOutcomeUnknown(err)) {
561
+ this.quarantine(ctx, pos, `close of long ${pos.longSizeRaw} on ${pos.longExchange} outcome unknown (${msg}) — short ${pos.shortSizeRaw} on ${pos.shortExchange} left open`);
562
+ return false;
568
563
  }
564
+ const n = (failures.get(pos.id) ?? 0) + 1;
565
+ failures.set(pos.id, n);
566
+ ctx.log(`[funding-auto] Close failed ${pos.symbol} (long leg, nothing changed): ${msg} (${n}/${MAX_CLOSE_FAILURES})`);
567
+ if (n >= MAX_CLOSE_FAILURES)
568
+ this.quarantine(ctx, pos, `close failed ${n} times in a row (last: ${msg})`);
569
+ return false;
570
+ }
571
+ failures.delete(pos.id);
572
+ if (lc.residual > 0) {
573
+ this.quarantine(ctx, pos, `close of long on ${pos.longExchange} closed ${lc.closed} of ${pos.longSizeRaw}, ${lc.residual} still open${lc.note !== null ? ` (${lc.note})` : ""} — short ${pos.shortSizeRaw} on ${pos.shortExchange} not closed (legs unbalanced)`);
574
+ return false;
575
+ }
576
+ let sc;
577
+ try {
578
+ sc = await closePerpVerified(shortAdapter, pos.shortExchange, pos.shortSymbol, "buy", pos.shortSizeRaw);
579
+ }
580
+ catch (err) {
581
+ this.quarantine(ctx, pos, `long closed on ${pos.longExchange} but the close of short ${pos.shortSizeRaw} on ${pos.shortExchange} ${isOutcomeUnknown(err) ? "has an unknown outcome" : "failed"} (${errText(err)}) — short unhedged`);
582
+ return false;
583
+ }
584
+ if (sc.residual > 0) {
585
+ this.quarantine(ctx, pos, `long closed; close of short on ${pos.shortExchange} closed ${sc.closed} of ${pos.shortSizeRaw}, ${sc.residual} still open${sc.note !== null ? ` (${sc.note})` : ""} — residual short unhedged`);
586
+ return false;
569
587
  }
588
+ const netPnl = pos.accumulatedFunding - pos.totalCost;
589
+ ctx.log(`[funding-auto] Closed perp-perp ${pos.symbol} | funding $${pos.accumulatedFunding.toFixed(4)} - fees $${pos.totalCost.toFixed(4)} = net $${netPnl.toFixed(4)}`);
590
+ return true;
570
591
  }
571
592
  }
572
593
  registerStrategy("funding-auto", (_config) => new FundingAutoStrategy());
@@ -29,16 +29,30 @@ export declare class SpotPerpArbStrategy implements Strategy {
29
29
  };
30
30
  private _config;
31
31
  private _failCooldown;
32
+ /** Consecutive failed close attempts per symbol (quarantined at MAX_CLOSE_FAILURES). */
33
+ private _closeFailures;
34
+ /** Venues whose positions could not be read during recovery: no entries there until re-read. */
35
+ private _unrecovered;
36
+ /** Perp symbols this bot entered or tracked this run — the only symbols its stop cancels orders on. */
37
+ private _traded;
32
38
  private _fundingScoreCache;
33
39
  private get params();
34
40
  init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
35
41
  onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
36
42
  onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
43
+ /** Close recorded in the arb state: entry removed (remaining null) or its legs set to the still-hedged remainder. */
44
+ private recordClose;
45
+ private quarantined;
46
+ /** Unknown / unhedged outcome (executor `manual`): stop touching this symbol until a human clears it. */
47
+ private quarantine;
37
48
  private scoreFundingLocal;
49
+ /** base (upper-case) → perp venues it may hedge on (canonical symbol + price guard, `botSpotPerpMatches`). */
50
+ private spotPerpVenues;
38
51
  /** Find all perp exchanges for a symbol, sorted by annualized rate (highest first). */
39
52
  private findPerpCandidates;
40
53
  private calcAnnualized;
41
54
  private shouldClose;
42
55
  private tryOpenPosition;
56
+ /** Rebuild positions from the venues; `unreadable` = venues whose positions / spot balances failed to load (never "none"). */
43
57
  private recoverPositions;
44
58
  }