perp-cli 0.14.3 → 0.15.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 (311) hide show
  1. package/README.md +152 -13
  2. package/dist/agent-wallet/store.d.ts +3 -2
  3. package/dist/agent-wallet/store.js +2 -2
  4. package/dist/api/public/hyperliquid.js +4 -0
  5. package/dist/api/public/pacifica.js +83 -13
  6. package/dist/api/public/urls.d.ts +2 -0
  7. package/dist/api/public/urls.js +3 -0
  8. package/dist/arb/utils.d.ts +1 -2
  9. package/dist/arb/utils.js +1 -2
  10. package/dist/bot/apex/pulse.js +3 -0
  11. package/dist/bot/apex/radar.js +2 -1
  12. package/dist/bot/engine.d.ts +10 -1
  13. package/dist/bot/engine.js +14 -6
  14. package/dist/bot/indicators.d.ts +7 -0
  15. package/dist/bot/indicators.js +16 -0
  16. package/dist/bot/perp-order.d.ts +4 -4
  17. package/dist/bot/perp-order.js +5 -5
  18. package/dist/bot/quarantine.d.ts +1 -1
  19. package/dist/bot/quarantine.js +1 -1
  20. package/dist/bot/strategies/apex-strategy.js +3 -2
  21. package/dist/bot/strategies/engine-mm.js +6 -5
  22. package/dist/bot/strategies/funding-arb-strategy.js +2 -2
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
  24. package/dist/bot/strategies/funding-arb-utils.js +5 -5
  25. package/dist/bot/strategies/funding-auto.js +2 -2
  26. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  27. package/dist/bot/strategies/liquidation-mm.js +8 -2
  28. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  29. package/dist/bot/strategies/momentum-breakout.js +11 -3
  30. package/dist/bot/strategies/spot-perp-arb-strategy.js +2 -2
  31. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  32. package/dist/bot/strategies/spot-perp-executor.js +1 -1
  33. package/dist/bot/strategy-types.d.ts +2 -1
  34. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  35. package/dist/bot/tui/BotDashboard.js +1 -1
  36. package/dist/cache.js +12 -6
  37. package/dist/cli-program.d.ts +15 -0
  38. package/dist/cli-program.js +125 -5
  39. package/dist/commands/agent-import.d.ts +7 -0
  40. package/dist/commands/agent-import.js +30 -1
  41. package/dist/commands/agent.d.ts +6 -3
  42. package/dist/commands/agent.js +160 -12
  43. package/dist/commands/arb/index.js +4 -4
  44. package/dist/commands/arb-auto.js +21 -17
  45. package/dist/commands/arb.d.ts +12 -0
  46. package/dist/commands/arb.js +61 -143
  47. package/dist/commands/bot.js +1 -1
  48. package/dist/commands/builder.js +1 -1
  49. package/dist/commands/funds.js +113 -3
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +27 -7
  52. package/dist/commands/manage.js +23 -21
  53. package/dist/commands/market.js +167 -35
  54. package/dist/commands/options.d.ts +9 -0
  55. package/dist/commands/options.js +134 -0
  56. package/dist/commands/pacifica-earn.d.ts +5 -0
  57. package/dist/commands/pacifica-earn.js +468 -0
  58. package/dist/commands/pacifica-orders.d.ts +14 -0
  59. package/dist/commands/pacifica-orders.js +142 -0
  60. package/dist/commands/portfolio.js +32 -47
  61. package/dist/commands/rebalance.d.ts +1 -0
  62. package/dist/commands/rebalance.js +15 -0
  63. package/dist/commands/risk.d.ts +11 -2
  64. package/dist/commands/risk.js +39 -44
  65. package/dist/commands/trade.d.ts +1 -0
  66. package/dist/commands/trade.js +63 -58
  67. package/dist/commands/wallet.js +1 -1
  68. package/dist/config.d.ts +2 -2
  69. package/dist/config.js +15 -4
  70. package/dist/constants.js +6 -0
  71. package/dist/dashboard/server.js +1 -1
  72. package/dist/errors.d.ts +24 -1
  73. package/dist/errors.js +25 -3
  74. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  75. package/dist/exchanges/arcus/adapter.js +12 -12
  76. package/dist/exchanges/arcus/agent.d.ts +1 -1
  77. package/dist/exchanges/arcus/agent.js +1 -1
  78. package/dist/exchanges/arcus/client.js +1 -1
  79. package/dist/exchanges/arcus/signing.d.ts +2 -3
  80. package/dist/exchanges/arcus/signing.js +1 -1
  81. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  82. package/dist/exchanges/arcus/spot-chain.js +1 -1
  83. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  84. package/dist/exchanges/arcus/spot-router.js +2 -2
  85. package/dist/exchanges/arcus/spot.d.ts +2 -3
  86. package/dist/exchanges/arcus/spot.js +2 -3
  87. package/dist/exchanges/aster-spot.d.ts +3 -3
  88. package/dist/exchanges/aster-spot.js +6 -9
  89. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  90. package/dist/exchanges/aster-typed-data.js +1 -1
  91. package/dist/exchanges/aster.d.ts +4 -4
  92. package/dist/exchanges/aster.js +31 -13
  93. package/dist/exchanges/builder-fee.d.ts +1 -1
  94. package/dist/exchanges/builder-fee.js +13 -2
  95. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  96. package/dist/exchanges/canonical-symbols.js +26 -2
  97. package/dist/exchanges/derive/client.d.ts +37 -0
  98. package/dist/exchanges/derive/client.js +135 -0
  99. package/dist/exchanges/derive/options.d.ts +68 -0
  100. package/dist/exchanges/derive/options.js +360 -0
  101. package/dist/exchanges/extended/adapter.d.ts +82 -0
  102. package/dist/exchanges/extended/adapter.js +179 -0
  103. package/dist/exchanges/extended/client.d.ts +38 -0
  104. package/dist/exchanges/extended/client.js +88 -0
  105. package/dist/exchanges/extended/public.d.ts +130 -0
  106. package/dist/exchanges/extended/public.js +429 -0
  107. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  108. package/dist/exchanges/grvt/adapter.js +206 -0
  109. package/dist/exchanges/grvt/client.d.ts +25 -0
  110. package/dist/exchanges/grvt/client.js +86 -0
  111. package/dist/exchanges/grvt/public.d.ts +151 -0
  112. package/dist/exchanges/grvt/public.js +438 -0
  113. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  114. package/dist/exchanges/hyperliquid-spot.js +5 -5
  115. package/dist/exchanges/hyperliquid.d.ts +22 -12
  116. package/dist/exchanges/hyperliquid.js +80 -34
  117. package/dist/exchanges/index.d.ts +1 -1
  118. package/dist/exchanges/index.js +1 -1
  119. package/dist/exchanges/interface.d.ts +94 -6
  120. package/dist/exchanges/interface.js +16 -0
  121. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  122. package/dist/exchanges/ioc-outcome.js +1 -1
  123. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  124. package/dist/exchanges/legacy-trading-v2.js +7 -2
  125. package/dist/exchanges/lighter-public.d.ts +2 -2
  126. package/dist/exchanges/lighter-public.js +2 -2
  127. package/dist/exchanges/lighter-signer.d.ts +1 -1
  128. package/dist/exchanges/lighter-signer.js +1 -1
  129. package/dist/exchanges/lighter-spot.d.ts +3 -4
  130. package/dist/exchanges/lighter-spot.js +6 -7
  131. package/dist/exchanges/lighter.d.ts +11 -11
  132. package/dist/exchanges/lighter.js +28 -14
  133. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  134. package/dist/exchanges/ondo/adapter.js +16 -14
  135. package/dist/exchanges/ondo/agent.d.ts +1 -1
  136. package/dist/exchanges/ondo/agent.js +1 -1
  137. package/dist/exchanges/ondo/client.d.ts +1 -1
  138. package/dist/exchanges/ondo/client.js +2 -2
  139. package/dist/exchanges/ondo/public.d.ts +8 -1
  140. package/dist/exchanges/ondo/public.js +13 -2
  141. package/dist/exchanges/ondo/spot.d.ts +2 -2
  142. package/dist/exchanges/ondo/spot.js +3 -3
  143. package/dist/exchanges/options-interface.d.ts +158 -0
  144. package/dist/exchanges/options-interface.js +17 -0
  145. package/dist/exchanges/options-venues.d.ts +12 -0
  146. package/dist/exchanges/options-venues.js +35 -0
  147. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  148. package/dist/exchanges/pacifica-atomic.js +440 -0
  149. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  150. package/dist/exchanges/pacifica-decimal.js +51 -0
  151. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  152. package/dist/exchanges/pacifica-presigned.js +141 -0
  153. package/dist/exchanges/pacifica-print.d.ts +200 -0
  154. package/dist/exchanges/pacifica-print.js +417 -0
  155. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  156. package/dist/exchanges/pacifica-spot.js +3 -3
  157. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  158. package/dist/exchanges/pacifica-typed-data.js +1 -1
  159. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  160. package/dist/exchanges/pacifica-vaults.js +264 -0
  161. package/dist/exchanges/pacifica.d.ts +32 -5
  162. package/dist/exchanges/pacifica.js +114 -48
  163. package/dist/exchanges/precision.d.ts +3 -1
  164. package/dist/exchanges/precision.js +9 -2
  165. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  166. package/dist/exchanges/qfex/adapter.js +32 -12
  167. package/dist/exchanges/qfex/agent.d.ts +1 -1
  168. package/dist/exchanges/qfex/agent.js +1 -1
  169. package/dist/exchanges/qfex/client.d.ts +1 -1
  170. package/dist/exchanges/qfex/client.js +1 -1
  171. package/dist/exchanges/qfex/errors.d.ts +1 -1
  172. package/dist/exchanges/qfex/errors.js +2 -2
  173. package/dist/exchanges/qfex/public.d.ts +3 -3
  174. package/dist/exchanges/qfex/public.js +3 -3
  175. package/dist/exchanges/qfex/signing.d.ts +2 -2
  176. package/dist/exchanges/qfex/signing.js +4 -5
  177. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  178. package/dist/exchanges/qfex/ws-transport.js +3 -3
  179. package/dist/exchanges/rate-limiter.d.ts +1 -1
  180. package/dist/exchanges/rate-limiter.js +5 -2
  181. package/dist/exchanges/registry.d.ts +1 -1
  182. package/dist/exchanges/registry.js +1 -1
  183. package/dist/exchanges/risex/adapter.d.ts +148 -0
  184. package/dist/exchanges/risex/adapter.js +796 -0
  185. package/dist/exchanges/risex/agent.d.ts +154 -0
  186. package/dist/exchanges/risex/agent.js +384 -0
  187. package/dist/exchanges/risex/client.d.ts +47 -0
  188. package/dist/exchanges/risex/client.js +174 -0
  189. package/dist/exchanges/risex/nonce.d.ts +43 -0
  190. package/dist/exchanges/risex/nonce.js +212 -0
  191. package/dist/exchanges/risex/public.d.ts +64 -0
  192. package/dist/exchanges/risex/public.js +150 -0
  193. package/dist/exchanges/risex/session.d.ts +71 -0
  194. package/dist/exchanges/risex/session.js +171 -0
  195. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  196. package/dist/exchanges/risex/typed-data.js +240 -0
  197. package/dist/exchanges/risex/units.d.ts +29 -0
  198. package/dist/exchanges/risex/units.js +104 -0
  199. package/dist/exchanges/slippage.d.ts +1 -1
  200. package/dist/exchanges/slippage.js +1 -1
  201. package/dist/exchanges/spot-factory.d.ts +3 -3
  202. package/dist/exchanges/spot-factory.js +7 -7
  203. package/dist/exchanges/spot-interface.d.ts +5 -5
  204. package/dist/exchanges/spot-interface.js +2 -2
  205. package/dist/exchanges/spot-result.d.ts +3 -3
  206. package/dist/exchanges/spot-result.js +3 -3
  207. package/dist/exchanges/trading-v2.d.ts +3 -3
  208. package/dist/exchanges/trading-v2.js +9 -6
  209. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  210. package/dist/exchanges/txflow/adapter.js +118 -0
  211. package/dist/exchanges/txflow/client.d.ts +47 -0
  212. package/dist/exchanges/txflow/client.js +153 -0
  213. package/dist/exchanges/txflow/public.d.ts +76 -0
  214. package/dist/exchanges/txflow/public.js +284 -0
  215. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  216. package/dist/exchanges/unknown-symbol.js +51 -0
  217. package/dist/exchanges/variational/adapter.d.ts +71 -0
  218. package/dist/exchanges/variational/adapter.js +137 -0
  219. package/dist/exchanges/variational/client.d.ts +21 -0
  220. package/dist/exchanges/variational/client.js +66 -0
  221. package/dist/exchanges/variational/public.d.ts +126 -0
  222. package/dist/exchanges/variational/public.js +430 -0
  223. package/dist/exchanges/venue-types.d.ts +6 -7
  224. package/dist/exchanges/venue-types.js +2 -2
  225. package/dist/exchanges/venue.d.ts +51 -8
  226. package/dist/exchanges/venue.js +54 -4
  227. package/dist/exchanges/venues/arcus.d.ts +1 -2
  228. package/dist/exchanges/venues/arcus.js +5 -4
  229. package/dist/exchanges/venues/aster.d.ts +2 -2
  230. package/dist/exchanges/venues/aster.js +6 -4
  231. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  232. package/dist/exchanges/venues/create-helpers.js +3 -3
  233. package/dist/exchanges/venues/extended.d.ts +21 -0
  234. package/dist/exchanges/venues/extended.js +96 -0
  235. package/dist/exchanges/venues/grvt.d.ts +30 -0
  236. package/dist/exchanges/venues/grvt.js +98 -0
  237. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  238. package/dist/exchanges/venues/hyperliquid.js +10 -8
  239. package/dist/exchanges/venues/index.d.ts +7 -2
  240. package/dist/exchanges/venues/index.js +8 -2
  241. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  242. package/dist/exchanges/venues/lighter-family.js +9 -11
  243. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  244. package/dist/exchanges/venues/lighter-rh.js +7 -5
  245. package/dist/exchanges/venues/lighter.d.ts +1 -1
  246. package/dist/exchanges/venues/lighter.js +5 -3
  247. package/dist/exchanges/venues/ondo.d.ts +1 -1
  248. package/dist/exchanges/venues/ondo.js +5 -3
  249. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  250. package/dist/exchanges/venues/pacifica.js +6 -4
  251. package/dist/exchanges/venues/qfex.d.ts +1 -1
  252. package/dist/exchanges/venues/qfex.js +4 -2
  253. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  254. package/dist/exchanges/venues/risex-release.js +14 -0
  255. package/dist/exchanges/venues/risex.d.ts +31 -0
  256. package/dist/exchanges/venues/risex.js +116 -0
  257. package/dist/exchanges/venues/txflow.d.ts +18 -0
  258. package/dist/exchanges/venues/txflow.js +77 -0
  259. package/dist/exchanges/venues/variational.d.ts +21 -0
  260. package/dist/exchanges/venues/variational.js +85 -0
  261. package/dist/execution-log.d.ts +1 -1
  262. package/dist/funding/legacy-public.d.ts +1 -1
  263. package/dist/funding/legacy-public.js +9 -4
  264. package/dist/funding/normalize.d.ts +1 -1
  265. package/dist/funding/normalize.js +1 -1
  266. package/dist/funding/rates.d.ts +44 -4
  267. package/dist/funding/rates.js +56 -12
  268. package/dist/index.js +7 -6
  269. package/dist/jobs.d.ts +1 -0
  270. package/dist/jobs.js +25 -1
  271. package/dist/landing.d.ts +2 -0
  272. package/dist/landing.js +1 -1
  273. package/dist/mcp-pacifica-earn.d.ts +9 -0
  274. package/dist/mcp-pacifica-earn.js +58 -0
  275. package/dist/mcp-server.js +113 -59
  276. package/dist/pacifica/client.d.ts +67 -4
  277. package/dist/pacifica/client.js +170 -8
  278. package/dist/pacifica/rate-limit.d.ts +144 -0
  279. package/dist/pacifica/rate-limit.js +267 -0
  280. package/dist/pacifica/types/order.d.ts +2 -1
  281. package/dist/plan-executor.js +7 -2
  282. package/dist/rebalance.js +1 -1
  283. package/dist/risk.d.ts +2 -1
  284. package/dist/risk.js +2 -4
  285. package/dist/settings.d.ts +32 -3
  286. package/dist/signer/ows-evm.d.ts +1 -1
  287. package/dist/signer/ows-evm.js +1 -1
  288. package/dist/smart-order.d.ts +1 -1
  289. package/dist/smart-order.js +1 -1
  290. package/dist/strategies/funding-arb.d.ts +1 -1
  291. package/dist/strategies/funding-arb.js +1 -1
  292. package/dist/trade-validator.js +2 -2
  293. package/dist/utils.d.ts +12 -3
  294. package/dist/utils.js +11 -3
  295. package/dist/venue-health.d.ts +40 -0
  296. package/dist/venue-health.js +75 -0
  297. package/dist/withdraw-gate.d.ts +1 -1
  298. package/dist/withdraw-gate.js +1 -1
  299. package/dist/ws/index.d.ts +959 -0
  300. package/dist/ws/index.js +4295 -0
  301. package/dist/ws/node.d.ts +975 -0
  302. package/dist/ws/node.js +4366 -0
  303. package/dist/ws/pacifica-instrument.d.ts +8 -0
  304. package/dist/ws/pacifica-instrument.js +8 -0
  305. package/dist/ws-cli/credentials.d.ts +62 -0
  306. package/dist/ws-cli/credentials.js +126 -0
  307. package/package.json +58 -10
  308. package/skills/perp-cli/SKILL.md +62 -9
  309. package/skills/perp-cli/references/agent-operations.md +7 -0
  310. package/skills/perp-cli/references/commands.md +76 -5
  311. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -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
@@ -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);
@@ -1099,7 +1099,7 @@ export class FundingArbStrategy {
1099
1099
  const transferAmt = Math.ceil(matched.notional * 1.02); // slight buffer for price movement
1100
1100
  if (await transferUsdcToSpot(spotAdapter, transferAmt))
1101
1101
  ctx.log(` [ARB] Transferred $${transferAmt} to spot`);
1102
- // Step 2: Buy spot — the hedge is sized to what actually filled (§12.4 #5).
1102
+ // Step 2: Buy spot — the hedge is sized to what actually filled.
1103
1103
  let hedgeSize;
1104
1104
  let spotFill;
1105
1105
  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
  */
@@ -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
@@ -172,7 +172,7 @@ function hasPerSymbolFundingHours(adapter) {
172
172
  /**
173
173
  * Funding basis (hours one quoted rate covers) for a market: the market's own
174
174
  * 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).
175
+ * History timestamp spacing is settlement timing, never the basis.
176
176
  * @returns null when the venue publishes per-symbol values and this symbol has none
177
177
  */
178
178
  async function marketFundingHours(adapter, exchangeName, symbol) {
@@ -217,7 +217,7 @@ export async function scoreFunding(adapter, symbol, exchangeName, periods, cache
217
217
  export async function fetchRates(adapter, exchangeName) {
218
218
  const markets = await adapter.getMarkets();
219
219
  const withRates = markets.filter(m => m.fundingRate != null);
220
- // fundingHours = basis (§10.13 #7): market value first, then the venue definition.
220
+ // fundingHours = basis: market value first, then the venue definition.
221
221
  // Per-symbol venues (Aster) load their whole table in one call; symbols it
222
222
  // does not list stay null and are skipped below.
223
223
  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) {
@@ -285,7 +285,7 @@ export class SpotPerpArbStrategy {
285
285
  ctx.state.set("spaPositions", remaining);
286
286
  ctx.state.set("arbPositions", remaining.length);
287
287
  }
288
- // ── Rotation check (only to perp venues that match the spot leg, §12.4 #6) ──
288
+ // ── Rotation check (only to perp venues that match the spot leg) ──
289
289
  const perpMarketsCache = new Map();
290
290
  const allowedBySpotVenue = new Map();
291
291
  const currentPositions = ctx.state.get("spaPositions");
@@ -375,7 +375,7 @@ export class SpotPerpArbStrategy {
375
375
  const spotAdapter = await getBotSpotAdapter(spotExName, spotExAdapter);
376
376
  if (!spotAdapter)
377
377
  continue;
378
- // Canonical symbol + price guard (§12.4 #6): base → perp venues it may hedge on.
378
+ // Canonical symbol + price guard: base → perp venues it may hedge on.
379
379
  const allowedByBase = await this.spotPerpVenues(spotExName, await spotAdapter.getSpotMarkets(), adapters, perpMarketsCache, ctx);
380
380
  for (const [base, allowed] of allowedByBase) {
381
381
  // HL spot: skip tokens not in whitelist
@@ -26,7 +26,7 @@ export type ExecutionResult = {
26
26
  /**
27
27
  * closed = position fully closed; remainingSize = partially closed, this size is still open (hedged).
28
28
  * manual = the outcome is unknown / unverified or a leg is unhedged: the caller must quarantine
29
- * the position and never retry it automatically (design §12.3).
29
+ * the position and never retry it automatically.
30
30
  */
31
31
  export type CloseResult = {
32
32
  closed: true;
@@ -140,7 +140,7 @@ export class SpotPerpExecutor {
140
140
  await this.returnToPerp(spotAdapter, actualTransferred, spotExchange, log);
141
141
  return { success: false, error: `pre-trade balance: ${msg}` };
142
142
  }
143
- // Step 1: Buy spot — the hedge is sized to what actually filled (§12.4 #5)
143
+ // Step 1: Buy spot — the hedge is sized to what actually filled
144
144
  let fill;
145
145
  try {
146
146
  fill = await spotMarketFill(spotAdapter, symbol, "buy", matched.size, spotStep);
@@ -10,7 +10,8 @@ export interface EnrichedSnapshot extends MarketSnapshot {
10
10
  bids: [string, string][];
11
11
  asks: [string, string][];
12
12
  };
13
- openInterest: string;
13
+ /** Base-asset open interest; null = not read / not published (never "0" as a stand-in). */
14
+ openInterest: string | null;
14
15
  }
15
16
  export type StrategyAction = {
16
17
  type: "place_order";
@@ -28,7 +28,7 @@ export interface BotTuiState {
28
28
  exchange: string;
29
29
  price: number;
30
30
  volume24h: number;
31
- openInterest: string;
31
+ openInterest: string | null;
32
32
  fundingRate: number;
33
33
  volatility24h: number;
34
34
  positions: Position[];
@@ -78,7 +78,7 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
78
78
  const askOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "sell");
79
79
  const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
80
80
  const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
81
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
81
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(totalEquity) })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsx(Text, { color: "white", children: Number.isFinite(state.volatility24h) ? `${state.volatility24h.toFixed(1)}%` : "unknown" })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest ?? "-" })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsx(Text, { bold: true, color: "white", children: usd(state.equity) }), _jsxs(Text, { color: "gray", children: [" (peak: ", usd(state.peakEquity), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsx(Text, { bold: true, color: drawdown === null ? "gray" : drawdown > 0 ? "red" : "green", children: drawdown === null ? "unknown" : `${usd(drawdown)} (${drawdownPct.toFixed(1)}%)` })] }), _jsxs(Text, { children: ["Orders: ", _jsx(Text, { bold: true, color: "white", children: state.ordersPlaced }), _jsx(Text, { color: "gray", children: " | Fills: " }), _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), eb.equity === null ? (_jsxs(Text, { color: "red", children: ["unavailable", eb.error ? ` (${eb.error})` : ""] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsx(Text, { color: "gray", children: eb.available === null ? "unavailable" : `$${parseFloat(eb.available).toFixed(2)}` })] }))] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), positionErrors.map((pe, i) => (_jsxs(Text, { color: "red", children: [" ", pe.exchange, ": unavailable (", pe.error, ")"] }, `err-${i}`))), state.positions.length === 0 ? (positionErrors.length === 0 && _jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
82
82
  const upnl = parseFloat(p.unrealizedPnl);
83
83
  return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsx(Text, { color: isNaN(upnl) ? "gray" : pnlColor(upnl), children: isNaN(upnl) ? "$—" : `$${upnl.toFixed(2)}` })] }, i));
84
84
  })] }))] }), openOrdersError !== null ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Open orders unavailable (", openOrdersError, ")"] }) })) : (_jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: state.strategyState.fundingTotal !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Funding & Arb" }), _jsxs(Box, { flexDirection: "row", marginLeft: 2, children: [_jsx(Text, { color: "gray", children: "Funding Income: " }), _jsx(Text, { color: "white", children: String(state.strategyState.fundingTotal) }), _jsx(Text, { color: "gray", children: " Positions: " }), _jsx(Text, { color: "white", children: String(state.strategyState.arbPositions ?? 0) })] }), Array.isArray(state.strategyState.arbPositionDetails) &&
package/dist/cache.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync } from "fs";
8
8
  import { join } from "path";
9
9
  import { tmpdir } from "os";
10
+ import { randomBytes } from "crypto";
10
11
  const CACHE_DIR = join(tmpdir(), "perp-cli-cache");
11
12
  // TTL presets
12
13
  export const TTL_ACCOUNT = 15_000; // 15s — balance, positions, orders (ticks are 30s; 15s ensures same-tick dedup)
@@ -41,16 +42,21 @@ export function getCached(key) {
41
42
  }
42
43
  export function setCached(key, data, ttlMs) {
43
44
  ensureDir();
45
+ const file = join(CACHE_DIR, `${safeKey(key)}.json`);
46
+ // Unique per writer: concurrent processes writing the same key must not share one tmp file
47
+ // (a rename of another writer's half-written tmp would publish a partial entry).
48
+ const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
44
49
  try {
45
50
  const entry = { ts: Date.now(), ttl: ttlMs, data };
46
- const content = JSON.stringify(entry);
47
- const file = join(CACHE_DIR, `${safeKey(key)}.json`);
48
- const tmp = file + ".tmp";
49
- writeFileSync(tmp, content);
51
+ writeFileSync(tmp, JSON.stringify(entry));
50
52
  renameSync(tmp, file);
51
53
  }
52
54
  catch {
53
- // cache write failure is non-fatal
55
+ // cache write failure is non-fatal; never leave this writer's tmp file behind
56
+ try {
57
+ unlinkSync(tmp);
58
+ }
59
+ catch { /* not written */ }
54
60
  }
55
61
  }
56
62
  /**
@@ -94,7 +100,7 @@ export function invalidateCache(keyPrefix) {
94
100
  const files = readdirSync(CACHE_DIR);
95
101
  for (const f of files) {
96
102
  if (!f.endsWith(".json"))
97
- continue;
103
+ continue; // also skips in-flight "<key>.json.<pid>.<rand>.tmp" files
98
104
  if (!prefix || f.startsWith(prefix)) {
99
105
  try {
100
106
  unlinkSync(join(CACHE_DIR, f));
@@ -28,8 +28,23 @@ export declare function landingData(program: Command, version: string): {
28
28
  name: string;
29
29
  description: string;
30
30
  }[];
31
+ venues: {
32
+ id: string;
33
+ displayName: string;
34
+ roles: string[];
35
+ apiStatus: "official" | "unofficial";
36
+ note: string | null;
37
+ }[];
31
38
  };
32
39
  export declare function buildProgram(opts: {
33
40
  defaultExchange: string;
34
41
  version: string;
35
42
  }): CliProgram;
43
+ /**
44
+ * The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
45
+ * `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
46
+ * (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
47
+ * before the dry-run guard and before any action, so a dry-run plan can never present a gated
48
+ * venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
49
+ */
50
+ export declare function assertCommandVenueRole(actionCommand: Command, exchangeOpt: string): void;