perp-cli 0.14.3 → 0.16.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 (363) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +138 -405
  4. package/README.zh.md +243 -0
  5. package/dist/agent-wallet/store.d.ts +3 -2
  6. package/dist/agent-wallet/store.js +2 -2
  7. package/dist/api/public/hyperliquid.js +4 -0
  8. package/dist/api/public/lighter.d.ts +5 -0
  9. package/dist/api/public/lighter.js +5 -0
  10. package/dist/api/public/pacifica.js +85 -15
  11. package/dist/api/public/urls.d.ts +2 -0
  12. package/dist/api/public/urls.js +3 -0
  13. package/dist/arb/sizing.js +4 -2
  14. package/dist/arb/state.d.ts +6 -0
  15. package/dist/arb/utils.d.ts +1 -2
  16. package/dist/arb/utils.js +1 -2
  17. package/dist/balance-by-address.d.ts +39 -0
  18. package/dist/balance-by-address.js +44 -0
  19. package/dist/bot/apex/pulse.js +3 -0
  20. package/dist/bot/apex/radar.js +2 -1
  21. package/dist/bot/engine.d.ts +10 -1
  22. package/dist/bot/engine.js +14 -6
  23. package/dist/bot/indicators.d.ts +7 -0
  24. package/dist/bot/indicators.js +16 -0
  25. package/dist/bot/perp-order.d.ts +4 -4
  26. package/dist/bot/perp-order.js +5 -5
  27. package/dist/bot/quarantine.d.ts +1 -1
  28. package/dist/bot/quarantine.js +1 -1
  29. package/dist/bot/quote-rules.js +1 -1
  30. package/dist/bot/strategies/apex-strategy.js +3 -2
  31. package/dist/bot/strategies/engine-mm.js +6 -5
  32. package/dist/bot/strategies/funding-arb-strategy.js +12 -5
  33. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
  34. package/dist/bot/strategies/funding-arb-utils.js +53 -20
  35. package/dist/bot/strategies/funding-auto.js +2 -2
  36. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  37. package/dist/bot/strategies/liquidation-mm.js +8 -2
  38. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  39. package/dist/bot/strategies/momentum-breakout.js +11 -3
  40. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  41. package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
  42. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  43. package/dist/bot/strategies/spot-perp-executor.js +3 -3
  44. package/dist/bot/strategy-types.d.ts +2 -1
  45. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  46. package/dist/bot/tui/BotDashboard.js +1 -1
  47. package/dist/cache.js +12 -6
  48. package/dist/cli-program.d.ts +15 -0
  49. package/dist/cli-program.js +126 -6
  50. package/dist/commands/account.js +168 -24
  51. package/dist/commands/agent-import.d.ts +7 -0
  52. package/dist/commands/agent-import.js +30 -1
  53. package/dist/commands/agent.d.ts +6 -3
  54. package/dist/commands/agent.js +160 -12
  55. package/dist/commands/arb/index.js +23 -81
  56. package/dist/commands/arb-auto.d.ts +24 -0
  57. package/dist/commands/arb-auto.js +175 -97
  58. package/dist/commands/arb.d.ts +20 -0
  59. package/dist/commands/arb.js +107 -163
  60. package/dist/commands/backtest.js +23 -21
  61. package/dist/commands/bot.js +1 -1
  62. package/dist/commands/builder.js +1 -1
  63. package/dist/commands/funds.js +113 -3
  64. package/dist/commands/init.d.ts +1 -1
  65. package/dist/commands/init.js +27 -7
  66. package/dist/commands/manage.js +23 -21
  67. package/dist/commands/market.d.ts +48 -0
  68. package/dist/commands/market.js +395 -100
  69. package/dist/commands/options.d.ts +14 -0
  70. package/dist/commands/options.js +151 -0
  71. package/dist/commands/pacifica-earn.d.ts +5 -0
  72. package/dist/commands/pacifica-earn.js +455 -0
  73. package/dist/commands/pacifica-orders.d.ts +14 -0
  74. package/dist/commands/pacifica-orders.js +142 -0
  75. package/dist/commands/portfolio.d.ts +16 -0
  76. package/dist/commands/portfolio.js +83 -55
  77. package/dist/commands/rebalance.d.ts +1 -0
  78. package/dist/commands/rebalance.js +15 -0
  79. package/dist/commands/risk.d.ts +11 -2
  80. package/dist/commands/risk.js +39 -44
  81. package/dist/commands/trade.d.ts +1 -0
  82. package/dist/commands/trade.js +67 -62
  83. package/dist/commands/wallet.js +1 -1
  84. package/dist/config.d.ts +2 -2
  85. package/dist/config.js +15 -4
  86. package/dist/constants.js +6 -0
  87. package/dist/cross-chain-margin.d.ts +3 -1
  88. package/dist/cross-chain-margin.js +4 -2
  89. package/dist/dashboard/server.js +3 -3
  90. package/dist/dashboard/ui.js +9 -6
  91. package/dist/errors.d.ts +29 -1
  92. package/dist/errors.js +35 -3
  93. package/dist/exchanges/address.d.ts +8 -0
  94. package/dist/exchanges/address.js +47 -0
  95. package/dist/exchanges/arcus/adapter.d.ts +16 -12
  96. package/dist/exchanges/arcus/adapter.js +45 -18
  97. package/dist/exchanges/arcus/agent.d.ts +1 -1
  98. package/dist/exchanges/arcus/agent.js +1 -1
  99. package/dist/exchanges/arcus/client.js +1 -1
  100. package/dist/exchanges/arcus/public.js +3 -2
  101. package/dist/exchanges/arcus/signing.d.ts +2 -3
  102. package/dist/exchanges/arcus/signing.js +1 -1
  103. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  104. package/dist/exchanges/arcus/spot-chain.js +1 -1
  105. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  106. package/dist/exchanges/arcus/spot-router.js +2 -2
  107. package/dist/exchanges/arcus/spot.d.ts +2 -3
  108. package/dist/exchanges/arcus/spot.js +2 -3
  109. package/dist/exchanges/aster-spot.d.ts +3 -3
  110. package/dist/exchanges/aster-spot.js +6 -9
  111. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  112. package/dist/exchanges/aster-typed-data.js +1 -1
  113. package/dist/exchanges/aster.d.ts +30 -10
  114. package/dist/exchanges/aster.js +153 -53
  115. package/dist/exchanges/builder-fee.d.ts +1 -1
  116. package/dist/exchanges/builder-fee.js +13 -2
  117. package/dist/exchanges/canonical-symbols.d.ts +8 -2
  118. package/dist/exchanges/canonical-symbols.js +46 -2
  119. package/dist/exchanges/capabilities.d.ts +2 -0
  120. package/dist/exchanges/capabilities.js +2 -1
  121. package/dist/exchanges/derive/client.d.ts +37 -0
  122. package/dist/exchanges/derive/client.js +135 -0
  123. package/dist/exchanges/derive/options.d.ts +73 -0
  124. package/dist/exchanges/derive/options.js +379 -0
  125. package/dist/exchanges/extended/adapter.d.ts +96 -0
  126. package/dist/exchanges/extended/adapter.js +241 -0
  127. package/dist/exchanges/extended/client.d.ts +38 -0
  128. package/dist/exchanges/extended/client.js +88 -0
  129. package/dist/exchanges/extended/public.d.ts +133 -0
  130. package/dist/exchanges/extended/public.js +436 -0
  131. package/dist/exchanges/grvt/adapter.d.ts +100 -0
  132. package/dist/exchanges/grvt/adapter.js +283 -0
  133. package/dist/exchanges/grvt/client.d.ts +25 -0
  134. package/dist/exchanges/grvt/client.js +86 -0
  135. package/dist/exchanges/grvt/public.d.ts +151 -0
  136. package/dist/exchanges/grvt/public.js +438 -0
  137. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  138. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  139. package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
  140. package/dist/exchanges/hyperliquid-spot.js +24 -16
  141. package/dist/exchanges/hyperliquid.d.ts +199 -22
  142. package/dist/exchanges/hyperliquid.js +550 -202
  143. package/dist/exchanges/index.d.ts +1 -1
  144. package/dist/exchanges/index.js +1 -1
  145. package/dist/exchanges/interface.d.ts +157 -13
  146. package/dist/exchanges/interface.js +47 -0
  147. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  148. package/dist/exchanges/ioc-outcome.js +1 -1
  149. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  150. package/dist/exchanges/legacy-trading-v2.js +7 -2
  151. package/dist/exchanges/lighter-public.d.ts +67 -12
  152. package/dist/exchanges/lighter-public.js +250 -82
  153. package/dist/exchanges/lighter-signer.d.ts +1 -1
  154. package/dist/exchanges/lighter-signer.js +1 -1
  155. package/dist/exchanges/lighter-spot.d.ts +10 -5
  156. package/dist/exchanges/lighter-spot.js +105 -97
  157. package/dist/exchanges/lighter.d.ts +80 -21
  158. package/dist/exchanges/lighter.js +312 -105
  159. package/dist/exchanges/ondo/adapter.d.ts +38 -13
  160. package/dist/exchanges/ondo/adapter.js +86 -27
  161. package/dist/exchanges/ondo/agent.d.ts +1 -1
  162. package/dist/exchanges/ondo/agent.js +1 -1
  163. package/dist/exchanges/ondo/client.d.ts +1 -1
  164. package/dist/exchanges/ondo/client.js +2 -2
  165. package/dist/exchanges/ondo/public.d.ts +35 -3
  166. package/dist/exchanges/ondo/public.js +75 -32
  167. package/dist/exchanges/ondo/spot.d.ts +2 -2
  168. package/dist/exchanges/ondo/spot.js +3 -3
  169. package/dist/exchanges/options-interface.d.ts +158 -0
  170. package/dist/exchanges/options-interface.js +17 -0
  171. package/dist/exchanges/options-venues.d.ts +12 -0
  172. package/dist/exchanges/options-venues.js +35 -0
  173. package/dist/exchanges/outcome-interface.d.ts +4 -0
  174. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  175. package/dist/exchanges/pacifica-atomic.js +440 -0
  176. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  177. package/dist/exchanges/pacifica-decimal.js +51 -0
  178. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  179. package/dist/exchanges/pacifica-presigned.js +141 -0
  180. package/dist/exchanges/pacifica-print.d.ts +200 -0
  181. package/dist/exchanges/pacifica-print.js +417 -0
  182. package/dist/exchanges/pacifica-spot.d.ts +8 -2
  183. package/dist/exchanges/pacifica-spot.js +26 -18
  184. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  185. package/dist/exchanges/pacifica-typed-data.js +1 -1
  186. package/dist/exchanges/pacifica-vaults.d.ts +130 -0
  187. package/dist/exchanges/pacifica-vaults.js +277 -0
  188. package/dist/exchanges/pacifica.d.ts +108 -10
  189. package/dist/exchanges/pacifica.js +428 -171
  190. package/dist/exchanges/precision.d.ts +3 -1
  191. package/dist/exchanges/precision.js +9 -2
  192. package/dist/exchanges/public-balances.d.ts +25 -0
  193. package/dist/exchanges/public-balances.js +158 -0
  194. package/dist/exchanges/public-positions.d.ts +43 -0
  195. package/dist/exchanges/public-positions.js +154 -0
  196. package/dist/exchanges/qfex/adapter.d.ts +69 -13
  197. package/dist/exchanges/qfex/adapter.js +102 -17
  198. package/dist/exchanges/qfex/agent.d.ts +1 -1
  199. package/dist/exchanges/qfex/agent.js +1 -1
  200. package/dist/exchanges/qfex/client.d.ts +1 -1
  201. package/dist/exchanges/qfex/client.js +1 -1
  202. package/dist/exchanges/qfex/errors.d.ts +1 -1
  203. package/dist/exchanges/qfex/errors.js +2 -2
  204. package/dist/exchanges/qfex/public.d.ts +65 -12
  205. package/dist/exchanges/qfex/public.js +131 -25
  206. package/dist/exchanges/qfex/signing.d.ts +2 -2
  207. package/dist/exchanges/qfex/signing.js +4 -5
  208. package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
  209. package/dist/exchanges/qfex/ws-transport.js +3 -3
  210. package/dist/exchanges/rate-limiter.d.ts +1 -1
  211. package/dist/exchanges/rate-limiter.js +5 -2
  212. package/dist/exchanges/registry.d.ts +1 -1
  213. package/dist/exchanges/registry.js +1 -1
  214. package/dist/exchanges/risex/adapter.d.ts +160 -0
  215. package/dist/exchanges/risex/adapter.js +842 -0
  216. package/dist/exchanges/risex/agent.d.ts +154 -0
  217. package/dist/exchanges/risex/agent.js +384 -0
  218. package/dist/exchanges/risex/client.d.ts +47 -0
  219. package/dist/exchanges/risex/client.js +174 -0
  220. package/dist/exchanges/risex/nonce.d.ts +43 -0
  221. package/dist/exchanges/risex/nonce.js +212 -0
  222. package/dist/exchanges/risex/public.d.ts +64 -0
  223. package/dist/exchanges/risex/public.js +150 -0
  224. package/dist/exchanges/risex/session.d.ts +71 -0
  225. package/dist/exchanges/risex/session.js +171 -0
  226. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  227. package/dist/exchanges/risex/typed-data.js +240 -0
  228. package/dist/exchanges/risex/units.d.ts +29 -0
  229. package/dist/exchanges/risex/units.js +104 -0
  230. package/dist/exchanges/slippage.d.ts +1 -1
  231. package/dist/exchanges/slippage.js +1 -1
  232. package/dist/exchanges/spot-factory.d.ts +3 -3
  233. package/dist/exchanges/spot-factory.js +8 -8
  234. package/dist/exchanges/spot-interface.d.ts +5 -5
  235. package/dist/exchanges/spot-interface.js +2 -2
  236. package/dist/exchanges/spot-result.d.ts +3 -3
  237. package/dist/exchanges/spot-result.js +3 -3
  238. package/dist/exchanges/trading-v2.d.ts +3 -3
  239. package/dist/exchanges/trading-v2.js +9 -6
  240. package/dist/exchanges/txflow/adapter.d.ts +61 -0
  241. package/dist/exchanges/txflow/adapter.js +150 -0
  242. package/dist/exchanges/txflow/client.d.ts +47 -0
  243. package/dist/exchanges/txflow/client.js +155 -0
  244. package/dist/exchanges/txflow/public.d.ts +87 -0
  245. package/dist/exchanges/txflow/public.js +355 -0
  246. package/dist/exchanges/unknown-symbol.d.ts +34 -0
  247. package/dist/exchanges/unknown-symbol.js +56 -0
  248. package/dist/exchanges/variational/adapter.d.ts +67 -0
  249. package/dist/exchanges/variational/adapter.js +137 -0
  250. package/dist/exchanges/variational/client.d.ts +21 -0
  251. package/dist/exchanges/variational/client.js +66 -0
  252. package/dist/exchanges/variational/public.d.ts +126 -0
  253. package/dist/exchanges/variational/public.js +430 -0
  254. package/dist/exchanges/venue-types.d.ts +52 -10
  255. package/dist/exchanges/venue-types.js +82 -11
  256. package/dist/exchanges/venue.d.ts +150 -11
  257. package/dist/exchanges/venue.js +126 -4
  258. package/dist/exchanges/venues/arcus.d.ts +1 -2
  259. package/dist/exchanges/venues/arcus.js +5 -4
  260. package/dist/exchanges/venues/aster.d.ts +2 -2
  261. package/dist/exchanges/venues/aster.js +6 -4
  262. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  263. package/dist/exchanges/venues/create-helpers.js +3 -3
  264. package/dist/exchanges/venues/extended.d.ts +21 -0
  265. package/dist/exchanges/venues/extended.js +96 -0
  266. package/dist/exchanges/venues/grvt.d.ts +30 -0
  267. package/dist/exchanges/venues/grvt.js +98 -0
  268. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  269. package/dist/exchanges/venues/hyperliquid.js +19 -8
  270. package/dist/exchanges/venues/index.d.ts +7 -2
  271. package/dist/exchanges/venues/index.js +8 -2
  272. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  273. package/dist/exchanges/venues/lighter-family.js +9 -11
  274. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  275. package/dist/exchanges/venues/lighter-rh.js +20 -8
  276. package/dist/exchanges/venues/lighter.d.ts +1 -1
  277. package/dist/exchanges/venues/lighter.js +18 -6
  278. package/dist/exchanges/venues/ondo.d.ts +1 -1
  279. package/dist/exchanges/venues/ondo.js +5 -3
  280. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  281. package/dist/exchanges/venues/pacifica.js +15 -4
  282. package/dist/exchanges/venues/qfex.d.ts +1 -1
  283. package/dist/exchanges/venues/qfex.js +4 -2
  284. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  285. package/dist/exchanges/venues/risex-release.js +14 -0
  286. package/dist/exchanges/venues/risex.d.ts +31 -0
  287. package/dist/exchanges/venues/risex.js +131 -0
  288. package/dist/exchanges/venues/txflow.d.ts +18 -0
  289. package/dist/exchanges/venues/txflow.js +77 -0
  290. package/dist/exchanges/venues/variational.d.ts +21 -0
  291. package/dist/exchanges/venues/variational.js +85 -0
  292. package/dist/execution-log.d.ts +1 -1
  293. package/dist/funding/legacy-public.d.ts +6 -4
  294. package/dist/funding/legacy-public.js +42 -41
  295. package/dist/funding/normalize.d.ts +1 -1
  296. package/dist/funding/normalize.js +1 -1
  297. package/dist/funding/rates.d.ts +95 -6
  298. package/dist/funding/rates.js +99 -19
  299. package/dist/index.js +7 -6
  300. package/dist/jobs.d.ts +1 -0
  301. package/dist/jobs.js +25 -1
  302. package/dist/landing.d.ts +2 -0
  303. package/dist/landing.js +1 -1
  304. package/dist/liquidity.d.ts +3 -1
  305. package/dist/liquidity.js +8 -4
  306. package/dist/mcp-pacifica-earn.d.ts +9 -0
  307. package/dist/mcp-pacifica-earn.js +64 -0
  308. package/dist/mcp-server.js +211 -68
  309. package/dist/pacifica/client.d.ts +69 -5
  310. package/dist/pacifica/client.js +181 -8
  311. package/dist/pacifica/rate-limit.d.ts +144 -0
  312. package/dist/pacifica/rate-limit.js +267 -0
  313. package/dist/pacifica/types/order.d.ts +2 -1
  314. package/dist/plan-executor.js +7 -2
  315. package/dist/positions-by-address.d.ts +34 -0
  316. package/dist/positions-by-address.js +46 -0
  317. package/dist/rebalance.js +1 -1
  318. package/dist/risk.d.ts +2 -1
  319. package/dist/risk.js +2 -4
  320. package/dist/settings.d.ts +32 -3
  321. package/dist/signer/ows-evm.d.ts +1 -1
  322. package/dist/signer/ows-evm.js +1 -1
  323. package/dist/smart-order.d.ts +1 -1
  324. package/dist/smart-order.js +1 -1
  325. package/dist/strategies/funding-arb.d.ts +1 -1
  326. package/dist/strategies/funding-arb.js +36 -63
  327. package/dist/trade-validator.js +2 -2
  328. package/dist/utils.d.ts +22 -3
  329. package/dist/utils.js +11 -3
  330. package/dist/venue-health.d.ts +40 -0
  331. package/dist/venue-health.js +75 -0
  332. package/dist/withdraw-gate.d.ts +1 -1
  333. package/dist/withdraw-gate.js +1 -1
  334. package/dist/ws/arcus-market.d.ts +9 -0
  335. package/dist/ws/arcus-market.js +9 -0
  336. package/dist/ws/aster-market.d.ts +33 -0
  337. package/dist/ws/aster-market.js +30 -0
  338. package/dist/ws/index.d.ts +980 -0
  339. package/dist/ws/index.js +4460 -0
  340. package/dist/ws/lighter-market.d.ts +14 -0
  341. package/dist/ws/lighter-market.js +13 -0
  342. package/dist/ws/node.d.ts +996 -0
  343. package/dist/ws/node.js +4554 -0
  344. package/dist/ws/pacifica-instrument.d.ts +8 -0
  345. package/dist/ws/pacifica-instrument.js +8 -0
  346. package/dist/ws/unclassified.d.ts +16 -0
  347. package/dist/ws/unclassified.js +43 -0
  348. package/dist/ws-cli/credentials.d.ts +62 -0
  349. package/dist/ws-cli/credentials.js +126 -0
  350. package/package.json +71 -10
  351. package/skills/perp-cli/SKILL.md +153 -36
  352. package/skills/perp-cli/package.json +15 -2
  353. package/skills/perp-cli/references/agent-operations.md +22 -12
  354. package/skills/perp-cli/references/commands.md +143 -17
  355. package/skills/perp-cli/references/strategies.md +38 -3
  356. package/skills/perp-cli/references/ws-library.md +523 -0
  357. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  358. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  359. package/skills/perp-cli/scripts/preflight.sh +27 -19
  360. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  361. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  362. package/dist/cli-spec.d.ts +0 -46
  363. package/dist/cli-spec.js +0 -81
@@ -20,7 +20,8 @@ function scoreStructure(snapshot) {
20
20
  else if (vol > 1_000_000)
21
21
  score += 15;
22
22
  // OI component (0-50): higher open interest = more conviction
23
- const oi = parseFloat(snapshot.openInterest);
23
+ // null (unknown) → no OI score, the same as below every threshold — never a parsed fake 0.
24
+ const oi = snapshot.openInterest === null ? NaN : parseFloat(snapshot.openInterest);
24
25
  if (oi > 50_000_000)
25
26
  score += 50;
26
27
  else if (oi > 20_000_000)
@@ -1,7 +1,7 @@
1
1
  import type { ExchangeAdapter } from "../exchanges/index.js";
2
2
  import { filledOf } from "./fill-size.js";
3
3
  import type { BotConfig } from "./config.js";
4
- import type { StrategyAction } from "./strategy-types.js";
4
+ import type { StrategyAction, EnrichedSnapshot } from "./strategy-types.js";
5
5
  import { type QuoteRules } from "./quote-rules.js";
6
6
  import "./strategies/grid-strategy.js";
7
7
  import "./strategies/dca-strategy.js";
@@ -87,6 +87,15 @@ export declare function executeActions(adapter: ExchangeAdapter, symbol: string,
87
87
  filled: number;
88
88
  }>;
89
89
  export { filledOf };
90
+ /**
91
+ * The tick's market snapshot. `unknown` names a failed orderbook / klines read (null = both
92
+ * read): the empty arrays in `snapshot` then are NOT data — the engine skips the tick of a
93
+ * strategy that reads them (MARKET_DATA_STRATEGIES).
94
+ */
95
+ export declare function getEnrichedSnapshot(adapter: ExchangeAdapter, symbol: string, log: BotLog): Promise<{
96
+ snapshot: EnrichedSnapshot;
97
+ unknown: string | null;
98
+ }>;
90
99
  /** TUI positions of one venue; a failed read → `error` set (shown "unavailable"), never an empty list. */
91
100
  export declare function fetchPositions(adapter: ExchangeAdapter, symbol: string, exchangeName?: string): Promise<{
92
101
  positions: import("./tui/index.js").Position[];
@@ -1,3 +1,4 @@
1
+ import { getVenue, requireRole, tryResolveVenueId } from "../exchanges/venue.js";
1
2
  import { assertCanClosePositions } from "../exchanges/trading-v2.js";
2
3
  import { resolveAdapterSlippage } from "../exchanges/slippage.js";
3
4
  import { filledOf } from "./fill-size.js";
@@ -100,6 +101,12 @@ export async function runBot(adapter, config, jobId, log, extraAdapters, // for
100
101
  mode,
101
102
  /** Global --dry-run: strategy actions are logged, never sent; non-simulatable strategies are refused. */
102
103
  isDryRun) {
104
+ // A venue without the trading role (TxFlow, release-gated RISEx) runs no bot — dry-run included.
105
+ for (const a of [adapter, ...(extraAdapters?.values() ?? [])]) {
106
+ const venueId = tryResolveVenueId(a.name);
107
+ if (venueId !== null)
108
+ requireRole(getVenue(venueId), "trading", "automation");
109
+ }
103
110
  if (isDryRun) {
104
111
  assertStrategySimulatable(config.strategy.type);
105
112
  adapter = dryRunAdapter(adapter);
@@ -192,7 +199,7 @@ isDryRun) {
192
199
  if (mode === "tui") {
193
200
  try {
194
201
  const { startDashboard } = await import("./tui/index.js");
195
- const initialTuiState = buildTuiState(state, config, { price: 0, volatility24h: 0, fundingRate: 0, volume24h: 0 }, [], [], strategyCtx);
202
+ const initialTuiState = buildTuiState(state, config, { price: 0, volatility24h: 0, fundingRate: 0, volume24h: 0, openInterest: null }, [], [], strategyCtx);
196
203
  const result = startDashboard({
197
204
  initialState: initialTuiState,
198
205
  onQuit: () => { running = false; state.phase = "stopped"; },
@@ -883,14 +890,14 @@ export { filledOf };
883
890
  * read): the empty arrays in `snapshot` then are NOT data — the engine skips the tick of a
884
891
  * strategy that reads them (MARKET_DATA_STRATEGIES).
885
892
  */
886
- async function getEnrichedSnapshot(adapter, symbol, log) {
893
+ export async function getEnrichedSnapshot(adapter, symbol, log) {
887
894
  // Multi-symbol strategies use "ALL" — skip market-specific data
888
895
  if (symbol.toUpperCase() === "ALL") {
889
896
  return {
890
897
  snapshot: {
891
898
  price: 0, high24h: 0, low24h: 0, volume24h: 0,
892
899
  fundingRate: 0, volatility24h: 0, rsi: NaN, spreadPct: 0,
893
- klines: [], orderbook: { bids: [], asks: [] }, openInterest: "0",
900
+ klines: [], orderbook: { bids: [], asks: [] }, openInterest: null,
894
901
  },
895
902
  unknown: null,
896
903
  };
@@ -903,7 +910,8 @@ async function getEnrichedSnapshot(adapter, symbol, log) {
903
910
  adapter.getKlines(symbol, "1h", Date.now() - 24 * 60 * 60 * 1000, Date.now()).catch(why("klines")),
904
911
  ]);
905
912
  return {
906
- snapshot: { ...base, klines: klines ?? [], orderbook: orderbook ?? { bids: [], asks: [] }, openInterest: "0" },
913
+ // Open interest is not read for the bot snapshot: null (unknown), never a "0" stand-in.
914
+ snapshot: { ...base, klines: klines ?? [], orderbook: orderbook ?? { bids: [], asks: [] }, openInterest: null },
907
915
  unknown: failed.length > 0 ? failed.join("; ") : null,
908
916
  };
909
917
  }
@@ -982,7 +990,7 @@ export async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
982
990
  }
983
991
  const _spotPriceWarned = new Set();
984
992
  async function fetchSpotPositions(adapter) {
985
- // Bot scope (§12.4 #3): only separate/runtime spot venues with transfer.
993
+ // Bot scope: only separate/runtime spot venues with transfer.
986
994
  const { getBotSpotAdapter, isSpotQuoteAsset, spotMarketsForValuation, spotNetworkOf, spotTokenBase } = await import("../exchanges/spot-factory.js");
987
995
  const { getVenue } = await import("../exchanges/venue.js");
988
996
  let raw;
@@ -1100,7 +1108,7 @@ function buildTuiState(state, config, snapshot, positions, openOrders, strategyC
1100
1108
  exchange: config.exchange,
1101
1109
  price: snapshot.price,
1102
1110
  volume24h: snapshot.volume24h ?? 0,
1103
- openInterest: snapshot.openInterest ?? "0",
1111
+ openInterest: snapshot.openInterest,
1104
1112
  fundingRate: snapshot.fundingRate,
1105
1113
  volatility24h: snapshot.volatility24h,
1106
1114
  positions,
@@ -7,6 +7,13 @@ export declare function calculateEMA(prices: number[], period: number): number[]
7
7
  /** Calculate Simple Moving Average */
8
8
  export declare function calculateSMA(prices: number[], period: number): number[];
9
9
  /** Calculate Volume Weighted Average Price */
10
+ /**
11
+ * Candle volumes as numbers, or null when ANY candle's volume is unknown — null (the venue does
12
+ * not publish it) or not a finite number ("", "undefined", NaN). Never a 0 stand-in.
13
+ */
14
+ export declare function klineVolumes(klines: ReadonlyArray<{
15
+ volume: string | null;
16
+ }>): number[] | null;
10
17
  export declare function calculateVWAP(candles: {
11
18
  close: number;
12
19
  volume: number;
@@ -33,6 +33,22 @@ export function calculateSMA(prices, period) {
33
33
  return sma;
34
34
  }
35
35
  /** Calculate Volume Weighted Average Price */
36
+ /**
37
+ * Candle volumes as numbers, or null when ANY candle's volume is unknown — null (the venue does
38
+ * not publish it) or not a finite number ("", "undefined", NaN). Never a 0 stand-in.
39
+ */
40
+ export function klineVolumes(klines) {
41
+ const out = [];
42
+ for (const k of klines) {
43
+ if (k.volume === null || k.volume.trim() === "")
44
+ return null;
45
+ const v = Number(k.volume);
46
+ if (!Number.isFinite(v))
47
+ return null;
48
+ out.push(v);
49
+ }
50
+ return out;
51
+ }
36
52
  export function calculateVWAP(candles) {
37
53
  let cumVolPrice = 0;
38
54
  let cumVol = 0;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Verified perp market order for bot / arb legs (design §12.3).
2
+ * Verified perp market order for bot / arb legs.
3
3
  *
4
4
  * A resolved `marketOrder()` promise does not prove a fill: Lighter returns success
5
5
  * when its /tx poll times out ("assuming executed") or the IOC filled nothing, and a
@@ -22,7 +22,7 @@
22
22
  * Likewise a plain (non-transport, non-5xx) "not a multiple of lot size N" rejection with
23
23
  * the position measured unchanged → ORDER_REJECTED with details.lotSize (rejectedLotSize()).
24
24
  *
25
- * Venue-final IOC outcome (design §13.10 #4): when the adapter is IocOutcomeCapable and
25
+ * Venue-final IOC outcome: when the adapter is IocOutcomeCapable and
26
26
  * iocOutcomeOf(raw) reports the order's FINAL filled size, that report is authoritative
27
27
  * (no 80% rule) once cross-checked against the measured position delta (tolerance one lot
28
28
  * step, taken from the precision of the order size / reported fill):
@@ -61,7 +61,7 @@ export declare function rejectedLotSize(e: unknown): number | null;
61
61
  /** true when `e` says the order outcome is unknown (quarantine, never retry / roll back blindly). */
62
62
  export declare function isOutcomeUnknown(e: unknown): boolean;
63
63
  /**
64
- * Second-leg residual correction (design §12.3): the second leg is sized to the first
64
+ * Second-leg residual correction: the second leg is sized to the first
65
65
  * leg's VERIFIED fill but may itself fill less (≥ 80% measured, or any venue-final partial fill). Reduce the first leg by exactly
66
66
  * this trade's excess (firstFilled − secondFilled, floored to `decimals`) — reduce-only,
67
67
  * verified, never the venue's whole position. `firstSide` is the side the first leg
@@ -79,7 +79,7 @@ export interface PerpCloseResult {
79
79
  note: string | null;
80
80
  }
81
81
  /**
82
- * Verified reduce-only close of `size` with residual handling (design §12.3, §13.10 #4). placePerpVerified
82
+ * Verified reduce-only close of `size` with residual handling. placePerpVerified
83
83
  * accepts a ≥ 80% measured fill or any venue-final partial IOC fill (cross-checked); a close must
84
84
  * not leave the rest silently open, so:
85
85
  * 1. reduce-only close of `size` (opts.place, e.g. a smartOrder IOC, or a market order);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Verified perp market order for bot / arb legs (design §12.3).
2
+ * Verified perp market order for bot / arb legs.
3
3
  *
4
4
  * A resolved `marketOrder()` promise does not prove a fill: Lighter returns success
5
5
  * when its /tx poll times out ("assuming executed") or the IOC filled nothing, and a
@@ -22,7 +22,7 @@
22
22
  * Likewise a plain (non-transport, non-5xx) "not a multiple of lot size N" rejection with
23
23
  * the position measured unchanged → ORDER_REJECTED with details.lotSize (rejectedLotSize()).
24
24
  *
25
- * Venue-final IOC outcome (design §13.10 #4): when the adapter is IocOutcomeCapable and
25
+ * Venue-final IOC outcome: when the adapter is IocOutcomeCapable and
26
26
  * iocOutcomeOf(raw) reports the order's FINAL filled size, that report is authoritative
27
27
  * (no 80% rule) once cross-checked against the measured position delta (tolerance one lot
28
28
  * step, taken from the precision of the order size / reported fill):
@@ -158,7 +158,7 @@ export async function placePerpVerified(adapter, venue, symbol, side, size, opts
158
158
  }
159
159
  throw orderOutcomeUnknown(venue, what, e, `Do NOT resubmit — the order may still execute. Check the ${venue} ${symbol} position`);
160
160
  }
161
- // Venue-final IOC outcome (§13.10 #4): the venue reported what this order filled; nothing more can fill.
161
+ // Venue-final IOC outcome: the venue reported what this order filled; nothing more can fill.
162
162
  const outcome = isIocOutcomeCapable(adapter) ? adapter.iocOutcomeOf(raw) : null;
163
163
  if (outcome !== null) {
164
164
  const reported = Number(outcome.filledSize);
@@ -226,7 +226,7 @@ export function isOutcomeUnknown(e) {
226
226
  return e instanceof PerpError && e.structured.code === "ORDER_PENDING";
227
227
  }
228
228
  /**
229
- * Second-leg residual correction (design §12.3): the second leg is sized to the first
229
+ * Second-leg residual correction: the second leg is sized to the first
230
230
  * leg's VERIFIED fill but may itself fill less (≥ 80% measured, or any venue-final partial fill). Reduce the first leg by exactly
231
231
  * this trade's excess (firstFilled − secondFilled, floored to `decimals`) — reduce-only,
232
232
  * verified, never the venue's whole position. `firstSide` is the side the first leg
@@ -245,7 +245,7 @@ export async function trimFirstLegToHedge(adapter, venue, symbol, firstSide, fir
245
245
  return excess;
246
246
  }
247
247
  /**
248
- * Verified reduce-only close of `size` with residual handling (design §12.3, §13.10 #4). placePerpVerified
248
+ * Verified reduce-only close of `size` with residual handling. placePerpVerified
249
249
  * accepts a ≥ 80% measured fill or any venue-final partial IOC fill (cross-checked); a close must
250
250
  * not leave the rest silently open, so:
251
251
  * 1. reduce-only close of `size` (opts.place, e.g. a smartOrder IOC, or a market order);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Position quarantine (design §12.3) — GLOBAL per symbol.
2
+ * Position quarantine — GLOBAL per symbol.
3
3
  *
4
4
  * A spot leg whose outcome is unknown (ORDER_PENDING), a perp leg that could not be
5
5
  * confirmed, a close whose post-trade verification failed, or a position left unhedged
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Position quarantine (design §12.3) — GLOBAL per symbol.
2
+ * Position quarantine — GLOBAL per symbol.
3
3
  *
4
4
  * A spot leg whose outcome is unknown (ORDER_PENDING), a perp leg that could not be
5
5
  * confirmed, a close whose post-trade verification failed, or a position left unhedged
@@ -19,7 +19,7 @@ export async function quoteRulesOf(adapter, symbol) {
19
19
  if (network === null) {
20
20
  throw new PerpError("EXCHANGE_ERROR", `${adapter.name}: the adapter's network is unknown, so the ${symbol} tick / minimums cannot be read; nothing was sent`, { exchange: adapter.name });
21
21
  }
22
- const [markets, stepSize] = await Promise.all([getVenue(adapter.name).publicData.fetchMarkets(network), lotStepOf(adapter, symbol)]);
22
+ const [markets, stepSize] = await Promise.all([getVenue(adapter.name).publicData.fetchMarkets(network, { funding: false }), lotStepOf(adapter, symbol)]);
23
23
  const vm = markets.find(m => symbolMatch(m.symbol, symbol));
24
24
  return { tickSize: vm?.tickSize ?? null, stepSize, minSize: vm?.minSize ?? null, minNotional: vm?.minNotional ?? null };
25
25
  }
@@ -85,11 +85,12 @@ export class ApexStrategy {
85
85
  this.slots.splice(idx, 1);
86
86
  }
87
87
  // ── Phase 2: Record pulse history ──
88
- const currentOi = parseFloat(snapshot.openInterest);
88
+ // Unknown OI (null) is not recorded: a fake 0 in the history would read as an OI jump later.
89
89
  const currentVolume = snapshot.volume24h;
90
90
  const historyKey = ctx.symbol;
91
91
  const existing = this.pulseHistory.get(historyKey) ?? [];
92
- existing.push({ oi: currentOi, volume: currentVolume });
92
+ if (snapshot.openInterest !== null)
93
+ existing.push({ oi: parseFloat(snapshot.openInterest), volume: currentVolume });
93
94
  // Keep last 10 entries
94
95
  if (existing.length > 10)
95
96
  existing.shift();
@@ -1,5 +1,5 @@
1
1
  import { registerStrategy } from "../strategy-registry.js";
2
- import { calculateEMA, calculateVWAP, calculateATR } from "../indicators.js";
2
+ import { calculateEMA, calculateVWAP, calculateATR, klineVolumes } from "../indicators.js";
3
3
  export class EngineMmStrategy {
4
4
  name = "engine-mm";
5
5
  describe() {
@@ -92,11 +92,12 @@ export class EngineMmStrategy {
92
92
  const components = [];
93
93
  // Mid-price (always available)
94
94
  components.push(snapshot.price);
95
- // VWAP from klines
96
- if (snapshot.klines.length > 0) {
97
- const candles = snapshot.klines.map(k => ({
95
+ // VWAP from klines — only when every candle volume is known (unknown is never weighted as 0)
96
+ const volumes = klineVolumes(snapshot.klines);
97
+ if (snapshot.klines.length > 0 && volumes !== null) {
98
+ const candles = snapshot.klines.map((k, i) => ({
98
99
  close: parseFloat(k.close),
99
- volume: parseFloat(k.volume),
100
+ volume: volumes[i],
100
101
  }));
101
102
  const vwap = calculateVWAP(candles);
102
103
  if (vwap > 0)
@@ -549,7 +549,7 @@ export class FundingArbStrategy {
549
549
  const perpRates = ratesByExchange.get(name);
550
550
  if (!perpRates)
551
551
  continue;
552
- // Same-venue spot↔perp: canonical symbol + price-deviation guard (§12.4 #6).
552
+ // Same-venue spot↔perp: canonical symbol + price-deviation guard.
553
553
  const spotMarkets = await spotAdapter.getSpotMarkets();
554
554
  const matched = await botSpotPerpMatches(name, spotMarkets, [name], perpMarketsCache, (msg) => ctx.log(` [ARB] ${msg}`));
555
555
  const spotSymbols = matched.get(name);
@@ -1064,9 +1064,16 @@ export class FundingArbStrategy {
1064
1064
  return false;
1065
1065
  }
1066
1066
  const perpSymbol = getPerpSymbol(opp.symbol, opp.shortExchange);
1067
- const priceEstimate = await getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
1068
- if (priceEstimate <= 0) {
1069
- ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: can't determine price`);
1067
+ let priceEstimate;
1068
+ try {
1069
+ priceEstimate = await getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
1070
+ }
1071
+ catch (err) {
1072
+ ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: market read failed: ${err instanceof Error ? err.message : String(err)}`);
1073
+ return false;
1074
+ }
1075
+ if (priceEstimate === null) {
1076
+ ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: no positive mark price on ${opp.shortExchange}`);
1070
1077
  return false;
1071
1078
  }
1072
1079
  // Liquidity check
@@ -1099,7 +1106,7 @@ export class FundingArbStrategy {
1099
1106
  const transferAmt = Math.ceil(matched.notional * 1.02); // slight buffer for price movement
1100
1107
  if (await transferUsdcToSpot(spotAdapter, transferAmt))
1101
1108
  ctx.log(` [ARB] Transferred $${transferAmt} to spot`);
1102
- // Step 2: Buy spot — the hedge is sized to what actually filled (§12.4 #5).
1109
+ // Step 2: Buy spot — the hedge is sized to what actually filled.
1103
1110
  let hedgeSize;
1104
1111
  let spotFill;
1105
1112
  try {
@@ -8,7 +8,7 @@ import type { SpotAdapter, SpotMarketInfo } from "../../exchanges/spot-interface
8
8
  import type { VenueMarket } from "../../exchanges/venue-types.js";
9
9
  import "../../exchanges/venues/index.js";
10
10
  /**
11
- * Funding strategies refuse a venue whose funding rate is unverified as a leg (design §14.12
11
+ * Funding strategies refuse a venue whose funding rate is unverified as a leg (
12
12
  * #6): every adapter of the run (primary + extraAdapters) is checked when the strategy starts.
13
13
  * Adapter names that are not registered venues (test doubles) are not venues and are skipped.
14
14
  */
@@ -58,8 +58,12 @@ export declare function cappedLeverage(configured: number, legs: {
58
58
  }[], logUnknown: (msg: string) => void): number;
59
59
  /** Match a symbol against a target, accounting for -PERP suffix variations. */
60
60
  export declare function matchSymbol(s: string, target: string): boolean;
61
- /** Get a price estimate for a symbol from the perp adapter. */
62
- export declare function getPriceEstimate(perpAdapter: ExchangeAdapter, perpSymbol: string, fallbackSymbol: string): Promise<number>;
61
+ /**
62
+ * The perp venue's mark price for a symbol. null = the market is not listed or its mark is not a
63
+ * positive finite number (Pacifica rows carry markPrice "-" when /info/prices has none). A failed
64
+ * market read throws (never a 0 that callers would size against).
65
+ */
66
+ export declare function getPriceEstimate(perpAdapter: ExchangeAdapter, perpSymbol: string, fallbackSymbol: string): Promise<number | null>;
63
67
  export interface FundingScore {
64
68
  symbol: string;
65
69
  exchange: string;
@@ -82,6 +86,14 @@ export declare function isPositionGone(e: unknown): boolean;
82
86
  * points, or the per-symbol funding basis is unknown). A failed read THROWS — callers must not
83
87
  * mistake a 429 / timeout for "history too thin".
84
88
  */
89
+ /**
90
+ * Funding-history rows one request (or the adapter's paging) can serve per venue — a
91
+ * `history_periods` above it fails every scoring call, so strategies validate it at config time.
92
+ * Venues without an entry page without a limit. Measured / documented in docs/VENUE_READ_CAPS.md.
93
+ */
94
+ export declare const FUNDING_HISTORY_CAP: Readonly<Record<string, number>>;
95
+ /** Throws INVALID_PARAMS when `periods` is not a positive integer or exceeds a leg venue's cap. */
96
+ export declare function assertFundingHistoryPeriods(periods: unknown, venues: readonly string[], param?: string): void;
85
97
  export declare function scoreFunding(adapter: ExchangeAdapter, symbol: string, exchangeName: string, periods: number, cache?: Map<string, {
86
98
  score: FundingScore;
87
99
  ts: number;
@@ -10,7 +10,7 @@ import { PerpError, extractErrorMessage } from "../../errors.js";
10
10
  import { assertFundingArbVenue, tryResolveVenueId } from "../../exchanges/venue.js";
11
11
  import "../../exchanges/venues/index.js";
12
12
  /**
13
- * Funding strategies refuse a venue whose funding rate is unverified as a leg (design §14.12
13
+ * Funding strategies refuse a venue whose funding rate is unverified as a leg (
14
14
  * #6): every adapter of the run (primary + extraAdapters) is checked when the strategy starts.
15
15
  * Adapter names that are not registered venues (test doubles) are not venues and are skipped.
16
16
  */
@@ -38,7 +38,7 @@ export function buildAdapterMap(ctx) {
38
38
  }
39
39
  return adapters;
40
40
  }
41
- // ── Transfer helpers (§12.4 #4) ──
41
+ // ── Transfer helpers ──
42
42
  // Spot adapters come from the factory (src/exchanges/spot-factory.ts, getBotSpotAdapter).
43
43
  /**
44
44
  * Move the spot venue's quote asset (quoteAssets[0]) perp → spot. Returns false when
@@ -52,7 +52,7 @@ export async function transferUsdcToSpot(spotAdapter, amount) {
52
52
  export async function transferUsdcToPerp(spotAdapter, amount) {
53
53
  return transferQuoteToPerp(spotAdapter, amount);
54
54
  }
55
- // ── Spot ↔ perp matching (§12.4 #6) ──
55
+ // ── Spot ↔ perp matching ──
56
56
  /**
57
57
  * Bot spot↔perp candidates: spot bases on `spotVenue` matched to each perp venue by
58
58
  * canonical symbol + price-deviation guard (`matchSpotPerp`). Bare-ticker equality is
@@ -133,18 +133,20 @@ export function matchSymbol(s, target) {
133
133
  const t = target.toUpperCase();
134
134
  return u === t || u === t + "-PERP" || u.replace(/-PERP$/, "") === t;
135
135
  }
136
- /** Get a price estimate for a symbol from the perp adapter. */
136
+ /**
137
+ * The perp venue's mark price for a symbol. null = the market is not listed or its mark is not a
138
+ * positive finite number (Pacifica rows carry markPrice "-" when /info/prices has none). A failed
139
+ * market read throws (never a 0 that callers would size against).
140
+ */
137
141
  export async function getPriceEstimate(perpAdapter, perpSymbol, fallbackSymbol) {
138
- try {
139
- const markets = await perpAdapter.getMarkets();
140
- const market = markets.find(m => m.symbol.toUpperCase() === perpSymbol.toUpperCase() ||
141
- m.symbol.toUpperCase() === fallbackSymbol.toUpperCase() ||
142
- m.symbol.toUpperCase() === `${fallbackSymbol.toUpperCase()}-PERP`);
143
- return market ? parseFloat(market.markPrice) : 0;
144
- }
145
- catch {
146
- return 0;
147
- }
142
+ const markets = await perpAdapter.getMarkets();
143
+ const market = markets.find(m => m.symbol.toUpperCase() === perpSymbol.toUpperCase() ||
144
+ m.symbol.toUpperCase() === fallbackSymbol.toUpperCase() ||
145
+ m.symbol.toUpperCase() === `${fallbackSymbol.toUpperCase()}-PERP`);
146
+ if (market === undefined)
147
+ return null;
148
+ const px = Number(market.markPrice);
149
+ return Number.isFinite(px) && px > 0 ? px : null;
148
150
  }
149
151
  /**
150
152
  * true when a reduce-only close was rejected because there is no position left to reduce.
@@ -172,7 +174,7 @@ function hasPerSymbolFundingHours(adapter) {
172
174
  /**
173
175
  * Funding basis (hours one quoted rate covers) for a market: the market's own
174
176
  * value when the venue publishes one per symbol, else the venue definition.
175
- * History timestamp spacing is settlement timing, never the basis (§10.13 #2).
177
+ * History timestamp spacing is settlement timing, never the basis.
176
178
  * @returns null when the venue publishes per-symbol values and this symbol has none
177
179
  */
178
180
  async function marketFundingHours(adapter, exchangeName, symbol) {
@@ -187,6 +189,29 @@ async function marketFundingHours(adapter, exchangeName, symbol) {
187
189
  * points, or the per-symbol funding basis is unknown). A failed read THROWS — callers must not
188
190
  * mistake a 429 / timeout for "history too thin".
189
191
  */
192
+ /**
193
+ * Funding-history rows one request (or the adapter's paging) can serve per venue — a
194
+ * `history_periods` above it fails every scoring call, so strategies validate it at config time.
195
+ * Venues without an entry page without a limit. Measured / documented in docs/VENUE_READ_CAPS.md.
196
+ */
197
+ export const FUNDING_HISTORY_CAP = {
198
+ // hyperliquid: fundingHistory pages forward, but `-l` above 5000 is refused (HL_FUNDING_HISTORY_MAX_LIMIT).
199
+ // pacifica: cursor paging, 4000 per page × 10 pages (PACIFICA_FUNDING_PAGE × PACIFICA_MAX_PAGES).
200
+ hyperliquid: 5000, pacifica: 40_000, lighter: 750, "lighter-rh": 750, arcus: 1000, aster: 1000, grvt: 10_000, extended: 10_000,
201
+ };
202
+ /** Throws INVALID_PARAMS when `periods` is not a positive integer or exceeds a leg venue's cap. */
203
+ export function assertFundingHistoryPeriods(periods, venues, param = "history_periods") {
204
+ if (typeof periods !== "number" || !Number.isInteger(periods) || periods < 1) {
205
+ throw new PerpError("INVALID_PARAMS", `${param} must be a positive integer (got ${String(periods)})`, { remediation: `Set ${param} to e.g. 6` });
206
+ }
207
+ const over = venues.map(v => v.toLowerCase()).filter(v => FUNDING_HISTORY_CAP[v] !== undefined && periods > FUNDING_HISTORY_CAP[v]);
208
+ if (over.length > 0) {
209
+ const caps = over.map(v => `${v} ${FUNDING_HISTORY_CAP[v]}`).join(", ");
210
+ throw new PerpError("INVALID_PARAMS", `${param} ${periods} exceeds the funding-history cap of ${caps}`, {
211
+ remediation: `Set ${param} to at most ${Math.min(...over.map(v => FUNDING_HISTORY_CAP[v]))}`,
212
+ });
213
+ }
214
+ }
190
215
  export async function scoreFunding(adapter, symbol, exchangeName, periods, cache) {
191
216
  const key = `${exchangeName}:${symbol}`;
192
217
  const cached = cache?.get(key);
@@ -197,15 +222,23 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
197
222
  if (fH === null)
198
223
  return null; // per-symbol basis unknown — the hourly rate would be a guess
199
224
  const history = await adapter.getFundingHistory(symbol, periods);
200
- if (history.length < 2)
225
+ // A print without a rate is unknown, not 0, and a closed-market window (QFEX outside
226
+ // market_hours: rate null, marketClosed) carries no funding: both left out of the average.
227
+ const known = history.filter((h) => h.rate !== null && h.marketClosed !== true);
228
+ if (known.length < 2)
201
229
  return null;
202
- const rates = history.map(h => parseFloat(h.rate));
230
+ const rates = known.map(h => parseFloat(h.rate));
203
231
  if (rates.some(r => !Number.isFinite(r))) {
204
232
  throw new PerpError("EXCHANGE_ERROR", `${exchangeName} ${symbol}: funding history has a non-numeric rate`, { exchange: exchangeName });
205
233
  }
206
- const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
234
+ // Every print normalized by its own interval (Lighter /fundings prints are per 1 h while the
235
+ // venue's quoted rate is per 8 h); intervalHours null → the market basis fH.
236
+ const hourly = known.map((h, i) => rates[i] / (typeof h.intervalHours === "number" ? h.intervalHours : fH));
237
+ const avgHourly = hourly.reduce((s, r) => s + r, 0) / hourly.length;
238
+ /** Average rate per the market's own interval (fH), from interval-normalized prints. */
239
+ const avgRate = avgHourly * fH;
207
240
  const consistency = rates.filter(r => r > 0).length / rates.length;
208
- const annualized = annualizeHourlyRate(avgRate / fH);
241
+ const annualized = annualizeHourlyRate(avgHourly);
209
242
  const score = { symbol, exchange: exchangeName, avgRate, consistency, annualized, payments: rates.length };
210
243
  cache?.set(key, { score, ts: Date.now() });
211
244
  return score;
@@ -217,7 +250,7 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
217
250
  export async function fetchRates(adapter, exchangeName) {
218
251
  const markets = await adapter.getMarkets();
219
252
  const withRates = markets.filter(m => m.fundingRate != null);
220
- // fundingHours = basis (§10.13 #7): market value first, then the venue definition.
253
+ // fundingHours = basis: market value first, then the venue definition.
221
254
  // Per-symbol venues (Aster) load their whole table in one call; symbols it
222
255
  // does not list stay null and are skipped below.
223
256
  for (const m of withRates) {
@@ -5,7 +5,7 @@ import { closePerpVerified, isOutcomeUnknown, placePerpVerified, trimFirstLegToH
5
5
  import { fillSizeString, sizeDecimalsOf } from "../../exchanges/spot-result.js";
6
6
  import { PerpError } from "../../errors.js";
7
7
  import { assertFundingArbLegs } from "./funding-arb-utils.js";
8
- /** Market basis first (ExchangeMarketInfo.fundingHours), else the venue definition's basis (§10.1). */
8
+ /** Market basis first (ExchangeMarketInfo.fundingHours), else the venue definition's basis. */
9
9
  function hourlyOf(info, exchange) {
10
10
  return info.fundingHours !== null ? info.rate / info.fundingHours : toHourlyRate(info.rate, exchange);
11
11
  }
@@ -177,7 +177,7 @@ class FundingAutoStrategy {
177
177
  const surviving = [];
178
178
  for (const pos of positions) {
179
179
  if (isQuarantined(pos.symbol, ctx.log)) {
180
- // Unknown / unhedged outcome recorded by some bot or arb path: hands off (design §12.3).
180
+ // Unknown / unhedged outcome recorded by some bot or arb path: hands off.
181
181
  surviving.push(pos);
182
182
  continue;
183
183
  }
@@ -19,6 +19,7 @@ export declare class LiquidationMmStrategy implements Strategy {
19
19
  };
20
20
  private get params();
21
21
  private _config;
22
+ private _oiUnknownLogged;
22
23
  init(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<void>;
23
24
  onTick(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
24
25
  onStop(_ctx: StrategyContext): Promise<StrategyAction[]>;
@@ -17,9 +17,10 @@ export class LiquidationMmStrategy {
17
17
  return this._config;
18
18
  }
19
19
  _config = {};
20
+ _oiUnknownLogged = false;
20
21
  async init(ctx, snapshot) {
21
22
  this._config = ctx.config;
22
- ctx.state.set("prevOi", parseFloat(snapshot.openInterest) || 0);
23
+ ctx.state.set("prevOi", snapshot.openInterest === null ? 0 : parseFloat(snapshot.openInterest) || 0);
23
24
  ctx.state.set("cascadeMode", false);
24
25
  ctx.state.set("cascadeTicks", 0);
25
26
  const p = this.params;
@@ -29,7 +30,12 @@ export class LiquidationMmStrategy {
29
30
  const params = this.params;
30
31
  const actions = [];
31
32
  actions.push({ type: "cancel_all" });
32
- const currentOi = parseFloat(snapshot.openInterest) || 0;
33
+ // Unknown OI (null): cascade detection is off — said once per run, never computed on a fake 0.
34
+ if (snapshot.openInterest === null && !this._oiUnknownLogged) {
35
+ this._oiUnknownLogged = true;
36
+ ctx.log(" [LIQUIDATION-MM] open interest unknown — OI cascade detection is off");
37
+ }
38
+ const currentOi = snapshot.openInterest === null ? 0 : parseFloat(snapshot.openInterest) || 0;
33
39
  const prevOi = ctx.state.get("prevOi") ?? currentOi;
34
40
  let cascadeMode = ctx.state.get("cascadeMode") ?? false;
35
41
  let cascadeTicks = ctx.state.get("cascadeTicks") ?? 0;
@@ -9,6 +9,7 @@ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from
9
9
  export declare class MomentumBreakoutStrategy implements Strategy {
10
10
  readonly name = "momentum-breakout";
11
11
  private _config;
12
+ private _noVolumeLogged;
12
13
  describe(): {
13
14
  description: string;
14
15
  params: ({
@@ -1,5 +1,5 @@
1
1
  import { registerStrategy } from "../strategy-registry.js";
2
- import { calculateATR, calculateSMA } from "../indicators.js";
2
+ import { calculateATR, calculateSMA, klineVolumes } from "../indicators.js";
3
3
  /**
4
4
  * Momentum breakout strategy.
5
5
  * Entry: price breaks above N-period high (buy) or below N-period low (sell)
@@ -10,6 +10,7 @@ import { calculateATR, calculateSMA } from "../indicators.js";
10
10
  export class MomentumBreakoutStrategy {
11
11
  name = "momentum-breakout";
12
12
  _config = {};
13
+ _noVolumeLogged = false;
13
14
  describe() {
14
15
  return {
15
16
  description: "Momentum breakout with volume confirmation and ATR trailing stop",
@@ -42,7 +43,14 @@ export class MomentumBreakoutStrategy {
42
43
  const closes = klines.map(k => parseFloat(k.close));
43
44
  const highs = klines.map(k => parseFloat(k.high));
44
45
  const lows = klines.map(k => parseFloat(k.low));
45
- const volumes = klines.map(k => parseFloat(k.volume));
46
+ // Unknown candle volume (null / non-numeric) cannot confirm a breakout: no entry, exits still run.
47
+ const known = klineVolumes(klines);
48
+ const volumeKnown = known !== null;
49
+ if (!volumeKnown && !this._noVolumeLogged) {
50
+ this._noVolumeLogged = true; // once per run, not every tick
51
+ ctx.log(" [BREAKOUT] no candle volume from the venue — volume confirmation unavailable, no new entries");
52
+ }
53
+ const volumes = known ?? klines.map(() => NaN);
46
54
  const price = snapshot.price;
47
55
  // Compute indicators
48
56
  const candles = klines.map(k => ({
@@ -61,7 +69,7 @@ export class MomentumBreakoutStrategy {
61
69
  const avgVolSma = calculateSMA(volumes, this.lookbackPeriod);
62
70
  const avgVolume = avgVolSma.length > 0 ? avgVolSma[avgVolSma.length - 1] : 0;
63
71
  const currentVolume = volumes[volumes.length - 1];
64
- const volumeConfirmed = avgVolume > 0 && currentVolume > avgVolume * this.volumeMultiplier;
72
+ const volumeConfirmed = volumeKnown && avgVolume > 0 && currentVolume > avgVolume * this.volumeMultiplier;
65
73
  const position = ctx.state.get("position");
66
74
  // If no position, look for breakout entry
67
75
  if (!position) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Spot-Perp Arbitrage Strategy
3
3
  *
4
- * Focused strategy: buy spot (HL or Lighter) + short perp on the BEST of 4 exchanges.
4
+ * Focused strategy: buy spot (HL or Lighter) + short perp on the best of the configured perp exchanges (`exchanges`).
5
5
  * No perp-perp logic. History-based entry/exit with short rotation.
6
6
  *
7
7
  * Strategy layer handles ONLY decisions (opportunity scanning, scoring, close/rotation decisions).