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.
- package/README.md +152 -13
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +2 -2
- package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
- package/dist/bot/strategies/funding-arb-utils.js +5 -5
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.js +2 -2
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +125 -5
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.js +167 -35
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +63 -58
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/dashboard/server.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +4 -4
- package/dist/exchanges/aster.js +31 -13
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -12
- package/dist/exchanges/hyperliquid.js +80 -34
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +94 -6
- package/dist/exchanges/interface.js +16 -0
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +148 -0
- package/dist/exchanges/risex/adapter.js +796 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +7 -7
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +10 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +6 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +116 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +1 -1
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +12 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
|
@@ -6,7 +6,7 @@ All commands support `--json` for structured output. Always use `--json` when ca
|
|
|
6
6
|
|
|
7
7
|
`--json` (required for agents) | `-e <venue>` | `-n mainnet|testnet` | `--dry-run` (only commands whose `--help` says "supports --dry-run"; others refuse with `INVALID_PARAMS`) | `--fields <f1,f2>` | `--ndjson` | `-w, --wallet <name>` | `--ows <name>` (OWS wallet) | `--no-agent` (skip agent-key routing) | `--no-builder` | `--dex <name>` (HIP-3, Hyperliquid)
|
|
8
8
|
|
|
9
|
-
Venues (`-e`, id | aliases): `pacifica|pac`, `hyperliquid|hl`, `lighter|lt`, `lighter-rh|lrh|rh`, `aster|ast`, `ondo|ondo-perps`, `arcus`, `qfex|qf
|
|
9
|
+
Venues (`-e`, id | aliases): `pacifica|pac`, `hyperliquid|hl`, `lighter|lt`, `lighter-rh|lrh|rh`, `aster|ast`, `ondo|ondo-perps`, `arcus`, `qfex|qf`, `risex|rise` (READ-ONLY in this release — market data + account reads; trading is planned for a later release, every trade / agent / bot write refuses with CAPABILITY_UNSUPPORTED), `txflow|txf` (market data only, UNOFFICIAL API — every write refuses with CAPABILITY_UNSUPPORTED), `variational|var|omni` (market data only, official API, RFQ venue: `market book` = indicative RFQ quotes, `market funding` = current terms — every write refuses with CAPABILITY_UNSUPPORTED), `grvt|grv` (market data only in this version, official API — every write refuses with CAPABILITY_UNSUPPORTED), `extended|ext|x10` (Extended on Starknet, official API, market data only in this version — every write refuses with CAPABILITY_UNSUPPORTED). Default = `settings.defaultExchange`, else `pacifica` — always pass `-e`.
|
|
10
10
|
`-k <key>` is rejected (keys only via stdin / OWS vault). `--passphrase` is deprecated → `OWS_PASSPHRASE` env or stdin.
|
|
11
11
|
|
|
12
12
|
## Market Data (read-only, safe)
|
|
@@ -91,6 +91,16 @@ perp --json -e <EX> trade scale-in <SYMBOL> <SIDE> --levels '<P1>:<PCT>,<P2>:<PC
|
|
|
91
91
|
perp --json -e <EX> trade pnl-track # real-time PnL tracker
|
|
92
92
|
perp --json -e <EX> trade twap <SYMBOL> <SIDE> <SIZE> <DURATION> # Pacifica: seconds (native), HL: minutes (native, min $100); others client-side: --background [--slices <n>]
|
|
93
93
|
|
|
94
|
+
# Pacifica Atomic basket (pacifica only; other venues → CAPABILITY_UNSUPPORTED). Legs SYMBOL[:WEIGHT] (≤ 5 per side, each market once);
|
|
95
|
+
# --usd is split by weight at each mark, floored to the lot, each leg ≥ the market's USD minimum. ONE POST /orders/batch of market
|
|
96
|
+
# orders — NOT all-or-nothing: each leg fills or fails on its own (ORDER_PARTIAL / ORDER_REJECTED / ORDER_PENDING per basket).
|
|
97
|
+
# --reduce-only: --long legs must close shorts, --short legs must close longs (checked before sending). Supports --dry-run (plan + unsigned body).
|
|
98
|
+
perp --json -e pacifica trade atomic --long BTC:50 --short ETH:50 --usd 1000 [--reduce-only] [--slippage-bps <n>] [--force]
|
|
99
|
+
# A leg needs notional ≥ the market minimum × (1 + slippage bound). Legs without an order_id in the ack are traced by their client_order_id.
|
|
100
|
+
# A batch failure other than HTTP 400 / 429 (timeout, 5xx, unreadable answer) → ORDER_PENDING and EVERY leg is quarantined.
|
|
101
|
+
# Pacifica Chase / OCO / Trailing Stop / Swarm / Trigger Swarm / Conditional Market|Limit: no documented API / SDK path (as of 2026-10-19) —
|
|
102
|
+
# nearest here: trade twap (≈ Swarm, split market execution), trade tpsl (≈ OCO bracket exit).
|
|
103
|
+
|
|
94
104
|
# Spot helpers (any venue with spot)
|
|
95
105
|
perp --json -e <EX> trade spot-buy <SYMBOL> <SIZE>
|
|
96
106
|
perp --json -e <EX> trade spot-sell <SYMBOL> <SIZE>
|
|
@@ -181,6 +191,61 @@ Encoding formula: `enc = 10 * outcome + side`; asset id = `100,000,000 + enc`.
|
|
|
181
191
|
|
|
182
192
|
Settlement at `expiryMs` is venue-side: winning side → 1 quote token per share, losing → 0. Decide before expiry whether to self-close or let the venue settle.
|
|
183
193
|
|
|
194
|
+
## Options market data (read-only, `-e derive`)
|
|
195
|
+
|
|
196
|
+
Derive (derive.xyz, formerly Lyra) options through its official public v3 API — keyless, no orders, no account. Options venues are separate from the perp venues (not in `perp health`, arb or `market`). JSON objects carry `schema: 1`; decimals are strings; unknown is `null` (empty book side → `bid`/`ask` `null`; IV the venue could not compute → `null`; 24h high/low `null` without trades).
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
perp --json -e derive options underlyings # underlyings with options (spot price)
|
|
200
|
+
perp --json -e derive options expiries ETH # active expiries: expiry (YYYY-MM-DD UTC), expiryMs, strikes, instruments
|
|
201
|
+
perp --json -e derive options chain ETH # nearest expiry (expirySelection: "nearest"), every strike
|
|
202
|
+
perp --json -e derive options chain ETH --expiry 2026-12-25 --strikes 10 # 10 strikes closest to ATM (forward, else index)
|
|
203
|
+
perp --json -e derive options quote ETH-20261225-3500-P # one option
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- Chain row: `{strike, call, put}`; each side is an option quote: `bid`/`ask` `{price, size}`, `mark`, `index`, `forward`, `discountFactor`, `iv {mark, bid, ask}`, `greeks {delta, gamma, theta, vega, rho}` (venue units, verbatim), `openInterest` (contracts), `stats24h {contracts, notional, premium, trades, high, low}`, `timestampMs`. Chain level: `index`, `forward` (both of the newest ticker of the expiry, not an average), `atm {strike, reference, price}`, `strikesListed`, `missingTickers`.
|
|
207
|
+
- Errors: unknown underlying / expiry / instrument → `SYMBOL_NOT_FOUND` (remediation names the listing command / active expiries); a perp name in `quote` → `INVALID_PARAMS`; an active option with no live ticker (activation / expiry race) → `EXCHANGE_ERROR` (retry); other Derive rejections → `EXCHANGE_ERROR`; no `-e` or a perp venue → `CAPABILITY_UNSUPPORTED`; Derive's rate limit → `RATE_LIMITED` (retryable).
|
|
208
|
+
- Cost: `expiries` 1 request, `chain` 2 (instruments + ONE tickers call for the expiry), `quote` 2. Derive's public budget is 5 requests/s per IP.
|
|
209
|
+
- `-n testnet` reads `testnet.api.derive.xyz`. MCP: `get_option_expiries`, `get_option_chain`, `get_option_quote` (`venue: "derive"`).
|
|
210
|
+
|
|
211
|
+
## Pacifica Print (`perp print`) and vaults (`perp vault`)
|
|
212
|
+
|
|
213
|
+
Pacifica-only products: these commands always use Pacifica (`-e <other venue>` → `INVALID_PARAMS`). Reads are unauthenticated GETs and never sign; `--account <address>` reads any account without a key. Every write supports `--dry-run`, which prints the exact message to sign and the body (`signature: null`), never signs and sends nothing. `--account` on a write only plans (with `--dry-run`); a live write with `--account` is refused.
|
|
214
|
+
|
|
215
|
+
**Signer rule (perp-cli, stricter than the venue):** `vault deposit` is **master-only** (like `funds withdraw pacifica`) — an agent key must never be able to move the balance into a vault someone else manages; with only an agent key on the machine it is `NO_SIGNER_AVAILABLE`. `vault withdraw` (pays only into the own account), `print deposit` and `print cancel` are signed by the active signer (the agent key when approved, else the master). `wallet manage lake deposit|withdraw` were replaced by these commands (they now refuse with a pointer); `wallet manage lake create --initial-deposit` is refused with an agent key.
|
|
216
|
+
|
|
217
|
+
**Idempotency:** every deposit / withdraw sends a fresh `idempotency_key`. After `ORDER_PENDING` never resubmit with a new key; the only safe retry is the same command with `--idempotency-key <the reported key>` (the venue answers a duplicate with HTTP 409; how long it remembers a key is unverified). `error.details.before` holds the pre-write state (Print order count / vault shares) to compare with `print positions` / `vault positions`.
|
|
218
|
+
|
|
219
|
+
### Print — strike orders that earn a premium every cycle
|
|
220
|
+
A Print order locks a USDC deposit (min 10) as margin at a target price: **long** = target BELOW mark, **short** = target ABOVE mark, 0.5% … 10% / 20% / 40% from mark for a 24-hour / 7-day / 30-day cycle. Leverage 1 … product max (`print games`). The premium is credited to the order at the START of each cycle. **While resting** (before a fill) it is evaluated only at the END of each cycle: it fills AT the target (size = deposit × leverage ÷ target), rolls to a new target at the same % distance, or is liquidated; max loss = deposit + premiums. **After a fill** it is an ordinary perp position in the main account — continuous mark-price liquidation, it nets with an existing position in the same market and may be cross-margined; read it with `perp -e pacifica account positions`. Product / pool deposit caps are totals across all users (the venue refuses when full). `--cycles 1…1000` caps the cycles (default: roll until cancelled). A cancel takes effect only at the end of the running cycle — funds stay locked until then (up to 30 days on BTC_1M).
|
|
221
|
+
```bash
|
|
222
|
+
perp --json -e pacifica print games # products: cycle, max lev, caps, allowed target range
|
|
223
|
+
perp --json -e pacifica print quote BTC_24H long 100 --strike 79000 --leverage 10 [--cycles 7]
|
|
224
|
+
# read-only preview: notional, size on fill, docsLiquidationPrice, maxLoss, lock; estimatedPremium is null
|
|
225
|
+
# (the venue's premium preview is a signed request — never called on a read path)
|
|
226
|
+
perp --json -e pacifica print positions [--account <addr>] # current orders: printing | cancelling | filled | liquidated | cancelled | completed | ended
|
|
227
|
+
perp --json -e pacifica print history [--account <addr>] [--limit N] [--cursor C]
|
|
228
|
+
perp --json -e pacifica print events <printAddress> [--limit N] [--cursor C] # created / premium / end_requested / position_opened / liquidated
|
|
229
|
+
perp --json -e pacifica --dry-run print deposit BTC_24H long 100 --strike 79000 --leverage 10 --cycles 7
|
|
230
|
+
perp --json -e pacifica print deposit BTC_24H long 100 --strike 79000 --leverage 10 --cycles 7 # POST /game/deposit (deposit_to_game)
|
|
231
|
+
perp --json -e pacifica --dry-run print cancel <printAddress> # UNOFFICIAL: POST /game/end (end_game) is not in the API docs (apiStatus: "unofficial")
|
|
232
|
+
perp --json -e pacifica print cancel <printAddress> --unverified-endpoint # live run needs the flag; only a `printing` order
|
|
233
|
+
```
|
|
234
|
+
- `docsLiquidationPrice` is the docs formula (margin fully lost) — an outer bound, not the trigger: a liquidation was observed earlier in one live case (2026-10-16, ZEC_24H short at mark 1216.84 vs formula ≈ 1269); the venue's trigger is unpublished. For a 1× long the formula gives ≤ 0: `null` = unknown, not "no liquidation". Show the user `maxLoss`.
|
|
235
|
+
|
|
236
|
+
### Vaults (API: lakes)
|
|
237
|
+
A vault is a Pacifica account traded by its manager; LPs get shares at NAV (lp_balance ÷ lp_shares). Min deposit 10 USDC; `deposit_cap` caps the LP balance; the manager takes `manager_profit_share` above the high-water mark. A withdrawal redeems shares into YOUR OWN trading balance (the API has no destination) only after `deposit_min_duration_ms` since your LAST deposit (each deposit restarts the lock for your whole balance), inside the withdrawal window when one is set (open while now_s mod window < duration), and from the vault's free balance (a fully deployed vault may pay partially). APY is not published (`apy: null`).
|
|
238
|
+
```bash
|
|
239
|
+
perp --json -e pacifica vault list [--account <addr>] # TVL, NAV/share, fee, lock-up, window, config
|
|
240
|
+
perp --json -e pacifica vault info <address|nickname> # + live equity, positions, deposit whitelist (UNOFFICIAL endpoint, advisory), window open now?
|
|
241
|
+
# a nickname must match exactly ONE vault (case-insensitive) — duplicates exist live ("Pacifica Vault" / "Pacifica vault");
|
|
242
|
+
# ambiguity → INVALID_PARAMS listing every candidate's address / manager / TVL. Writes: prefer the ADDRESS.
|
|
243
|
+
perp --json -e pacifica vault positions [--account <addr>] # your LP (or manager) shares and value
|
|
244
|
+
perp --json -e pacifica --dry-run vault deposit <vault> <amount> # POST /lake/deposit (deposit_to_lake) — MASTER-ONLY; refused when the LP NAV is zero / undefined
|
|
245
|
+
perp --json -e pacifica --dry-run vault withdraw <vault> <shares>|--all # POST /lake/withdraw (withdraw_from_lake)
|
|
246
|
+
```
|
|
247
|
+
MCP (read-only): `pacifica_print_products`, `pacifica_print_quote`, `pacifica_print_positions`, `pacifica_vaults`. Print / vault writes are CLI-only.
|
|
248
|
+
|
|
184
249
|
## Funds (deposit, withdraw, transfer, bridge, rebalance)
|
|
185
250
|
|
|
186
251
|
All fund movement lives under `perp funds`.
|
|
@@ -201,6 +266,8 @@ perp --json funds deposit lighter-rh <AMOUNT> # USDG on Robinhood
|
|
|
201
266
|
# Ondo Perps: NO API withdrawals — `funds withdraw ondo` → CAPABILITY_UNSUPPORTED; withdraw in the Ondo web app
|
|
202
267
|
perp --json funds withdraw arcus <AMOUNT> # USDG → the account owner (min 1)
|
|
203
268
|
perp --json funds deposit qfex # prints the USDC / Arbitrum One deposit address (sends nothing)
|
|
269
|
+
# RISEx: deposits / withdrawals are app-only — `funds deposit|withdraw risex` → CAPABILITY_UNSUPPORTED (app.rise.trade)
|
|
270
|
+
# RISEx is read-only in this release (trading later): `funds faucet risex` → CAPABILITY_UNSUPPORTED too
|
|
204
271
|
# QFEX: withdrawals disabled in perp-cli for now — `funds withdraw qfex` → CAPABILITY_UNSUPPORTED; withdraw in the QFEX web app
|
|
205
272
|
perp --json -e qfex funds transfer <AMOUNT> primary --from <account-uuid> # between your QFEX accounts
|
|
206
273
|
perp --json funds deposit aster <AMOUNT> [--chain bsc|arbitrum] [--asset USDT|USDC] # vault approve + deposit (mainnet; default BNB Chain USDT)
|
|
@@ -259,7 +326,9 @@ perp --json arb scan --compare <SYMBOL> # compare a symbol a
|
|
|
259
326
|
perp --json arb scan --positions # position funding impact; actual24h null + fundingHistoryErrors when a venue's history is unreadable
|
|
260
327
|
perp --json arb scan --live --interval 60 # continuous monitoring
|
|
261
328
|
perp --json arb scan --top 10 --hold-days 7 --bridge-cost 0 --size 100 # cost-aware filtering
|
|
262
|
-
perp --json
|
|
329
|
+
perp --json -e extended market list # Extended (read-only): rows carry assetClass, venueSymbol (TSLA_24_5-USD), tickSize, minSize, tradable, fundingRateKind "accruing"
|
|
330
|
+
perp --json -e extended market funding BTC -l 24 # applied hourly prints: rateKind "lastSettled", intervalHours 1, annualizedPct
|
|
331
|
+
perp --json arb scan --rates --include-unverified # also show QFEX, TxFlow, Variational, GRVT and Extended (unverified funding; TxFlow rows apiStatus "unofficial"; display only, never ranked)
|
|
263
332
|
# netSpread / grossSpread = annualized % (netSpread = gross minus round-trip costs amortized over --hold-days)
|
|
264
333
|
# spot-perp rows: netSpreadPct / grossSpreadPct, spotExchanges[], perpExchange, direction
|
|
265
334
|
|
|
@@ -337,6 +406,9 @@ perp --json wallet agent approve <exchange> \
|
|
|
337
406
|
[--account-index <0-9|255>] # Arcus subaccount
|
|
338
407
|
[--api-key-index <n>] # Lighter slot 4-254 (default: first slot free locally AND on-chain)
|
|
339
408
|
[--force] # Lighter: overwrite an --api-key-index slot that already holds an on-chain key
|
|
409
|
+
# risex: READ-ONLY in this release (trading is planned for a later release) — `wallet agent approve|import|revoke|rotate
|
|
410
|
+
# risex` → CAPABILITY_UNSUPPORTED (also with --dry-run); `wallet agent verify risex` (read-only) still works.
|
|
411
|
+
# txflow / variational / extended: every agent command → CAPABILITY_UNSUPPORTED (market data only).
|
|
340
412
|
|
|
341
413
|
# Import a key the USER created on the exchange WEBSITE (master wallet not needed here).
|
|
342
414
|
# Key on stdin only (one line; ondo: key id + secret; qfex: public key + secret) — an argv key is rejected (INVALID_PARAMS).
|
|
@@ -383,9 +455,8 @@ perp --json wallet manage account-mode [unified|standard|portfolio] # Hyperliq
|
|
|
383
455
|
perp --json wallet manage sub create <name> # not supported (CAPABILITY_UNSUPPORTED): the subaccount wallet must co-sign — use the web app
|
|
384
456
|
perp --json wallet manage sub list
|
|
385
457
|
perp --json wallet manage sub transfer <from> <to> <amount> # <from> must be the signing account
|
|
386
|
-
perp --json wallet manage lake create [--manager <address>] [--nickname <name>] [--initial-deposit <amount>]
|
|
387
|
-
perp --json wallet manage lake deposit
|
|
388
|
-
perp --json wallet manage lake withdraw <lake> <shares> # shares to redeem, not USDC
|
|
458
|
+
perp --json wallet manage lake create [--manager <address>] [--nickname <name>] [--initial-deposit <amount>] # --initial-deposit: master-signed only
|
|
459
|
+
perp --json wallet manage lake deposit|withdraw … # MOVED → perp -e pacifica vault deposit|withdraw (refused here)
|
|
389
460
|
perp --json wallet manage builder overview # Pacifica builder-OWNER tools only (user approvals: wallet builder)
|
|
390
461
|
perp --json wallet manage builder trades <code>
|
|
391
462
|
perp --json wallet manage builder leaderboard <code>
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# perp-cli/ws — live market, funding and account streams (library)
|
|
2
|
+
|
|
3
|
+
`perp-cli/ws` is a composable WebSocket library inside the `perp-cli` npm package. Use it to build a
|
|
4
|
+
custom live dashboard, funding comparison or price/account feed for **hyperliquid** (native + HIP-3),
|
|
5
|
+
**pacifica**, **lighter**, **lighter-rh**, **aster** and **arcus**, in the browser or in Node.
|
|
6
|
+
|
|
7
|
+
> **Stability:** ships in **0.15.0** and is **@experimental** through 0.15.x — breaking changes only in a
|
|
8
|
+
> minor bump (CHANGELOG callouts); every event carries `schema: 1`. **@stable from 0.16.0** after consumer
|
|
9
|
+
> sign-off. Planned for **0.15.1**: Aster user channels, Ondo / QFEX, `book` / `trades`, `perp stream`.
|
|
10
|
+
> The `VenueId` type already includes `"ondo"` and `"qfex"`, but there are no codecs for them until 0.15.1.
|
|
11
|
+
> **GRVT** (CLI venue, market data only) has no codec: its public WS has per-instrument selectors only (no
|
|
12
|
+
> all-markets stream) and a 150-stream cap below its 184 perpetuals, so `allMarkets` cannot be served.
|
|
13
|
+
> **Extended** (CLI venue `extended`, market data only) has no codec: its public streams are one socket
|
|
14
|
+
> per stream URL (`wss://api.starknet.extended.exchange/stream.extended.exchange/v1/prices/mark`,
|
|
15
|
+
> `/orderbooks/<m>?depth=1`, `/funding`, …) pushed on connect with no subscribe frame (the bare root
|
|
16
|
+
> answers HTTP 404), while a codec here is one `url(network)` plus subscribe / unsubscribe frames. Its
|
|
17
|
+
> accruing funding rate is REST-only (`/info/markets`); the `/funding` stream is documented as applied
|
|
18
|
+
> rates only. Use the CLI (`perp -e extended market …`) for Extended.
|
|
19
|
+
|
|
20
|
+
Runnable examples: `examples/ws-funding-compare-browser`, `examples/ws-next-smoke`,
|
|
21
|
+
`examples/ws-node-stream` in the perp-cli repository.
|
|
22
|
+
|
|
23
|
+
## 1. Install and import
|
|
24
|
+
|
|
25
|
+
Two packages, **the same code and API** (built from one source):
|
|
26
|
+
|
|
27
|
+
| Package | Install | When |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **`@hypurrquant/perp-ws`** (recommended for integrators) | `npm install @hypurrquant/perp-ws` (+ `ws` for the Node entry) | apps, dashboards, bots — **no dependencies** (~100 KB ESM) |
|
|
30
|
+
| `perp-cli/ws` | `npm install perp-cli` | perp-cli is already installed (the CLI pulls ~140 MB of CLI deps) |
|
|
31
|
+
|
|
32
|
+
| Runtime | `@hypurrquant/perp-ws` | `perp-cli` | Transport |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| Browser (any bundler, Next.js, Vite, static page), Node ≥ 22 without `ws` | `@hypurrquant/perp-ws` | `perp-cli/ws` | the runtime's native `WebSocket` and `fetch` |
|
|
35
|
+
| Node 20+ | `@hypurrquant/perp-ws/node` | `perp-cli/ws/node` | `ws` (16 MiB message cap, protocol ping/pong); same API |
|
|
36
|
+
|
|
37
|
+
- **`ws` for the Node entry:** `@hypurrquant/perp-ws` declares `ws` (`^8.21.0`) as an **optional peer
|
|
38
|
+
dependency** — `npm install @hypurrquant/perp-ws ws`. Importing `/node` never fails; creating a stream
|
|
39
|
+
or transport without `ws` throws `INVALID_PARAMS` ("npm install ws@^8.21.0"). The Node entry uses `ws`
|
|
40
|
+
even on Node ≥ 22 (which has a global `WebSocket`) for the inbound size cap and ping/pong handling; on
|
|
41
|
+
Node ≥ 22 you may use the browser entry instead (global `WebSocket`, no `ws`, no size cap of ours).
|
|
42
|
+
`perp-cli` ships `ws` as a regular dependency.
|
|
43
|
+
- ESM only, ES2020, no side effects, no top-level await, touches no global at module load (safe in
|
|
44
|
+
SSR / prerendering). Next.js 13.5+: `transpilePackages: ["@hypurrquant/perp-ws"]` (or `["perp-cli"]`).
|
|
45
|
+
- TypeScript: `moduleResolution` `"bundler"`, `"node16"` / `"nodenext"`, or `"node"` (via `typesVersions`).
|
|
46
|
+
- Only the entries above (and `<package>/package.json`) are importable (no deep imports).
|
|
47
|
+
- The examples below import `@hypurrquant/perp-ws`; with perp-cli, replace it by `perp-cli/ws`
|
|
48
|
+
(`@hypurrquant/perp-ws/node` → `perp-cli/ws/node`). Everything else is identical.
|
|
49
|
+
- The Node entry caps one inbound message at 16 MiB (`DEFAULT_MAX_PAYLOAD`; real frames are ≤ ~150 KB).
|
|
50
|
+
An oversized message closes the socket (`reconnecting` reason `WS_PROTOCOL_ERROR`, naming
|
|
51
|
+
`WS_ERR_UNSUPPORTED_MESSAGE_LENGTH`); 3 within 10 min on one socket fail the subscriptions on it with `WS_PROTOCOL_ERROR`.
|
|
52
|
+
Raise the cap with `createVenueStream(codec, { transport: createNodeTransport({ maxPayload }) })`.
|
|
53
|
+
- The browser entry in Node 20 without a `transport` throws `INVALID_PARAMS` ("import from the /node
|
|
54
|
+
entry … or pass transport").
|
|
55
|
+
|
|
56
|
+
### Static page without a bundler
|
|
57
|
+
|
|
58
|
+
`dist/index.js` of `@hypurrquant/perp-ws` (`dist/ws/index.js` of perp-cli) is one self-contained ESM
|
|
59
|
+
file with **no bare imports**, so a plain HTML page can load it through an import map. The map points the
|
|
60
|
+
public specifier at the file; your code still imports `"@hypurrquant/perp-ws"` (that is the "no deep
|
|
61
|
+
imports" rule: never write `.../dist/...` in JS — the file path lives only in the map, which is the
|
|
62
|
+
page's job, like a bundler's resolver):
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<script type="importmap">
|
|
66
|
+
{ "imports": { "@hypurrquant/perp-ws": "/node_modules/@hypurrquant/perp-ws/dist/index.js" } }
|
|
67
|
+
</script>
|
|
68
|
+
<script type="module">
|
|
69
|
+
import { createVenueStream, hyperliquid } from "@hypurrquant/perp-ws";
|
|
70
|
+
const stream = createVenueStream(hyperliquid);
|
|
71
|
+
stream.subscribe({ kind: "allMarkets", canonicals: ["BTC"] }).on((e) => {
|
|
72
|
+
if (e.type === "market") document.title = `BTC ${e.mark ?? "—"}`;
|
|
73
|
+
});
|
|
74
|
+
</script>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Serve the page and `node_modules` from the same static server (any origin works: the venues do not check
|
|
78
|
+
Origin and their REST CORS is `*`). Do the same with a CDN that serves npm files (e.g.
|
|
79
|
+
`https://cdn.jsdelivr.net/npm/@hypurrquant/perp-ws@0.15.0/dist/index.js`). The `/node` entry is
|
|
80
|
+
Node-only (it loads `ws`); never map it in a browser.
|
|
81
|
+
|
|
82
|
+
## 2. Minimal examples
|
|
83
|
+
|
|
84
|
+
All-market state for one venue (Node):
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createVenueStream, hyperliquid, toApr } from "@hypurrquant/perp-ws/node";
|
|
88
|
+
|
|
89
|
+
const stream = createVenueStream(hyperliquid, { hip3Dexes: ["xyz"] }); // HIP-3 dexes are opt-in
|
|
90
|
+
stream.onState((s) => console.log("state", s.status));
|
|
91
|
+
const sub = stream.subscribe({ kind: "allMarkets", canonicals: ["BTC", "ETH", "xyz:TSLA"] });
|
|
92
|
+
sub.on((e) => {
|
|
93
|
+
if (e.type === "market") {
|
|
94
|
+
console.log(e.symbol, e.mark, e.funding.rate, e.funding.rateKind, toApr(e.funding.rate, e.funding.rateIntervalHours));
|
|
95
|
+
} else if (e.type === "error") {
|
|
96
|
+
console.error(e.fatal ? "FATAL" : "warn", e.code, e.message, e.remediation); // fatal = this subscription ended
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
await sub.ready; // rejects with a WsError (code + remediation) — let it throw, or catch and show it
|
|
100
|
+
// …later
|
|
101
|
+
await stream.close();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Async iteration instead of callbacks:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { createVenueStream, pacifica } from "@hypurrquant/perp-ws/node";
|
|
108
|
+
|
|
109
|
+
const stream = createVenueStream(pacifica);
|
|
110
|
+
for await (const e of stream.subscribe({ kind: "market", symbol: "BTC", channels: ["market", "bbo"] })) {
|
|
111
|
+
if (e.type === "bbo") console.log(e.bid, e.bidSize, e.ask, e.askSize);
|
|
112
|
+
if (e.type === "market") break; // break → unsubscribes
|
|
113
|
+
}
|
|
114
|
+
await stream.close();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The same markets on several venues (browser) — **`canonicals`, and every rejection made visible**:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { arcus, aster, createVenueStream, hyperliquid, lighter, pacifica, type VenueStream } from "@hypurrquant/perp-ws";
|
|
121
|
+
|
|
122
|
+
declare function show(venue: string, text: string): void; // your UI: one status cell per venue
|
|
123
|
+
|
|
124
|
+
const streams: VenueStream[] = [hyperliquid, pacifica, lighter, aster, arcus].map((c) => createVenueStream(c));
|
|
125
|
+
for (const stream of streams) {
|
|
126
|
+
// canonicals: "BTC" → hyperliquid/pacifica/lighter BTC, aster BTCUSDT + BTCUSD1, arcus BTC-USD.
|
|
127
|
+
// (symbols would need each venue's directory symbols; ["BTC"] rejects on aster and arcus.)
|
|
128
|
+
// A canonical this venue does not list (e.g. SOL somewhere) is skipped with a non-fatal warning;
|
|
129
|
+
// ready rejects only when the venue lists none of them.
|
|
130
|
+
const sub = stream.subscribe({ kind: "allMarkets", canonicals: ["BTC", "ETH", "SOL"] });
|
|
131
|
+
sub.ready.then(
|
|
132
|
+
() => show(stream.venue, "live"),
|
|
133
|
+
(err: unknown) => show(stream.venue, `FAILED: ${err instanceof Error ? err.message : String(err)}`),
|
|
134
|
+
);
|
|
135
|
+
sub.on((e) => {
|
|
136
|
+
if (e.type === "market") show(stream.venue, `${e.symbol} ${e.mark ?? "—"}`);
|
|
137
|
+
else if (e.type === "error" && e.code === "SYMBOL_NOT_FOUND") {
|
|
138
|
+
show(stream.venue, `not listed here: ${e.details?.missing?.join(", ")}`); // non-fatal: the rest stream
|
|
139
|
+
} else if (e.type === "error" && e.fatal) show(stream.venue, `FAILED: ${e.code} ${e.message}`);
|
|
140
|
+
});
|
|
141
|
+
stream.onState((st) => { if (st.status === "closed") show(stream.venue, `closed: ${st.reason.message}`); });
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Funding comparison across venues (browser):
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import {
|
|
149
|
+
arcus, aster, createFundingComparator, createVenueStream, hyperliquid, lighter, lighterRh, pacifica, toHourly,
|
|
150
|
+
} from "@hypurrquant/perp-ws";
|
|
151
|
+
|
|
152
|
+
const streams = [hyperliquid, pacifica, lighter, lighterRh, aster, arcus].map((c) => createVenueStream(c));
|
|
153
|
+
const cmp = createFundingComparator({ streams, kind: "next", minVenues: 2 });
|
|
154
|
+
const off = cmp.subscribe((rows) => {
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
const hourly = row.legs
|
|
157
|
+
.filter((l) => !l.stale && l.rate !== null)
|
|
158
|
+
.map((l) => ({ venue: l.venue, kind: l.rateKind, h: toHourly(l.rate, l.funding.rateIntervalHours) }));
|
|
159
|
+
console.log(row.canonical, row.mixedKinds, hourly);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// …later
|
|
163
|
+
off();
|
|
164
|
+
cmp.close();
|
|
165
|
+
await Promise.all(streams.map((s) => s.close()));
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 3. Streams, subscriptions, connection state
|
|
169
|
+
|
|
170
|
+
- `createVenueStream(codec, opts)` — one socket per venue, opened lazily on the first subscription, closed
|
|
171
|
+
(`idle`) when the last one leaves. The market directory is loaded once over REST (`stream.markets()`).
|
|
172
|
+
- `subscribe(target)`:
|
|
173
|
+
- `{ kind: "allMarkets", canonicals? | symbols? }` → `MarketUpdate` for every market, or a subset:
|
|
174
|
+
**`canonicals`** (recommended across venues) selects every directory market whose `canonical` matches
|
|
175
|
+
— `["BTC"]` is `BTC` on hyperliquid / pacifica / lighter / lighter-rh (USDG), `BTCUSDT` + `BTCUSD1` on
|
|
176
|
+
aster and `BTC-USD` on arcus; HIP-3 markets are `"<dex>:<BASE>"` (`"xyz:TSLA"`, with `hip3Dexes`) and
|
|
177
|
+
`"BTC"` never pulls them in. Canonicals come from `opts.canonicalize` (default: USD-like quotes
|
|
178
|
+
collapse) — see `MarketDirectoryEntry.canonical` in `stream.markets()`. Canonicals the venue does
|
|
179
|
+
not list are **skipped and reported once** as a non-fatal control
|
|
180
|
+
`{ type: "error", code: "SYMBOL_NOT_FOUND", fatal: false, details: { missing: string[], suggestions:
|
|
181
|
+
Record<string, string[]> } }` (suggestions e.g. `"xyz:TSLA"` for `"TSLA"`; the others are subscribed and
|
|
182
|
+
`ready` resolves). Only when the venue lists **none** of them does `ready` reject `INVALID_PARAMS`.
|
|
183
|
+
**`symbols`** are this venue's exact directory symbols. Pass **one or the other** — both together
|
|
184
|
+
reject `INVALID_PARAMS`. Same wire subscription and budget either way (filtering is client-side);
|
|
185
|
+
- `{ kind: "market", symbol, channels: ["market" | "bbo"] }`. `"book"` and `"trades"` are already in
|
|
186
|
+
the types but **reject with `CAPABILITY_UNSUPPORTED` until 0.15.1** — don't ship UI that needs them.
|
|
187
|
+
Any other channel name (e.g. `"ticker"`) or an empty `channels` rejects with `INVALID_PARAMS`;
|
|
188
|
+
- `{ kind: "user", channels: ["account" | "positions" | "orders" | "fills"] }` (needs `credentials`).
|
|
189
|
+
- **`symbols` / `symbol` mean the venue's directory symbols, case-sensitive** — not canonical names. `BTC` is `"BTC"` on
|
|
190
|
+
hyperliquid / pacifica / lighter, `"BTC-USD"` on arcus and `"BTCUSDT"` / `"BTCUSD1"` on aster. Pass
|
|
191
|
+
`MarketUpdate.symbol` / `FundingLeg.symbol`, or look them up in `stream.markets()`. Unknown symbols reject
|
|
192
|
+
with `INVALID_PARAMS` before anything is sent; the message carries a "did you mean …" hint
|
|
193
|
+
(`details.suggestions`) when a directory symbol matches case-insensitively or by canonical / base name.
|
|
194
|
+
- `Subscription`: `on(cb)` (unbuffered; a listener that throws is isolated — the other listeners and the
|
|
195
|
+
stream keep running — and reported to `opts.logger` as `{ event: "listener-error" }`, not as a stream
|
|
196
|
+
event), `for await` (bounded buffer, `opts.buffer.size`, default 1024),
|
|
197
|
+
`ready` (resolves when every wire channel is acknowledged; rejects with a `WsError`), `status`,
|
|
198
|
+
`unsubscribe()`. Wire channels are ref-counted: two subscriptions on one venue channel = one wire
|
|
199
|
+
subscription.
|
|
200
|
+
- Connection state (`stream.state`, `stream.onState(cb)`, and `{type:"state"}` events):
|
|
201
|
+
`idle → connecting → live ⇄ stale → reconnecting → … → closed`. `stale` = no message on a channel for
|
|
202
|
+
`staleAfterMs` (from message timestamps, re-checked when a hidden tab becomes visible); 2× → forced
|
|
203
|
+
reconnect. Reconnect backoff 1 s → 30 s with jitter, a per-venue new-connection budget, then
|
|
204
|
+
resubscribe; the first frame after a (re)subscribe has `source: "snapshot"`.
|
|
205
|
+
- Several sockets (Pacifica, > 20 channels of one kind): each socket reconnects and resubscribes only
|
|
206
|
+
its own channels (gaps only for channels on it). `reconnect.maxAttempts` counts per socket, but **one
|
|
207
|
+
socket exhausting it closes the whole stream** (every subscription ends with that reason). The state is
|
|
208
|
+
the aggregate, worst first: `closed` > `reconnecting` > `stale` > `connecting` (of a socket that was
|
|
209
|
+
open before) > `live`. A socket opening for the first time (just added, waiting on the
|
|
210
|
+
new-connection budget, or retrying a failed first handshake) does not turn a live stream into
|
|
211
|
+
`connecting` / `reconnecting`: it stays `live` with
|
|
212
|
+
`sockets` = open sockets and `pending` = sockets still opening — the channels on those are not ready
|
|
213
|
+
yet (their `ready` resolves when they open). `pending` is present only on multi-socket streams.
|
|
214
|
+
- `closed` is terminal: a later `subscribe` rejects with the close reason. A handshake failure before
|
|
215
|
+
open = `EXCHANGE_UNREACHABLE` ("network, venue outage, or Origin/CORS block").
|
|
216
|
+
- Ack timeout: no acknowledgement and no data within the channel's stale threshold → a **pending**
|
|
217
|
+
subscription fails with `WS_PROTOCOL_ERROR`.
|
|
218
|
+
- Iterator buffer: market/bbo/account/positions/orders events are **state** and coalesce by key (never
|
|
219
|
+
fatal). Fills and control events are not coalesced; a full buffer → fatal `STREAM_OVERFLOW` and the
|
|
220
|
+
subscription closes. Before the iterator exists, state/error controls are kept in a ring of the last
|
|
221
|
+
`size` (the iterator first gets a non-fatal `STREAM_OVERFLOW` with the discarded count); fills,
|
|
222
|
+
gaps and the one-time `SYMBOL_NOT_FOUND` warning are never evicted.
|
|
223
|
+
|
|
224
|
+
## 4. Event fields and nullability
|
|
225
|
+
|
|
226
|
+
Rules: **unknown = `null`, never `0`**. A field the venue never sent is `null`; a field the latest frame
|
|
227
|
+
did not carry keeps its last value, with `asOf[field]` = receive time. `Dec` = the venue's decimal string
|
|
228
|
+
(exact; parse it yourself). Check `null` before arithmetic — `null` is not zero.
|
|
229
|
+
|
|
230
|
+
`MarketUpdate`: `venue, dex, symbol, venueSymbol, quote, canonical, mark, index, bestBid, bestAsk, last,
|
|
231
|
+
funding{ rate, rateKind, rateSource, rateIntervalHours, settlementIntervalHours, intervalSource,
|
|
232
|
+
nextFundingTime, predictedRate, marketClosed }, openInterest{ value, unit: "base"|"usd"|null, sides },
|
|
233
|
+
volume24hNotional, venueTs, recvTs, source, changed[], asOf{}`.
|
|
234
|
+
|
|
235
|
+
Funding:
|
|
236
|
+
- `rate` is **raw per `rateIntervalHours`** — never pre-annualized. Use `toHourly(rate, h)` to compare and
|
|
237
|
+
`toApr(rate, h)` only for display (both return `null` for `null` input).
|
|
238
|
+
- `rateKind`: `"lastSettled"` (the last payment), `"accruing"` (the current period so far, the "if it
|
|
239
|
+
settled now" rate), `"predicted"` (the venue's estimate of the next payment). `predictedRate` is always
|
|
240
|
+
kind `"predicted"`.
|
|
241
|
+
|
|
242
|
+
| Venue | `rate` kind | `predictedRate` | interval | OI | notes |
|
|
243
|
+
|---|---|---|---|---|---|
|
|
244
|
+
| hyperliquid | accruing | null | 1 h | base, one side | bid/ask/last null → `bbo` channel |
|
|
245
|
+
| pacifica | lastSettled | next_funding | 1 h | base, `sides` null | bid/ask/last/nextFundingTime null |
|
|
246
|
+
| lighter / lighter-rh | lastSettled | current_funding_rate | 1 h | **usd** | wire values are percent/h, emitted as fractions/h |
|
|
247
|
+
| aster | accruing | null | per market (`rest:/fapi/v3/fundingInfo`, null if missing) | null | all quotes (USDT, USD1, …) with `quote` set |
|
|
248
|
+
| arcus | lastSettled | nextFundingRate | 1 h | base, one side | bid/ask null → `bbo` channel |
|
|
249
|
+
|
|
250
|
+
`stream.capabilities` carries the full matrix: `browser`, `networks`, `channels`, `fields` (false = always
|
|
251
|
+
null) and `notes` (why). lighter-rh has no testnet (`CAPABILITY_UNSUPPORTED`).
|
|
252
|
+
|
|
253
|
+
`BboUpdate`: `bid, bidSize, ask, askSize` (null = that side of the book is empty), `venueTs, recvTs`.
|
|
254
|
+
Venue sequence numbers only drop out-of-order frames (they are not contiguous; bbo never emits gaps).
|
|
255
|
+
Pacifica allows ≤ 20 subscriptions per channel per connection: the stream spreads further `bbo` channels
|
|
256
|
+
over more sockets (≤ 50 per stream; `budget.maxSockets` lowers it; `SUBSCRIPTION_LIMIT` beyond).
|
|
257
|
+
`live.sockets` counts the open ones.
|
|
258
|
+
|
|
259
|
+
## 5. Funding comparator
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { createFundingComparator, type FundingRow, type VenueStream } from "@hypurrquant/perp-ws";
|
|
263
|
+
|
|
264
|
+
declare const streams: VenueStream[];
|
|
265
|
+
const cmp = createFundingComparator({ streams, kind: "next", include: ["BTC", "ETH"], minVenues: 2 });
|
|
266
|
+
const rows: FundingRow[] = cmp.rows();
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Options and defaults:
|
|
270
|
+
|
|
271
|
+
| Option | Default | Meaning |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `kind` | `"current"` | which rate each leg contributes (below) |
|
|
274
|
+
| `minVenues` | `2` | rows need ≥ this many **distinct venues with a non-null rate** |
|
|
275
|
+
| `staleAfterMs` | 2 × the venue's `stream.staleAfterMs` (Arcus 3×) | a leg is `stale` when its stream is not `live` or its rate is older than this. Venue defaults: hyperliquid 30 s (all markets), pacifica 10 s, lighter / lighter-rh 10 s, aster 5 s, arcus 15 s |
|
|
276
|
+
| `include` | all | allowlist of canonical symbols |
|
|
277
|
+
| `canonicalize` | `defaultCanonicalize` | HIP-3 → `"<dex>:<BASE>"`; USD-like quotes (USDT, USD1, USDC, USD, USDG) → `"<BASE>"`; any other quote → `null` (not compared) |
|
|
278
|
+
| `onControl` | none | the streams' `state` / `error` events and `{type:"listener-error"}` |
|
|
279
|
+
|
|
280
|
+
- `kind`: `"current"` (each leg's `rate`), `"predicted"`, `"lastSettled"`, `"accruing"`, and **`"next"`** —
|
|
281
|
+
per venue the accruing rate when published, else the predicted one. A leg without the requested kind
|
|
282
|
+
has `rate: null, kindUnavailable: true` (never substituted).
|
|
283
|
+
- **`mixedKinds`** is true when a row's legs with a rate use different kinds. With `kind: "next"` it is
|
|
284
|
+
**true by design** whenever an accruing venue (hyperliquid, aster: the current period so far) meets a
|
|
285
|
+
predicted one (pacifica, lighter, lighter-rh, arcus: the venue's estimate of the next payment). Both
|
|
286
|
+
are forward-looking estimates of the coming payment, so the row is usable for ranking; care when you
|
|
287
|
+
need exact like-for-like numbers (e.g. P&L attribution, or an accruing rate early in its period that
|
|
288
|
+
can still move a lot) — then use `kind: "accruing"` or `"predicted"` and accept fewer legs.
|
|
289
|
+
- **Several legs per venue:** one venue can contribute more than one leg to a row. Aster lists BTC as
|
|
290
|
+
`BTCUSDT` (quote USDT) and `BTCUSD1` (quote USD1); both canonicalize to `"BTC"`. Pick one per venue
|
|
291
|
+
before ranking — by `quote`, or by the most liquid market (below) — or pass a `canonicalize` that
|
|
292
|
+
returns `null` for the quotes you don't want.
|
|
293
|
+
- **`fundingVerified`** (per leg): the CLI's verdict that the venue's funding unit and which published
|
|
294
|
+
rate actually settles were confirmed against its funding payments. All six venues in 0.15.0 are
|
|
295
|
+
`true`; a `false` leg (a future venue) is indicative only — the CLI keeps such venues out of funding
|
|
296
|
+
comparison and refuses them as funding-arb legs, so do the same.
|
|
297
|
+
- **Filter illiquid outliers before ranking spreads.** Tiny or newly listed markets often show extreme
|
|
298
|
+
rates (hundreds of % APR) that cannot be traded at size. Require a minimum 24 h notional and/or open
|
|
299
|
+
interest per leg; the comparator legs carry no volume, so read it from the same streams:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import {
|
|
303
|
+
createFundingComparator, createVenueStream, aster, hyperliquid, lighter, toHourly,
|
|
304
|
+
type FundingLeg, type FundingRow, type MarketUpdate,
|
|
305
|
+
} from "@hypurrquant/perp-ws";
|
|
306
|
+
|
|
307
|
+
const streams = [hyperliquid, lighter, aster].map((c) => createVenueStream(c));
|
|
308
|
+
// Same wire channel as the comparator's own subscription (ref-counted): no extra venue traffic.
|
|
309
|
+
const notional = new Map<string, number>(); // "<venue>|<symbol>" → 24 h notional
|
|
310
|
+
for (const s of streams) {
|
|
311
|
+
s.subscribe({ kind: "allMarkets" }).on((e) => {
|
|
312
|
+
if (e.type !== "market") return;
|
|
313
|
+
const u: MarketUpdate = e;
|
|
314
|
+
if (u.volume24hNotional !== null) notional.set(`${u.venue}|${u.symbol}`, Number(u.volume24hNotional));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const MIN_NOTIONAL = 1_000_000;
|
|
319
|
+
/** One leg per venue: the most liquid market that passes the filter (null volume = unknown → skipped). */
|
|
320
|
+
function pickLegs(row: FundingRow): FundingLeg[] {
|
|
321
|
+
const best = new Map<string, { leg: FundingLeg; vol: number }>();
|
|
322
|
+
for (const leg of row.legs) {
|
|
323
|
+
if (leg.rate === null || leg.stale || !leg.fundingVerified) continue;
|
|
324
|
+
const vol = notional.get(`${leg.venue}|${leg.symbol}`);
|
|
325
|
+
if (vol === undefined || vol < MIN_NOTIONAL) continue;
|
|
326
|
+
const cur = best.get(leg.venue);
|
|
327
|
+
if (cur === undefined || vol > cur.vol) best.set(leg.venue, { leg, vol });
|
|
328
|
+
}
|
|
329
|
+
return Array.from(best.values(), (b) => b.leg);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const cmp = createFundingComparator({ streams, kind: "next" });
|
|
333
|
+
cmp.subscribe((rows) => {
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
const legs = pickLegs(row)
|
|
336
|
+
.map((l) => ({ venue: l.venue, h: toHourly(l.rate, l.funding.rateIntervalHours) }))
|
|
337
|
+
.filter((x): x is { venue: typeof x.venue; h: number } => x.h !== null)
|
|
338
|
+
.sort((a, b) => a.h - b.h);
|
|
339
|
+
const lo = legs[0];
|
|
340
|
+
const hi = legs[legs.length - 1];
|
|
341
|
+
if (lo !== undefined && hi !== undefined && legs.length >= 2) console.log(row.canonical, lo.venue, hi.venue);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## 6. User (account) channels
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { createVenueStream, lighter, type CredentialProvider } from "@hypurrquant/perp-ws";
|
|
350
|
+
|
|
351
|
+
const credentials: CredentialProvider = {
|
|
352
|
+
user: async () => ({ venue: "lighter", accountIndex: 12345 }),
|
|
353
|
+
// Only Lighter's orders channel needs a token. Your backend mints it; the page never sees the key.
|
|
354
|
+
authToken: async ({ purpose, maxTtlSec }) => {
|
|
355
|
+
const qs = `purpose=${encodeURIComponent(purpose)}&ttl=${encodeURIComponent(String(maxTtlSec))}`;
|
|
356
|
+
// Same-origin request carrying the user's session cookie: the endpoint must authenticate the caller.
|
|
357
|
+
const res = await fetch(`/api/lighter-token?${qs}`, { credentials: "same-origin" });
|
|
358
|
+
if (!res.ok) throw new Error(`lighter-token: HTTP ${res.status}`);
|
|
359
|
+
const body = (await res.json()) as { token: string; expiresAt: number }; // expiresAt: epoch ms
|
|
360
|
+
// Only accept a READ-ONLY auth token in the page; a full token could be used to trade.
|
|
361
|
+
if (typeof body.token !== "string" || !body.token.startsWith("ro:")) throw new Error("lighter-token: expected a read-only ro: token");
|
|
362
|
+
return body;
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Server side (sketch of the /api/lighter-token handler; your framework and session store will differ).
|
|
367
|
+
interface Session { accountIndex: number }
|
|
368
|
+
declare function getSession(req: Request): Promise<Session | null>; // your auth (cookie → session)
|
|
369
|
+
declare function mintLighterReadOnlyToken(accountIndex: number, ttlSec: number): Promise<string>; // "ro:…"
|
|
370
|
+
const MAX_TOKEN_TTL_SEC = 600; // server-side cap: never trust the client's ttl
|
|
371
|
+
export async function handleLighterToken(req: Request): Promise<Response> {
|
|
372
|
+
const session = await getSession(req);
|
|
373
|
+
if (session === null) return new Response("unauthorized", { status: 401 });
|
|
374
|
+
const url = new URL(req.url);
|
|
375
|
+
// purpose is "<venue>:account_all_orders" (lighter or lighter-rh); refresh follows the expiresAt you return.
|
|
376
|
+
if (!/^lighter(-rh)?:account_all_orders$/.test(url.searchParams.get("purpose") ?? "")) return new Response("bad purpose", { status: 400 });
|
|
377
|
+
const asked = Number(url.searchParams.get("ttl"));
|
|
378
|
+
const ttlSec = Math.min(Number.isFinite(asked) && asked > 0 ? Math.floor(asked) : MAX_TOKEN_TTL_SEC, MAX_TOKEN_TTL_SEC);
|
|
379
|
+
const token = await mintLighterReadOnlyToken(session.accountIndex, ttlSec); // the session's own account only
|
|
380
|
+
return Response.json({ token, expiresAt: Date.now() + ttlSec * 1000 });
|
|
381
|
+
}
|
|
382
|
+
const stream = createVenueStream(lighter, { credentials });
|
|
383
|
+
const sub = stream.subscribe({ kind: "user", channels: ["account", "positions", "orders", "fills"] });
|
|
384
|
+
sub.on((e) => {
|
|
385
|
+
switch (e.type) {
|
|
386
|
+
case "account": console.log(e.equity, e.available); break;
|
|
387
|
+
case "positions": console.log(e.positions.length, e.snapshot); break;
|
|
388
|
+
case "orders": console.log(e.orders.length, e.complete); break;
|
|
389
|
+
case "fills": console.log("snapshot batch", e.fills.length); break;
|
|
390
|
+
case "fill": console.log(e.fillId, e.fill.side, e.fill.size, e.fill.price, e.fill.clientId); break;
|
|
391
|
+
case "gap": console.log("refetch over REST:", e.channel); break;
|
|
392
|
+
default: break;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
- **The library never holds keys.** `CredentialProvider` is async callbacks: `user()` (the account
|
|
398
|
+
identity), `authToken({ purpose, maxTtlSec })` (Lighter `account_all_orders`; re-called at 80 % of the
|
|
399
|
+
TTL with backoff retries; a read-only `ro:` token is recommended in browsers), `hmacSha256` /
|
|
400
|
+
`signTypedData` (reserved for 0.15.1 venues). Tokens handed in are redacted from every error, log and
|
|
401
|
+
raw excerpt.
|
|
402
|
+
- `allowInPageSecrets: true` is required for venues whose auth needs a signing secret in the page
|
|
403
|
+
(Aster / Ondo / QFEX, 0.15.1). Without it they reject `CAPABILITY_UNSUPPORTED` in a browser.
|
|
404
|
+
- `UserRef` per venue: hyperliquid `{ address }` (no auth), pacifica `{ account }` (Solana address, no
|
|
405
|
+
auth), lighter / lighter-rh `{ accountIndex }`, arcus `{ address, subaccount }` (`null` = account 0, no auth).
|
|
406
|
+
- **KNOWN GAP (0.15.0): Aster user channels are not implemented** — `kind: "user"` on aster rejects
|
|
407
|
+
`CAPABILITY_UNSUPPORTED` (no parity; 0.15.1).
|
|
408
|
+
|
|
409
|
+
Events:
|
|
410
|
+
- `account` — `equity, available, marginUsed, unrealizedPnl` (+ `dex` on hyperliquid, one per perp dex);
|
|
411
|
+
`source: "snapshot"` when the frame was a full snapshot.
|
|
412
|
+
- `positions` / `orders` — always the **full current set** (snapshot + merged deltas). Pacifica
|
|
413
|
+
`account_positions` snapshots older than the last applied one (envelope `li`) are dropped.
|
|
414
|
+
`orders.complete` = every part of the set came from a venue snapshot. **Pacifica sends no order
|
|
415
|
+
snapshot: `complete` stays `false` — refetch open orders over REST.**
|
|
416
|
+
- `fills` (`snapshot: true`) — ONE event per venue fill snapshot (HL / Arcus replay recent fills on every
|
|
417
|
+
(re)subscribe; up to 2000), only fills not seen before. `fill` (`snapshot: false`) — one live fill.
|
|
418
|
+
De-duplicated in-library by `fillId` + side + order id (both legs of a self-trade are kept).
|
|
419
|
+
- `gap { channel, action: "refetch-required" }` — the library cannot give a complete picture; refetch
|
|
420
|
+
over REST: after a reconnect on non-replayable channels (Pacifica fills + orders, Lighter fills), at
|
|
421
|
+
subscribe for Pacifica orders, and when a replayed fill snapshot does not reach back to the fills seen
|
|
422
|
+
before (outage longer than the replay window).
|
|
423
|
+
|
|
424
|
+
`clientId`, fees and liquidation price per venue:
|
|
425
|
+
|
|
426
|
+
| Venue | `OrderRow.clientId` | `FillRow.clientId` | `FillRow.fee` | `PositionRow.liquidationPrice` |
|
|
427
|
+
|---|---|---|---|---|
|
|
428
|
+
| hyperliquid | cloid (null if none) | cloid only when the frame carries one (not documented) → join by `orderId` | fee in `feeToken` | liquidationPx (null when none) |
|
|
429
|
+
| pacifica | `I` | `I` | `f` (feeToken null) | `l` (null when not applicable) |
|
|
430
|
+
| lighter / lighter-rh | client_order_id (0 = none → null) | ask/bid client id of your side (0 → null) | usd_amount × rate × 1e-6 (rate in 1e-6, omitted when 0) | liquidation_price; "0" (none) → null |
|
|
431
|
+
| arcus | clientId | clientId | `fee` (feeToken null) | always null (not published) |
|
|
432
|
+
|
|
433
|
+
Mark price in `PositionRow` is null except on Arcus — join the `MarketUpdate` by symbol.
|
|
434
|
+
|
|
435
|
+
## 7. Errors
|
|
436
|
+
|
|
437
|
+
`StreamControl { type: "error", code, message, remediation, raw (redacted ≤ 200 chars), fatal }` — always
|
|
438
|
+
check `fatal`: a fatal error ended that subscription, a non-fatal one is informational (it keeps running);
|
|
439
|
+
`ready` rejects with a `WsError` whose `structured = { code, message, status, retryable, remediation? }`.
|
|
440
|
+
|
|
441
|
+
Where an error surfaces:
|
|
442
|
+
- **Thrown synchronously** (`WsError`, `INVALID_PARAMS`) — bad options, before anything starts:
|
|
443
|
+
`createVenueStream` (codec not a codec export, unknown `network`, bad `buffer.size`, no `WebSocket` /
|
|
444
|
+
`fetch` in the runtime and none passed) and `createFundingComparator` (unknown `kind`, bad `streams`,
|
|
445
|
+
`minVenues`, `include`, `staleAfterMs`, or a non-function `canonicalize` / `onControl` / `clock`).
|
|
446
|
+
- **`ready` rejects** (and the subscription ends with a fatal error event) — per-subscription problems:
|
|
447
|
+
unknown symbol / channel, `book` / `trades` or an unsupported network (`CAPABILITY_UNSUPPORTED`), budget
|
|
448
|
+
limits, credentials, REST bootstrap failures, ack timeout, a closed stream.
|
|
449
|
+
- **Error events** (`{ type: "error" }` via `on()` / the iterator / comparator `onControl`) — everything
|
|
450
|
+
after `ready`: malformed frames, token-refresh retries, reconnects, overflow.
|
|
451
|
+
|
|
452
|
+
| Code | Meaning / what to do |
|
|
453
|
+
|---|---|
|
|
454
|
+
| `WS_PROTOCOL_ERROR` | malformed / unrecognised frame, or no ack — reported, never silently dropped |
|
|
455
|
+
| `WS_AUTH_FAILED` | credentials problem. **Check `fatal`:** non-fatal = a token refresh failed and is being retried (the channel still works with the current token); fatal = no / expired / definitively rejected credentials, the subscription ended |
|
|
456
|
+
| `SUBSCRIPTION_LIMIT` | venue or `budget` limit (`details.needed` / `allowed`) — narrow the subscription |
|
|
457
|
+
| `STREAM_OVERFLOW` | iterator buffer full — consume faster, use `on()`, raise `buffer.size` |
|
|
458
|
+
| `CAPABILITY_UNSUPPORTED` | channel / network not available for the venue (see `capabilities.notes`) |
|
|
459
|
+
| `EXCHANGE_UNREACHABLE` | network / outage / handshake refused (reconnects automatically) |
|
|
460
|
+
| `INVALID_PARAMS` | bad symbol, dex, channel or option |
|
|
461
|
+
| `SYMBOL_NOT_FOUND` | non-fatal: requested `canonicals` this venue does not list (`details.missing`, `details.suggestions`); the rest stream |
|
|
462
|
+
| `RATE_LIMITED`, `EXCHANGE_ERROR` | REST bootstrap failures |
|
|
463
|
+
|
|
464
|
+
## 8. Budgets and limits
|
|
465
|
+
|
|
466
|
+
- `budget: { maxSubscriptions, maxSockets, newConnectionsPerMin }` caps below the venue limits (checked
|
|
467
|
+
before sending). One socket per stream, except venues whose caps are per connection (Pacifica: 20 per
|
|
468
|
+
channel per connection, 100 connections per IP → up to 50 sockets per stream). The new-connection
|
|
469
|
+
budget is per stream (a socket waiting on it is `pending`; it never opens once its channels are
|
|
470
|
+
released); one socket exhausting its reconnects closes the stream.
|
|
471
|
+
- **Per-IP limits are shared by every tab and app on one IP** (HL: 10 connections, 1000 subscriptions,
|
|
472
|
+
30 new connections/min, 10 users). Several tabs of one dashboard share them — keep one stream per venue
|
|
473
|
+
per page.
|
|
474
|
+
- HL `allMarkets` is a single `allDexsAssetCtxs` subscription; `hip3Dexes: "all"` adds every HIP-3 dex.
|
|
475
|
+
|
|
476
|
+
## 9. Do / don't
|
|
477
|
+
|
|
478
|
+
- **Do** treat every field as nullable; `null ≠ 0`. Don't default missing data to 0.
|
|
479
|
+
- **Do** compare funding per hour (`toHourly`) and like-for-like kinds (comparator `kind`); **don't**
|
|
480
|
+
annualize in your data layer (`toApr` is for display).
|
|
481
|
+
- **Do** check `quote` before comparing (Aster lists USDT, USD1 and other quotes per base).
|
|
482
|
+
- **Don't** add a REST fallback when a field is null or a channel is unsupported — the capability matrix
|
|
483
|
+
says why; show it as unknown.
|
|
484
|
+
- **Do** handle `gap → refetch-required` (refetch fills / open orders over REST) and `orders.complete`.
|
|
485
|
+
- **Do** respect `SUBSCRIPTION_LIMIT` and the per-IP note above; coalesce UI updates (~1/s).
|
|
486
|
+
- **Don't** pass private keys to the library; return short-lived tokens from `authToken()`.
|
|
487
|
+
- **Do** use `canonicals` for the same markets on several venues; `symbols` are per-venue directory symbols
|
|
488
|
+
(`["BTC"]` rejects on aster and arcus). Show the `SYMBOL_NOT_FOUND` warning (which canonicals a venue
|
|
489
|
+
lacks) instead of an empty cell.
|
|
490
|
+
- **Do** surface every `ready` rejection and every `fatal` error event per venue (see the price-wall
|
|
491
|
+
pattern in §2). **Never treat `idle` (or a venue with no data) as fine**: `idle` means the stream has no
|
|
492
|
+
active subscription — usually because a subscribe was rejected — not "connected and quiet".
|