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
@@ -4,7 +4,15 @@
4
4
  */
5
5
  import type { StrategyContext } from "../strategy-types.js";
6
6
  import type { ExchangeAdapter } from "../../exchanges/index.js";
7
- import type { SpotAdapter } from "../../exchanges/spot-interface.js";
7
+ import type { SpotAdapter, SpotMarketInfo } from "../../exchanges/spot-interface.js";
8
+ import type { VenueMarket } from "../../exchanges/venue-types.js";
9
+ import "../../exchanges/venues/index.js";
10
+ /**
11
+ * Funding strategies refuse a venue whose funding rate is unverified as a leg (design §14.12
12
+ * #6): every adapter of the run (primary + extraAdapters) is checked when the strategy starts.
13
+ * Adapter names that are not registered venues (test doubles) are not venues and are skipped.
14
+ */
15
+ export declare function assertFundingArbLegs(ctx: StrategyContext): void;
8
16
  export type RateEntry = {
9
17
  rate: number;
10
18
  price: number;
@@ -15,12 +23,26 @@ export type RateEntry = {
15
23
  export type RateMap = Map<string, Map<string, RateEntry>>;
16
24
  /** Build adapter map from primary adapter + extraAdapters in context state. */
17
25
  export declare function buildAdapterMap(ctx: StrategyContext): Map<string, ExchangeAdapter>;
18
- /** Get or create a spot adapter for a given exchange name + perp adapter instance. */
19
- export declare function getSpotAdapter(name: string, adapter: ExchangeAdapter): Promise<SpotAdapter | null>;
20
- /** Transfer USDC from perp to spot account (exchange-specific). */
21
- export declare function transferUsdcToSpot(spotAdapter: SpotAdapter, exchangeName: string, amount: number): Promise<void>;
22
- /** Transfer USDC from spot to perp account (exchange-specific). */
23
- export declare function transferUsdcToPerp(spotAdapter: SpotAdapter, exchangeName: string, amount: number): Promise<void>;
26
+ /**
27
+ * Move the spot venue's quote asset (quoteAssets[0]) perp → spot. Returns false when
28
+ * spot shares perp margin (no-op — callers must not log "Transferred"); throws when
29
+ * the venue has no transfer.
30
+ */
31
+ export declare function transferUsdcToSpot(spotAdapter: SpotAdapter, amount: number): Promise<boolean>;
32
+ /** Spot → perp counterpart of transferUsdcToSpot. */
33
+ export declare function transferUsdcToPerp(spotAdapter: SpotAdapter, amount: number): Promise<boolean>;
34
+ /**
35
+ * Bot spot↔perp candidates: spot bases on `spotVenue` matched to each perp venue by
36
+ * canonical symbol + price-deviation guard (`matchSpotPerp`). Bare-ticker equality is
37
+ * not enough. Bot positions store one symbol and derive the perp leg with
38
+ * `getPerpSymbol(base)`, so a canonical pair whose perp symbol differs from the spot
39
+ * base (e.g. UBTC ↔ BTC) is reported and skipped rather than traded under the wrong
40
+ * perp symbol. Returns perp venue → matched spot bases (upper-case).
41
+ *
42
+ * `perpMarketsCache` lets one scan reuse each perp venue's public market fetch.
43
+ * A perp venue whose markets cannot be fetched is logged and gets no candidates.
44
+ */
45
+ export declare function botSpotPerpMatches(spotVenue: string, spotMarkets: readonly SpotMarketInfo[], perpVenues: readonly string[], perpMarketsCache: Map<string, Promise<VenueMarket[]>>, log: (msg: string) => void): Promise<Map<string, Set<string>>>;
24
46
  /** Resolve perp symbol for a given base symbol on an exchange. */
25
47
  export declare function getPerpSymbol(baseSymbol: string, _exchangeName: string): string;
26
48
  /** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP. */
@@ -29,15 +51,6 @@ export declare function findRate(ratesByExchange: RateMap, exchange: string, sym
29
51
  export declare function matchSymbol(s: string, target: string): boolean;
30
52
  /** Get a price estimate for a symbol from the perp adapter. */
31
53
  export declare function getPriceEstimate(perpAdapter: ExchangeAdapter, perpSymbol: string, fallbackSymbol: string): Promise<number>;
32
- export interface RecoveredPosition {
33
- symbol: string;
34
- mode: "spot-perp" | "perp-perp";
35
- longExchange: string;
36
- shortExchange: string;
37
- size: string;
38
- }
39
- /** Recover arb positions from exchange state (perp-perp + spot-perp, same + cross exchange). */
40
- export declare function recoverArbPositions(adapters: Map<string, ExchangeAdapter>, log: (msg: string) => void): Promise<RecoveredPosition[]>;
41
54
  export interface FundingScore {
42
55
  symbol: string;
43
56
  exchange: string;
@@ -46,18 +59,24 @@ export interface FundingScore {
46
59
  annualized: number;
47
60
  payments: number;
48
61
  }
49
- /** Returns true if a reduceOnly rejection indicates the position is already gone. */
50
- export declare function isPositionGone(msg: string): boolean;
51
- /** Compute funding period in hours from history timestamps. Falls back to static map. */
52
- export declare function computeFundingHoursFromHistory(history: {
53
- time: number;
54
- }[]): number;
62
+ /**
63
+ * true when a reduce-only close was rejected because there is no position left to reduce.
64
+ * Prefers the typed signal: placePerpVerified's ORDER_REJECTED with details.reduceOnlyFlat
65
+ * (flat before and after the order — "(no X position to reduce)"). Venue texts that carry the
66
+ * same meaning are matched too: HL "Reduce only order would increase position", Aster -2022
67
+ * / "ReduceOnly Order is rejected". Only ever applied to a DEFINITE rejection (an
68
+ * ORDER_PENDING outcome is never "gone").
69
+ */
70
+ export declare function isPositionGone(e: unknown): boolean;
55
71
  /** Score a symbol's funding rate using history. Returns null if insufficient data. */
56
72
  export declare function scoreFunding(adapter: ExchangeAdapter, symbol: string, exchangeName: string, periods: number, cache?: Map<string, {
57
73
  score: FundingScore;
58
74
  ts: number;
59
75
  }>): Promise<FundingScore | null>;
60
- /** Fetch funding rates from an exchange adapter. */
76
+ /**
77
+ * Fetch funding rates from an exchange adapter. Failures propagate — callers
78
+ * record them per venue (an empty list would read as "venue has no markets").
79
+ */
61
80
  export declare function fetchRates(adapter: ExchangeAdapter, exchangeName: string): Promise<{
62
81
  symbol: string;
63
82
  rate: number;
@@ -66,3 +85,9 @@ export declare function fetchRates(adapter: ExchangeAdapter, exchangeName: strin
66
85
  maxLeverage?: number;
67
86
  fundingHours?: number;
68
87
  }[]>;
88
+ /**
89
+ * Cancel the open orders of `symbols` (the bot's own) on every venue in `adapters` — never an
90
+ * account-wide cancel. Each failure is logged (never "cancelled"); nothing is logged as
91
+ * cancelled when the bot traded no symbol.
92
+ */
93
+ export declare function cancelBotSymbols(adapters: Map<string, ExchangeAdapter>, symbols: ReadonlySet<string>, tag: string, log: (msg: string) => void): Promise<void>;
@@ -3,7 +3,29 @@
3
3
  * Extracted to keep individual strategy files under size limits.
4
4
  */
5
5
  import { getFundingHours } from "../../funding.js";
6
+ import { sharedApiNetwork } from "../../api/public/urls.js";
6
7
  import { annualizeHourlyRate } from "../../funding/normalize.js";
8
+ import { fetchPerpMarketsForMatching, matchSpotPerp, transferQuoteToPerp, transferQuoteToSpot } from "../../exchanges/spot-factory.js";
9
+ import { PerpError, extractErrorMessage } from "../../errors.js";
10
+ import { assertFundingArbVenue, tryResolveVenueId } from "../../exchanges/venue.js";
11
+ import "../../exchanges/venues/index.js";
12
+ /**
13
+ * Funding strategies refuse a venue whose funding rate is unverified as a leg (design §14.12
14
+ * #6): every adapter of the run (primary + extraAdapters) is checked when the strategy starts.
15
+ * Adapter names that are not registered venues (test doubles) are not venues and are skipped.
16
+ */
17
+ export function assertFundingArbLegs(ctx) {
18
+ const names = new Set([ctx.adapter.name]);
19
+ const extra = ctx.state.get("extraAdapters");
20
+ if (extra !== undefined)
21
+ for (const a of extra.values())
22
+ names.add(a.name);
23
+ for (const name of names) {
24
+ if (tryResolveVenueId(name.split(":")[0]) === null)
25
+ continue;
26
+ assertFundingArbVenue(name);
27
+ }
28
+ }
7
29
  // ── Adapter helpers ──
8
30
  /** Build adapter map from primary adapter + extraAdapters in context state. */
9
31
  export function buildAdapterMap(ctx) {
@@ -16,73 +38,64 @@ export function buildAdapterMap(ctx) {
16
38
  }
17
39
  return adapters;
18
40
  }
19
- const spotAdapterCache = new Map();
20
- /** Get or create a spot adapter for a given exchange name + perp adapter instance. */
21
- export async function getSpotAdapter(name, adapter) {
22
- const cached = spotAdapterCache.get(name);
23
- if (cached)
24
- return cached;
25
- try {
26
- let spot = null;
27
- if (name === "hyperliquid") {
28
- const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
29
- const { HyperliquidAdapter } = await import("../../exchanges/hyperliquid.js");
30
- if (adapter instanceof HyperliquidAdapter) {
31
- const instance = new HyperliquidSpotAdapter(adapter);
32
- await instance.init();
33
- spot = instance;
34
- }
35
- }
36
- else if (name === "lighter") {
37
- const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
38
- const { LighterAdapter } = await import("../../exchanges/lighter.js");
39
- if (adapter instanceof LighterAdapter) {
40
- const instance = new LighterSpotAdapter(adapter);
41
- await instance.init();
42
- spot = instance;
43
- }
44
- }
45
- if (spot)
46
- spotAdapterCache.set(name, spot);
47
- return spot;
48
- }
49
- catch { /* not supported */ }
50
- return null;
41
+ // ── Transfer helpers (§12.4 #4) ──
42
+ // Spot adapters come from the factory (src/exchanges/spot-factory.ts, getBotSpotAdapter).
43
+ /**
44
+ * Move the spot venue's quote asset (quoteAssets[0]) perp → spot. Returns false when
45
+ * spot shares perp margin (no-op — callers must not log "Transferred"); throws when
46
+ * the venue has no transfer.
47
+ */
48
+ export async function transferUsdcToSpot(spotAdapter, amount) {
49
+ return transferQuoteToSpot(spotAdapter, amount);
51
50
  }
52
- // ── Transfer helpers ──
53
- /** Transfer USDC from perp to spot account (exchange-specific). */
54
- export async function transferUsdcToSpot(spotAdapter, exchangeName, amount) {
55
- if (exchangeName === "hyperliquid") {
56
- const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
57
- if (spotAdapter instanceof HyperliquidSpotAdapter) {
58
- await spotAdapter.transferUsdcToSpot(amount);
59
- return;
51
+ /** Spot → perp counterpart of transferUsdcToSpot. */
52
+ export async function transferUsdcToPerp(spotAdapter, amount) {
53
+ return transferQuoteToPerp(spotAdapter, amount);
54
+ }
55
+ // ── Spot ↔ perp matching (§12.4 #6) ──
56
+ /**
57
+ * Bot spot↔perp candidates: spot bases on `spotVenue` matched to each perp venue by
58
+ * canonical symbol + price-deviation guard (`matchSpotPerp`). Bare-ticker equality is
59
+ * not enough. Bot positions store one symbol and derive the perp leg with
60
+ * `getPerpSymbol(base)`, so a canonical pair whose perp symbol differs from the spot
61
+ * base (e.g. UBTC ↔ BTC) is reported and skipped rather than traded under the wrong
62
+ * perp symbol. Returns perp venue → matched spot bases (upper-case).
63
+ *
64
+ * `perpMarketsCache` lets one scan reuse each perp venue's public market fetch.
65
+ * A perp venue whose markets cannot be fetched is logged and gets no candidates.
66
+ */
67
+ export async function botSpotPerpMatches(spotVenue, spotMarkets, perpVenues, perpMarketsCache, log) {
68
+ const out = new Map();
69
+ for (const perpVenue of perpVenues) {
70
+ let pending = perpMarketsCache.get(perpVenue);
71
+ if (pending === undefined) {
72
+ pending = fetchPerpMarketsForMatching(perpVenue);
73
+ perpMarketsCache.set(perpVenue, pending);
60
74
  }
61
- }
62
- else if (exchangeName === "lighter") {
63
- const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
64
- if (spotAdapter instanceof LighterSpotAdapter) {
65
- await spotAdapter.transferUsdcToSpot(amount);
66
- return;
75
+ let perpMarkets;
76
+ try {
77
+ perpMarkets = await pending;
67
78
  }
68
- }
69
- }
70
- /** Transfer USDC from spot to perp account (exchange-specific). */
71
- export async function transferUsdcToPerp(spotAdapter, exchangeName, amount) {
72
- if (exchangeName === "hyperliquid") {
73
- const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
74
- if (spotAdapter instanceof HyperliquidSpotAdapter) {
75
- await spotAdapter.transferUsdcToPerp(amount);
76
- return;
79
+ catch (e) {
80
+ log(`spot↔perp matching: ${perpVenue} perp markets unavailable (${e instanceof Error ? e.message : String(e)}) — no ${perpVenue} candidates this scan`);
81
+ out.set(perpVenue, new Set());
82
+ continue;
77
83
  }
78
- }
79
- else if (exchangeName === "lighter") {
80
- const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
81
- if (spotAdapter instanceof LighterSpotAdapter) {
82
- await spotAdapter.transferUsdcToPerp(amount);
83
- return;
84
+ const matches = matchSpotPerp(spotVenue, spotMarkets, perpVenue, perpMarkets, log, sharedApiNetwork());
85
+ const bases = new Set();
86
+ const aliased = [];
87
+ for (const m of matches.values()) {
88
+ if (getPerpSymbol(m.spotBase, perpVenue) === m.perpSymbol.toUpperCase())
89
+ bases.add(m.spotBase);
90
+ else
91
+ aliased.push(`${m.spotBase}↔${m.perpSymbol}`);
92
+ }
93
+ if (aliased.length > 0) {
94
+ log(`spot↔perp matching ${spotVenue}→${perpVenue}: ${aliased.length} canonical pair(s) with a different perp symbol skipped (bot positions use one symbol): ${aliased.join(", ")}`);
84
95
  }
96
+ out.set(perpVenue, bases);
85
97
  }
98
+ return out;
86
99
  }
87
100
  // ── Rate / symbol helpers ──
88
101
  /** Resolve perp symbol for a given base symbol on an exchange. */
@@ -116,112 +129,41 @@ export async function getPriceEstimate(perpAdapter, perpSymbol, fallbackSymbol)
116
129
  return 0;
117
130
  }
118
131
  }
119
- /** Recover arb positions from exchange state (perp-perp + spot-perp, same + cross exchange). */
120
- export async function recoverArbPositions(adapters, log) {
121
- const positionsByExchange = new Map();
122
- for (const [name, a] of adapters) {
123
- try {
124
- const positions = await a.getPositions();
125
- positionsByExchange.set(name, positions.map(p => ({ symbol: p.symbol.toUpperCase(), side: p.side, size: p.size })));
126
- }
127
- catch { /* skip */ }
128
- }
129
- const recovered = [];
130
- const used = new Set();
131
- // Cross-exchange perp-perp
132
- for (const [exA, posA] of positionsByExchange) {
133
- for (const pA of posA) {
134
- const keyA = `${exA}:${pA.symbol}`;
135
- if (used.has(keyA))
136
- continue;
137
- for (const [exB, posB] of positionsByExchange) {
138
- if (exA === exB)
139
- continue;
140
- for (const pB of posB) {
141
- const keyB = `${exB}:${pB.symbol}`;
142
- if (used.has(keyB) || pA.symbol !== pB.symbol || pA.side === pB.side)
143
- continue;
144
- const longEx = pA.side === "long" ? exA : exB;
145
- const shortEx = pA.side === "short" ? exA : exB;
146
- recovered.push({ symbol: pA.symbol, mode: "perp-perp", longExchange: longEx, shortExchange: shortEx, size: pA.side === "long" ? pA.size : pB.size });
147
- used.add(keyA);
148
- used.add(keyB);
149
- break;
150
- }
151
- if (used.has(keyA))
152
- break;
153
- }
154
- }
155
- }
156
- // Same-exchange spot-perp hedges
157
- for (const [name, a] of adapters) {
158
- try {
159
- const spotAdapter = await getSpotAdapter(name, a);
160
- if (!spotAdapter)
161
- continue;
162
- const bals = await spotAdapter.getSpotBalances();
163
- const nonUsdc = bals.filter(b => Number(b.total) > 0 && !b.token.toUpperCase().startsWith("USDC"));
164
- if (nonUsdc.length === 0)
165
- continue;
166
- for (const perp of positionsByExchange.get(name) ?? []) {
167
- const perpKey = `${name}:${perp.symbol}`;
168
- if (used.has(perpKey))
169
- continue;
170
- const base = perp.symbol.replace(/-PERP$/, "").toUpperCase();
171
- const spotBal = nonUsdc.find(b => b.token.toUpperCase().replace(/-SPOT$/, "") === base);
172
- if (spotBal && perp.side === "short") {
173
- recovered.push({ symbol: base, mode: "spot-perp", longExchange: `${name}-spot`, shortExchange: name, size: perp.size });
174
- used.add(perpKey);
175
- log(` Recovered spot-perp: ${base} ${name}-spot<>${name}`);
176
- }
177
- }
178
- }
179
- catch { /* no spot */ }
180
- }
181
- // Cross-exchange spot-perp
182
- for (const [spotExName, spotExAdapter] of adapters) {
183
- try {
184
- const spotAdapter = await getSpotAdapter(spotExName, spotExAdapter);
185
- if (!spotAdapter)
186
- continue;
187
- const bals = await spotAdapter.getSpotBalances();
188
- const nonUsdc = bals.filter(b => Number(b.total) > 0 && !b.token.toUpperCase().startsWith("USDC"));
189
- for (const bal of nonUsdc) {
190
- const base = bal.token.toUpperCase().replace(/-SPOT$/, "");
191
- for (const [perpExName, perpPositions] of positionsByExchange) {
192
- if (perpExName === spotExName)
193
- continue;
194
- for (const perp of perpPositions) {
195
- const perpKey = `${perpExName}:${perp.symbol}`;
196
- if (used.has(perpKey))
197
- continue;
198
- if (perp.symbol.replace(/-PERP$/, "").toUpperCase() === base && perp.side === "short") {
199
- recovered.push({ symbol: base, mode: "spot-perp", longExchange: `${spotExName}-spot`, shortExchange: perpExName, size: perp.size });
200
- used.add(perpKey);
201
- log(` Recovered cross spot-perp: ${base} ${spotExName}-spot<>${perpExName}`);
202
- }
203
- }
204
- }
205
- }
206
- }
207
- catch { /* no spot */ }
208
- }
209
- return recovered;
132
+ /**
133
+ * true when a reduce-only close was rejected because there is no position left to reduce.
134
+ * Prefers the typed signal: placePerpVerified's ORDER_REJECTED with details.reduceOnlyFlat
135
+ * (flat before and after the order — "(no X position to reduce)"). Venue texts that carry the
136
+ * same meaning are matched too: HL "Reduce only order would increase position", Aster -2022
137
+ * / "ReduceOnly Order is rejected". Only ever applied to a DEFINITE rejection (an
138
+ * ORDER_PENDING outcome is never "gone").
139
+ */
140
+ export function isPositionGone(e) {
141
+ if (e instanceof PerpError) {
142
+ if (e.structured.code === "ORDER_PENDING")
143
+ return false;
144
+ if (e.structured.code === "ORDER_REJECTED" && e.structured.details?.reduceOnlyFlat === true)
145
+ return true;
146
+ }
147
+ const msg = typeof e === "string" ? e : extractErrorMessage(e);
148
+ return /position to reduce\)/.test(msg)
149
+ || /reduce only order would increase position/i.test(msg)
150
+ || msg.includes("ReduceOnly") || msg.includes("reduceOnly") || msg.includes("-2022");
210
151
  }
211
- /** Returns true if a reduceOnly rejection indicates the position is already gone. */
212
- export function isPositionGone(msg) {
213
- return msg.includes("ReduceOnly") || msg.includes("reduceOnly") || msg.includes("-2022");
152
+ function hasPerSymbolFundingHours(adapter) {
153
+ return typeof adapter.getFundingHours === "function";
214
154
  }
215
- /** Compute funding period in hours from history timestamps. Falls back to static map. */
216
- export function computeFundingHoursFromHistory(history) {
217
- if (history.length < 2)
218
- return getFundingHours("unknown"); // fallback to 1
219
- const times = history.map(h => h.time).sort((a, b) => a - b);
220
- let totalInterval = 0;
221
- for (let i = 1; i < times.length; i++)
222
- totalInterval += times[i] - times[i - 1];
223
- const avgIntervalH = totalInterval / (times.length - 1) / 3600000;
224
- return avgIntervalH < 1.5 ? 1 : avgIntervalH < 5 ? 4 : 8;
155
+ /**
156
+ * Funding basis (hours one quoted rate covers) for a market: the market's own
157
+ * value when the venue publishes one per symbol, else the venue definition.
158
+ * History timestamp spacing is settlement timing, never the basis (§10.13 #2).
159
+ * @returns null when the venue publishes per-symbol values and this symbol has none
160
+ */
161
+ async function marketFundingHours(adapter, exchangeName, symbol) {
162
+ if (hasPerSymbolFundingHours(adapter)) {
163
+ const hours = await adapter.getFundingHours(symbol);
164
+ return hours === undefined ? null : hours;
165
+ }
166
+ return getFundingHours(exchangeName);
225
167
  }
226
168
  /** Score a symbol's funding rate using history. Returns null if insufficient data. */
227
169
  export async function scoreFunding(adapter, symbol, exchangeName, periods, cache) {
@@ -229,12 +171,16 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
229
171
  const cached = cache?.get(key);
230
172
  if (cached && Date.now() - cached.ts < 10 * 60 * 1000)
231
173
  return cached.score;
174
+ // Outside the try: an unknown venue is a configuration error, not "no score".
175
+ const venueHours = hasPerSymbolFundingHours(adapter) ? null : getFundingHours(exchangeName);
232
176
  try {
177
+ const fH = venueHours !== null ? venueHours : await marketFundingHours(adapter, exchangeName, symbol);
178
+ if (fH === null)
179
+ return null; // per-symbol basis unknown — the hourly rate would be a guess
233
180
  const history = await adapter.getFundingHistory(symbol, periods);
234
181
  if (history.length < 2)
235
182
  return null;
236
183
  const rates = history.map(h => parseFloat(h.rate));
237
- const fH = computeFundingHoursFromHistory(history);
238
184
  const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
239
185
  const consistency = rates.filter(r => r > 0).length / rates.length;
240
186
  const annualized = annualizeHourlyRate(avgRate / fH);
@@ -246,42 +192,60 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
246
192
  return null;
247
193
  }
248
194
  }
249
- /** Fetch funding rates from an exchange adapter. */
195
+ /**
196
+ * Fetch funding rates from an exchange adapter. Failures propagate — callers
197
+ * record them per venue (an empty list would read as "venue has no markets").
198
+ */
250
199
  export async function fetchRates(adapter, exchangeName) {
251
- try {
252
- const markets = await adapter.getMarkets();
253
- const withRates = markets.filter(m => m.fundingRate != null);
254
- // Bootstrap aster funding hours lazily
255
- if (exchangeName === "aster" && "getFundingHours" in adapter) {
256
- const aster = adapter;
257
- const uncached = withRates.filter(m => {
258
- const c = adapter?._fundingHoursCache?.get?.(m.symbol);
259
- return c === undefined;
260
- });
261
- for (const m of uncached.slice(0, 20)) {
262
- m.fundingHours = await aster.getFundingHours(m.symbol);
263
- }
264
- }
265
- // For non-aster exchanges, set fundingHours from static map if not already set
266
- if (exchangeName !== "aster") {
267
- const defaultFH = getFundingHours(exchangeName);
268
- for (const m of withRates) {
269
- if (m.fundingHours == null)
270
- m.fundingHours = defaultFH;
271
- }
272
- }
273
- return withRates
274
- .filter(m => m.fundingHours != null) // skip aster symbols with unknown funding period
275
- .map(m => ({
200
+ const markets = await adapter.getMarkets();
201
+ const withRates = markets.filter(m => m.fundingRate != null);
202
+ // fundingHours = basis (§10.13 #7): market value first, then the venue definition.
203
+ // Per-symbol venues (Aster) load their whole table in one call; symbols it
204
+ // does not list stay null and are skipped below.
205
+ for (const m of withRates) {
206
+ if (m.fundingHours !== null)
207
+ continue;
208
+ m.fundingHours = await marketFundingHours(adapter, exchangeName, m.symbol);
209
+ }
210
+ const result = [];
211
+ for (const m of withRates) {
212
+ // Skip aster symbols with unknown funding period — the hourly rate would be a guess.
213
+ if (m.fundingHours === null)
214
+ continue;
215
+ // Skip markets whose size precision the venue does not publish — a leg could
216
+ // not be sized to the venue's lot, and substituting a static guess mis-sizes it.
217
+ if (m.sizeDecimals === null)
218
+ continue;
219
+ result.push({
276
220
  symbol: m.symbol,
277
221
  rate: parseFloat(m.fundingRate),
278
222
  price: parseFloat(m.markPrice),
279
223
  sizeDecimals: m.sizeDecimals,
280
224
  maxLeverage: m.maxLeverage,
281
225
  fundingHours: m.fundingHours,
282
- }));
226
+ });
283
227
  }
284
- catch {
285
- return [];
228
+ return result;
229
+ }
230
+ /**
231
+ * Cancel the open orders of `symbols` (the bot's own) on every venue in `adapters` — never an
232
+ * account-wide cancel. Each failure is logged (never "cancelled"); nothing is logged as
233
+ * cancelled when the bot traded no symbol.
234
+ */
235
+ export async function cancelBotSymbols(adapters, symbols, tag, log) {
236
+ if (symbols.size === 0) {
237
+ log(` ${tag} No bot symbols traded — no orders to cancel`);
238
+ return;
239
+ }
240
+ for (const [name, a] of adapters) {
241
+ for (const sym of symbols) {
242
+ try {
243
+ await a.cancelAllOrders(sym);
244
+ }
245
+ catch (e) {
246
+ log(` ${tag} WARNING: cancelling ${sym} orders on ${name} FAILED (${e instanceof Error ? e.message : String(e)}) — check: perp -e ${name} account orders`);
247
+ }
248
+ }
286
249
  }
250
+ log(` ${tag} Cancel requested for ${[...symbols].join(", ")} on ${[...adapters.keys()].join(", ")}`);
287
251
  }
@@ -2,6 +2,8 @@ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from
2
2
  export declare class FundingArbV2Strategy implements Strategy {
3
3
  readonly name = "funding-arb-v2";
4
4
  private _config;
5
+ /** Symbols this bot entered or recovered this run — the only symbols its stop cancels orders on. */
6
+ private _traded;
5
7
  describe(): {
6
8
  description: string;
7
9
  params: ({
@@ -24,8 +26,38 @@ export declare class FundingArbV2Strategy implements Strategy {
24
26
  private get sizeUsd();
25
27
  private get scanIntervalSec();
26
28
  init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
29
+ /**
30
+ * Pairs held on the venues: a long on one venue + a short of the same symbol and EXACTLY the
31
+ * same size on another. A single leg or a size-mismatched long/short is quarantined (both
32
+ * sizes recorded) — never tracked, never stacked on. A venue whose positions cannot be read
33
+ * throws (the start fails and is retried): unknown exposure is never "flat".
34
+ */
35
+ private recoverPositions;
27
36
  onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
37
+ /**
38
+ * Close every tracked position (reduce-only, verified). A position that cannot be
39
+ * closed is never dropped silently: it is quarantined (persisted with what is open)
40
+ * so no bot / arb path touches it until a human resolves it.
41
+ */
28
42
  onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
43
+ /** A live position of `symbol` on one of the venues that the bot does not track (or an unreadable venue): no entry on top of it. */
44
+ private heldUntracked;
45
+ private quarantine;
46
+ /**
47
+ * Long then short, each verified; the short is sized to the long's verified fill and
48
+ * any excess the short did not cover is trimmed off the long (reduce-only, verified).
49
+ * A definite short failure rolls the long back reduce-only. Returns the hedged size
50
+ * (both legs), or null when nothing is open / the symbol was quarantined.
51
+ */
52
+ private openPosition;
53
+ /**
54
+ * Close both legs reduce-only with the opened size, verified. Reduce-only makes a
55
+ * retry safe (a leg already flat is rejected, never re-opened), so a definite failure
56
+ * of the LONG close keeps the (still hedged) position for the next tick; MAX_CLOSE_FAILURES
57
+ * in a row, an unknown outcome, or a short close that definitely failed once the long is
58
+ * closed (naked short) quarantines it. Returns true when both legs are flat.
59
+ */
60
+ private closePosition;
29
61
  private getAdapters;
30
62
  /** Convert 8h funding rate difference to annualized spread % */
31
63
  private annualizeSpread;