perp-cli 0.14.3 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +138 -405
- package/README.zh.md +243 -0
- 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/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +85 -15
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- 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/quote-rules.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 +12 -5
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
- package/dist/bot/strategies/funding-arb-utils.js +53 -20
- 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.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +3 -3
- 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 +126 -6
- package/dist/commands/account.js +168 -24
- 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 +23 -81
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +175 -97
- package/dist/commands/arb.d.ts +20 -0
- package/dist/commands/arb.js +107 -163
- package/dist/commands/backtest.js +23 -21
- 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.d.ts +48 -0
- package/dist/commands/market.js +395 -100
- package/dist/commands/options.d.ts +14 -0
- package/dist/commands/options.js +151 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +455 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +83 -55
- 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 +67 -62
- 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/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +3 -3
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +29 -1
- package/dist/errors.js +35 -3
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +16 -12
- package/dist/exchanges/arcus/adapter.js +45 -18
- 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/public.js +3 -2
- 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 +30 -10
- package/dist/exchanges/aster.js +153 -53
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +8 -2
- package/dist/exchanges/canonical-symbols.js +46 -2
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +73 -0
- package/dist/exchanges/derive/options.js +379 -0
- package/dist/exchanges/extended/adapter.d.ts +96 -0
- package/dist/exchanges/extended/adapter.js +241 -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 +133 -0
- package/dist/exchanges/extended/public.js +436 -0
- package/dist/exchanges/grvt/adapter.d.ts +100 -0
- package/dist/exchanges/grvt/adapter.js +283 -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-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
- package/dist/exchanges/hyperliquid-spot.js +24 -16
- package/dist/exchanges/hyperliquid.d.ts +199 -22
- package/dist/exchanges/hyperliquid.js +550 -202
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +157 -13
- package/dist/exchanges/interface.js +47 -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 +67 -12
- package/dist/exchanges/lighter-public.js +250 -82
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +10 -5
- package/dist/exchanges/lighter-spot.js +105 -97
- package/dist/exchanges/lighter.d.ts +80 -21
- package/dist/exchanges/lighter.js +312 -105
- package/dist/exchanges/ondo/adapter.d.ts +38 -13
- package/dist/exchanges/ondo/adapter.js +86 -27
- 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 +35 -3
- package/dist/exchanges/ondo/public.js +75 -32
- 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/outcome-interface.d.ts +4 -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 +8 -2
- package/dist/exchanges/pacifica-spot.js +26 -18
- 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 +130 -0
- package/dist/exchanges/pacifica-vaults.js +277 -0
- package/dist/exchanges/pacifica.d.ts +108 -10
- package/dist/exchanges/pacifica.js +428 -171
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/public-balances.d.ts +25 -0
- package/dist/exchanges/public-balances.js +158 -0
- package/dist/exchanges/public-positions.d.ts +43 -0
- package/dist/exchanges/public-positions.js +154 -0
- package/dist/exchanges/qfex/adapter.d.ts +69 -13
- package/dist/exchanges/qfex/adapter.js +102 -17
- 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 +65 -12
- package/dist/exchanges/qfex/public.js +131 -25
- 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 +3 -3
- 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 +160 -0
- package/dist/exchanges/risex/adapter.js +842 -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 +8 -8
- 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 +61 -0
- package/dist/exchanges/txflow/adapter.js +150 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +155 -0
- package/dist/exchanges/txflow/public.d.ts +87 -0
- package/dist/exchanges/txflow/public.js +355 -0
- package/dist/exchanges/unknown-symbol.d.ts +34 -0
- package/dist/exchanges/unknown-symbol.js +56 -0
- package/dist/exchanges/variational/adapter.d.ts +67 -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 +52 -10
- package/dist/exchanges/venue-types.js +82 -11
- package/dist/exchanges/venue.d.ts +150 -11
- package/dist/exchanges/venue.js +126 -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 +19 -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 +20 -8
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +18 -6
- 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 +15 -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 +131 -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 +6 -4
- package/dist/funding/legacy-public.js +42 -41
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +95 -6
- package/dist/funding/rates.js +99 -19
- 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/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +64 -0
- package/dist/mcp-server.js +211 -68
- package/dist/pacifica/client.d.ts +69 -5
- package/dist/pacifica/client.js +181 -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/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- 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 +36 -63
- package/dist/trade-validator.js +2 -2
- package/dist/utils.d.ts +22 -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/arcus-market.d.ts +9 -0
- package/dist/ws/arcus-market.js +9 -0
- package/dist/ws/aster-market.d.ts +33 -0
- package/dist/ws/aster-market.js +30 -0
- package/dist/ws/index.d.ts +980 -0
- package/dist/ws/index.js +4460 -0
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +996 -0
- package/dist/ws/node.js +4554 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +71 -10
- package/skills/perp-cli/SKILL.md +153 -36
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +22 -12
- package/skills/perp-cli/references/commands.md +143 -17
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +523 -0
- package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
- package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
- package/skills/perp-cli/scripts/preflight.sh +27 -19
- package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
- package/skills/perp-cli/scripts/validate-arb.sh +56 -39
- package/dist/cli-spec.d.ts +0 -46
- package/dist/cli-spec.js +0 -81
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variational public market data — the OFFICIAL read-only API (venues/variational.ts).
|
|
3
|
+
*
|
|
4
|
+
* Source: `GET /metadata/v2/stats` (docs.variational.io/technical-documentation/api). One call
|
|
5
|
+
* returns every listing; it is shared by every reader for VARIATIONAL_STATS_TTL_MS through the
|
|
6
|
+
* file cache (cache.ts, cross-process, raw JSON body only) plus an in-process in-flight promise,
|
|
7
|
+
* so `market list` + `arb scan` + `perp health` inside the window cost ONE request.
|
|
8
|
+
*
|
|
9
|
+
* Listings (575 on 2026-10-19: 557 perpetuals, 18 swaps):
|
|
10
|
+
* - ticker "BTC-PERP" / "XAU-SWAP", contract_type "perpetual" | "swap". Perp display symbol =
|
|
11
|
+
* the base ("BTC"); a swap keeps its suffix ("XAU-SWAP") so it never collides with "XAU" perp.
|
|
12
|
+
* - mark_price, volume_24h (USDC), open_interest.{long,short}_open_interest (USDC, single-sided:
|
|
13
|
+
* trader positions only — the OLP is the other side). Base-unit OI is NOT published, so the
|
|
14
|
+
* legacy openInterest (base units) is null; the USD figures are carried separately.
|
|
15
|
+
* - quotes.{base,size_1k,size_100k,size_1m}.{bid,ask}: the OLP's INDICATIVE RFQ quotes at fixed
|
|
16
|
+
* notionals (may be cached up to 600 s; quotes.updated_at). Omitted while a market is closed.
|
|
17
|
+
*
|
|
18
|
+
* Funding (docs.variational.io/omni/trading/funding-rates):
|
|
19
|
+
* - perpetual: funding.last.interval_rate is a decimal fraction for ONE interval_seconds window
|
|
20
|
+
* (1h / 4h / 8h, per market), positive = longs pay. annualized_rate is the same rate × 365 d /
|
|
21
|
+
* interval — the identity is checked per row; a row that breaks it gets no rate (warned).
|
|
22
|
+
* - rateKind "predicted": `last` is recomputed during the window (live samples 2026-10-19: 175
|
|
23
|
+
* of 557 perps changed within 2 min under the same next_timestamp) and `next` ("predicted
|
|
24
|
+
* funding rate for the next interval") equals it. It is the rate the OLP will charge at
|
|
25
|
+
* next_timestamp, not a settled print.
|
|
26
|
+
* - swap: separate long / short rates paid once per trading day — there is no single funding
|
|
27
|
+
* rate, so fundingRate is null and swaps are never matched across venues (canonical null);
|
|
28
|
+
* the long / short rates are shown on the Variational rows only.
|
|
29
|
+
*/
|
|
30
|
+
import { getCached, setCached } from "../../cache.js";
|
|
31
|
+
import { canonicalFor } from "../canonical-symbols.js";
|
|
32
|
+
import { networkConfig } from "../venue.js";
|
|
33
|
+
import { variationalVenue } from "../venues/variational.js";
|
|
34
|
+
import { variationalShapeError, VariationalClient } from "./client.js";
|
|
35
|
+
import { symbolNotFound } from "../unknown-symbol.js";
|
|
36
|
+
/** How long one stats payload serves every reader (the venue itself sends max-age=30). */
|
|
37
|
+
export const VARIATIONAL_STATS_TTL_MS = 5_000;
|
|
38
|
+
const YEAR_SECONDS = 365 * 86_400;
|
|
39
|
+
/** interval_rate vs annualized_rate × interval / 365 d: both are rounded to ~4 significant digits. */
|
|
40
|
+
const RATE_IDENTITY_REL_TOL = 0.005;
|
|
41
|
+
const RATE_IDENTITY_ABS_TOL = 1e-9;
|
|
42
|
+
/**
|
|
43
|
+
* TradFi perpetuals by class (the API publishes no asset class). Equities / ETFs match by ticker
|
|
44
|
+
* identity; commodities / pre-IPO only through EXPLICIT_CANONICAL. A perp whose `name` reads like
|
|
45
|
+
* a company or fund but is missing here is listed as equity with NO canonical symbol (never matched
|
|
46
|
+
* across venues) until it is classified — see looksTradFi().
|
|
47
|
+
*/
|
|
48
|
+
const TRADFI_PERP_CLASS = {
|
|
49
|
+
// Commodities
|
|
50
|
+
XAU: "commodity", XAG: "commodity", XPD: "commodity", XPT: "commodity", CL: "commodity", BZ: "commodity",
|
|
51
|
+
COPPER: "commodity", NATGAS: "commodity",
|
|
52
|
+
// Pre-IPO
|
|
53
|
+
ANTHROPIC: "preIpo", OPENAI: "preIpo",
|
|
54
|
+
// ETFs
|
|
55
|
+
DRAM: "etf", EWJ: "etf", EWT: "etf", EWY: "etf", EWZ: "etf", IWM: "etf", KSTR: "etf", QQQ: "etf", SOXL: "etf",
|
|
56
|
+
SOXS: "etf", SPCX: "etf", TMF: "etf", TZA: "etf", URNM: "etf", UVXY: "etf", XBI: "etf", XLE: "etf", US500: "etf",
|
|
57
|
+
// Equities
|
|
58
|
+
AAOI: "equity", AAPL: "equity", ADBE: "equity", ALAB: "equity", AMAT: "equity", AMD: "equity", AMZN: "equity",
|
|
59
|
+
APP: "equity", ARM: "equity", ASML: "equity", AVGO: "equity", BABA: "equity", BBX: "equity", BE: "equity",
|
|
60
|
+
BMNR: "equity", BNC: "equity", BOT: "equity", BRKB: "equity", BX: "equity", CAT: "equity", CBRS: "equity",
|
|
61
|
+
CIEN: "equity", COIN: "equity", COST: "equity", CRCL: "equity", CRDO: "equity", CRM: "equity", CRWD: "equity",
|
|
62
|
+
CRWV: "equity", CSCO: "equity", DELL: "equity", DIS: "equity", DJT: "equity", DKNG: "equity", EBAY: "equity",
|
|
63
|
+
FLNC: "equity", FWDI: "equity", GLW: "equity", GME: "equity", GOOGL: "equity", GPRO: "equity", HD: "equity",
|
|
64
|
+
HIMS: "equity", HOOD: "equity", HPE: "equity", IBM: "equity", INTC: "equity", IREN: "equity", JPM: "equity",
|
|
65
|
+
KLAC: "equity", KO: "equity", LITE: "equity", LLY: "equity", META: "equity", MRNA: "equity", MRVL: "equity",
|
|
66
|
+
MSFT: "equity", MSTR: "equity", MU: "equity", NBIS: "equity", NFLX: "equity", NOK: "equity", NOW: "equity",
|
|
67
|
+
NVDA: "equity", NVO: "equity", ONDS: "equity", ORCL: "equity", PAYP: "equity", PENG: "equity", PLTR: "equity",
|
|
68
|
+
QCOM: "equity", QNTX: "equity", RDDT: "equity", RIVN: "equity", RKLB: "equity", SHAZ: "equity", SHOP: "equity",
|
|
69
|
+
SKHY: "equity", SMCI: "equity", SNDK: "equity", SNOW: "equity", SOFI: "equity", SONY: "equity", STRC: "equity",
|
|
70
|
+
STXX: "equity", TER: "equity", TSLA: "equity", TSM: "equity", TTWO: "equity", TXN: "equity", UBER: "equity",
|
|
71
|
+
USAR: "equity", VISA: "equity", WEN: "equity", WMT: "equity", ZM: "equity",
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Variational tickers whose canonical symbol is NOT the ticker:
|
|
75
|
+
* - US500-PERP is "State Street SPDR S&P 500 ETF Trust" (SPY, mark ≈ $760), not the S&P 500 index
|
|
76
|
+
* that other venues list as US500 (→ SPX).
|
|
77
|
+
* `null` = never matched across venues — the same exclusions the other venues apply:
|
|
78
|
+
* - SPX-PERP is the SPX6900 token; "SPX" is the canonical key of the S&P 500 index, and
|
|
79
|
+
* Hyperliquid / Aster / Lighter all exclude their SPX6900 (legacy-public.ts AMBIGUOUS,
|
|
80
|
+
* lighter-public.ts LIGHTER_AMBIGUOUS), so it could only ever pair with the index.
|
|
81
|
+
* - QNT-PERP is Quant (crypto); QNT is Quantinuum on Arcus / QFEX (LIGHTER_AMBIGUOUS too).
|
|
82
|
+
*
|
|
83
|
+
* Live cross-venue survey (2026-10-19, 440 Variational canonicals shared with another venue):
|
|
84
|
+
* ZETA (ZetaChain vs QFEX Zeta Global), DIA (DIA oracle vs Lighter DIA ETF) and BEAM (Beam vs QFEX
|
|
85
|
+
* Beam Global) also collide with an equity / ETF elsewhere but are the same token on the crypto
|
|
86
|
+
* venues; the wrong pairing is dropped, with a stderr warning, by the cross-venue price guard
|
|
87
|
+
* (rates.ts dropPriceOutliers / pair gap).
|
|
88
|
+
*/
|
|
89
|
+
const CANONICAL_OVERRIDE = {
|
|
90
|
+
US500: "SPY",
|
|
91
|
+
SPX: null,
|
|
92
|
+
QNT: null,
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Hint for a cross-venue lookup by a Variational ticker whose canonical symbol differs (arb scan
|
|
96
|
+
* --compare US500): where it is compared instead, or that it is never matched. null = no override.
|
|
97
|
+
*/
|
|
98
|
+
export function canonicalHintFor(symbol) {
|
|
99
|
+
const up = symbol.trim().toUpperCase().replace(/-PERP$/, "");
|
|
100
|
+
if (!(up in CANONICAL_OVERRIDE))
|
|
101
|
+
return null;
|
|
102
|
+
const canonical = CANONICAL_OVERRIDE[up];
|
|
103
|
+
return canonical === null
|
|
104
|
+
? `Variational's ${up}-PERP is never matched across venues (the ticker means a different asset elsewhere)`
|
|
105
|
+
: `Variational's ${up}-PERP is compared as ${canonical}: perp arb scan --compare ${canonical} --include-unverified`;
|
|
106
|
+
}
|
|
107
|
+
/** Swaps (daily-funded TradFi swaps) by class — display only: swaps never get a canonical symbol. */
|
|
108
|
+
const SWAP_CLASS = {
|
|
109
|
+
XAU: "commodity", XAG: "commodity", UKOIL: "commodity", USOIL: "commodity", HOIL: "commodity", GASOL: "commodity",
|
|
110
|
+
COCOA: "commodity", COPP: "commodity",
|
|
111
|
+
US30: "index", US100: "index", US500: "index", US400: "index", US2K: "index", CNA50: "index", TWI: "index",
|
|
112
|
+
USFNG: "index", VXI: "index",
|
|
113
|
+
DXI: "fx",
|
|
114
|
+
};
|
|
115
|
+
/** A listing name that reads like a company / fund (an unclassified TradFi perp). */
|
|
116
|
+
const TRADFI_NAME = /\b(Inc|Corp|Corporation|Company|Holdings?|Ltd|Limited|plc|N\.V|A\/S|Oyj|ETF|Common Stock|Class [A-Z])\b\.?/i;
|
|
117
|
+
export function looksTradFi(name) {
|
|
118
|
+
return name !== null && TRADFI_NAME.test(name);
|
|
119
|
+
}
|
|
120
|
+
/** Each distinct warning once per process (the stats body is re-parsed on every read). */
|
|
121
|
+
const warned = new Set();
|
|
122
|
+
export function warnOnce(warn, msg) {
|
|
123
|
+
if (warned.has(msg))
|
|
124
|
+
return;
|
|
125
|
+
warned.add(msg);
|
|
126
|
+
warn(msg);
|
|
127
|
+
}
|
|
128
|
+
export const QUOTE_TIERS = ["base", "size_1k", "size_100k", "size_1m"];
|
|
129
|
+
/** USD notional of each quote tier; "base" = the market's minimum order size (notional not published). */
|
|
130
|
+
export const QUOTE_TIER_NOTIONAL_USD = {
|
|
131
|
+
base: null, size_1k: "1000", size_100k: "100000", size_1m: "1000000",
|
|
132
|
+
};
|
|
133
|
+
const DEC = /^-?\d+(\.\d+)?$/;
|
|
134
|
+
const isDec = (v) => typeof v === "string" && DEC.test(v);
|
|
135
|
+
class RowError extends Error {
|
|
136
|
+
}
|
|
137
|
+
const bad = (what) => { throw new RowError(what); };
|
|
138
|
+
const dec = (v, what) => (isDec(v) ? v : bad(`${what} is not a decimal string`));
|
|
139
|
+
const obj = (v, what) => (v !== null && typeof v === "object" && !Array.isArray(v) ? v : bad(`${what} is not an object`));
|
|
140
|
+
/** Unix seconds → ms; null stays null (documented for swaps). */
|
|
141
|
+
function tsMs(v, what, nullable) {
|
|
142
|
+
if (v === null && nullable)
|
|
143
|
+
return null;
|
|
144
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v <= 0)
|
|
145
|
+
return bad(`${what} is not a unix-seconds integer`);
|
|
146
|
+
return v * 1000;
|
|
147
|
+
}
|
|
148
|
+
function perpRate(v, what) {
|
|
149
|
+
const r = obj(v, what);
|
|
150
|
+
return { annualized: dec(r.annualized_rate, `${what}.annualized_rate`), interval: dec(r.interval_rate, `${what}.interval_rate`) };
|
|
151
|
+
}
|
|
152
|
+
function swapRate(v, what) {
|
|
153
|
+
if (v === null)
|
|
154
|
+
return null;
|
|
155
|
+
const r = obj(v, what);
|
|
156
|
+
return {
|
|
157
|
+
annualizedLong: dec(r.annualized_long_rate, `${what}.annualized_long_rate`),
|
|
158
|
+
annualizedShort: dec(r.annualized_short_rate, `${what}.annualized_short_rate`),
|
|
159
|
+
intervalLong: dec(r.interval_long_rate, `${what}.interval_long_rate`),
|
|
160
|
+
intervalShort: dec(r.interval_short_rate, `${what}.interval_short_rate`),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function funding(v, kind) {
|
|
164
|
+
if (v === null)
|
|
165
|
+
return null;
|
|
166
|
+
const f = obj(v, "funding");
|
|
167
|
+
const intervalSeconds = f.interval_seconds;
|
|
168
|
+
if (typeof intervalSeconds !== "number" || !Number.isInteger(intervalSeconds) || intervalSeconds <= 0)
|
|
169
|
+
return bad("funding.interval_seconds is not a positive integer");
|
|
170
|
+
if (kind === "perpetual") {
|
|
171
|
+
return {
|
|
172
|
+
kind, intervalSeconds,
|
|
173
|
+
nextFundingTime: tsMs(f.next_timestamp, "funding.next_timestamp", false),
|
|
174
|
+
last: perpRate(f.last, "funding.last"),
|
|
175
|
+
next: f.next === null ? null : perpRate(f.next, "funding.next"),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
kind, intervalSeconds,
|
|
180
|
+
nextFundingTime: tsMs(f.next_timestamp, "funding.next_timestamp", true),
|
|
181
|
+
last: swapRate(f.last, "funding.last"),
|
|
182
|
+
next: swapRate(f.next, "funding.next"),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function quotes(v) {
|
|
186
|
+
if (v === undefined)
|
|
187
|
+
return null; // documented: omitted when unavailable (e.g. a closed swap)
|
|
188
|
+
const q = obj(v, "quotes");
|
|
189
|
+
if (typeof q.updated_at !== "string" || Number.isNaN(Date.parse(q.updated_at)))
|
|
190
|
+
return bad("quotes.updated_at is not an ISO time");
|
|
191
|
+
const tiers = [];
|
|
192
|
+
for (const tier of QUOTE_TIERS) {
|
|
193
|
+
if (q[tier] === undefined)
|
|
194
|
+
continue; // documented: individual sizes are omitted when unavailable
|
|
195
|
+
const t = obj(q[tier], `quotes.${tier}`);
|
|
196
|
+
tiers.push({ tier, notionalUsd: QUOTE_TIER_NOTIONAL_USD[tier], bid: dec(t.bid, `quotes.${tier}.bid`), ask: dec(t.ask, `quotes.${tier}.ask`) });
|
|
197
|
+
}
|
|
198
|
+
return { updatedAt: q.updated_at, tiers };
|
|
199
|
+
}
|
|
200
|
+
function classify(base, contractType, name) {
|
|
201
|
+
if (contractType === "swap") {
|
|
202
|
+
const assetClass = SWAP_CLASS[base];
|
|
203
|
+
return assetClass === undefined ? null : { assetClass, canonical: null };
|
|
204
|
+
}
|
|
205
|
+
const known = TRADFI_PERP_CLASS[base];
|
|
206
|
+
const assetClass = known ?? (looksTradFi(name) ? "equity" : "crypto");
|
|
207
|
+
if (known === undefined && assetClass === "equity")
|
|
208
|
+
return { assetClass, canonical: null }; // unclassified TradFi
|
|
209
|
+
if (base in CANONICAL_OVERRIDE)
|
|
210
|
+
return { assetClass, canonical: CANONICAL_OVERRIDE[base] };
|
|
211
|
+
return { assetClass, canonical: canonicalFor("variational", base, assetClass) };
|
|
212
|
+
}
|
|
213
|
+
/** Parse one listing. Throws RowError (caught per row by parseStats) on any shape drift. */
|
|
214
|
+
function parseListing(raw) {
|
|
215
|
+
const r = obj(raw, "listing");
|
|
216
|
+
if (typeof r.ticker !== "string")
|
|
217
|
+
return bad("ticker is not a string");
|
|
218
|
+
const m = /^(.+)-(PERP|SWAP)$/.exec(r.ticker);
|
|
219
|
+
if (m === null)
|
|
220
|
+
return bad(`ticker ${r.ticker} has no -PERP / -SWAP suffix`);
|
|
221
|
+
const contractType = r.contract_type;
|
|
222
|
+
if (contractType !== "perpetual" && contractType !== "swap")
|
|
223
|
+
return { skip: `unknown contract_type ${String(contractType)}` };
|
|
224
|
+
if ((contractType === "perpetual") !== (m[2] === "PERP"))
|
|
225
|
+
return bad(`ticker suffix ${m[2]} disagrees with contract_type ${contractType}`);
|
|
226
|
+
const base = m[1].toUpperCase();
|
|
227
|
+
const name = r.name === undefined ? null : typeof r.name === "string" ? r.name.replace(//g, "").trim() : bad("name is not a string");
|
|
228
|
+
const markPrice = dec(r.mark_price, "mark_price");
|
|
229
|
+
if (!(Number(markPrice) > 0))
|
|
230
|
+
return { skip: "no positive mark price" };
|
|
231
|
+
const oi = obj(r.open_interest, "open_interest");
|
|
232
|
+
if (!("funding" in r))
|
|
233
|
+
return bad("funding is missing (documented as always present)");
|
|
234
|
+
const cls = classify(base, contractType, name);
|
|
235
|
+
if (cls === null)
|
|
236
|
+
return { skip: "unclassified swap market" };
|
|
237
|
+
return {
|
|
238
|
+
ticker: r.ticker,
|
|
239
|
+
contractType,
|
|
240
|
+
symbol: contractType === "perpetual" ? base : r.ticker,
|
|
241
|
+
base,
|
|
242
|
+
name,
|
|
243
|
+
markPrice,
|
|
244
|
+
volume24h: dec(r.volume_24h, "volume_24h"),
|
|
245
|
+
longOpenInterestUsd: dec(oi.long_open_interest, "open_interest.long_open_interest"),
|
|
246
|
+
shortOpenInterestUsd: dec(oi.short_open_interest, "open_interest.short_open_interest"),
|
|
247
|
+
funding: funding(r.funding, contractType),
|
|
248
|
+
baseSpreadBps: r.base_spread_bps === undefined ? null : dec(r.base_spread_bps, "base_spread_bps"),
|
|
249
|
+
quotes: quotes(r.quotes),
|
|
250
|
+
assetClass: cls.assetClass,
|
|
251
|
+
canonical: cls.canonical,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Parse the stats payload. A non-object payload / missing listings array is EXCHANGE_ERROR; a
|
|
256
|
+
* single malformed or unclassifiable listing is dropped with a warning naming it (never guessed).
|
|
257
|
+
*/
|
|
258
|
+
export function parseVariationalStats(body, warn) {
|
|
259
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
260
|
+
throw variationalShapeError("payload is not an object");
|
|
261
|
+
const listings = body.listings;
|
|
262
|
+
if (!Array.isArray(listings))
|
|
263
|
+
throw variationalShapeError("payload has no listings array");
|
|
264
|
+
const out = [];
|
|
265
|
+
const dropped = [];
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
for (const raw of listings) {
|
|
268
|
+
const ticker = typeof raw?.ticker === "string" ? raw.ticker : "?";
|
|
269
|
+
try {
|
|
270
|
+
const l = parseListing(raw);
|
|
271
|
+
if ("skip" in l) {
|
|
272
|
+
dropped.push(`${ticker} (${l.skip})`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (seen.has(l.symbol)) {
|
|
276
|
+
dropped.push(`${ticker} (listed twice)`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
seen.add(l.symbol);
|
|
280
|
+
out.push(l);
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
if (!(e instanceof RowError))
|
|
284
|
+
throw e;
|
|
285
|
+
dropped.push(`${ticker} (${e.message})`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (dropped.length > 0)
|
|
289
|
+
warnOnce(warn, `[variational] dropped ${dropped.length} listing(s): ${dropped.join(", ")}`);
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
// ── Funding ──
|
|
293
|
+
/** interval_rate agrees with annualized_rate × interval / 365 d (both published, both rounded). */
|
|
294
|
+
export function perpRateConsistent(rate, intervalSeconds) {
|
|
295
|
+
const interval = Number(rate.interval);
|
|
296
|
+
const expected = Number(rate.annualized) * intervalSeconds / YEAR_SECONDS;
|
|
297
|
+
return Math.abs(interval - expected) <= Math.max(RATE_IDENTITY_ABS_TOL, Math.abs(expected) * RATE_IDENTITY_REL_TOL);
|
|
298
|
+
}
|
|
299
|
+
/** Basis of one perp's interval_rate: perHours(interval_seconds / 3600). */
|
|
300
|
+
export function fundingBasisOf(f) {
|
|
301
|
+
return { kind: "perHours", hours: f.intervalSeconds / 3600 };
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The current (predicted) funding rate per interval, or null: swaps (no single rate), no funding
|
|
305
|
+
* terms, or a perp whose two published representations disagree (unit drift — warned).
|
|
306
|
+
*/
|
|
307
|
+
export function currentPerpRate(l, warn) {
|
|
308
|
+
const f = l.funding;
|
|
309
|
+
if (f === null || f.kind !== "perpetual")
|
|
310
|
+
return null;
|
|
311
|
+
if (!perpRateConsistent(f.last, f.intervalSeconds)) {
|
|
312
|
+
warnOnce(warn, `[variational] ${l.ticker}: interval_rate ${f.last.interval} disagrees with annualized_rate ${f.last.annualized} over ${f.intervalSeconds}s — funding not shown`);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return f.last.interval;
|
|
316
|
+
}
|
|
317
|
+
function clientFor(net) {
|
|
318
|
+
return new VariationalClient(networkConfig(variationalVenue, net).restUrl, variationalVenue.rateLimit); // testnet → NOT_IMPLEMENTED
|
|
319
|
+
}
|
|
320
|
+
const DEFAULT_DEPS = {
|
|
321
|
+
stats: net => clientFor(net).stats(),
|
|
322
|
+
cache: { get: key => getCached(key), set: (key, value, ttl) => setCached(key, value, ttl) },
|
|
323
|
+
warn: msg => process.stderr.write(`${msg}\n`),
|
|
324
|
+
};
|
|
325
|
+
const inflight = new Map();
|
|
326
|
+
export const statsCacheKey = (net) => `pub:variational:${net}:stats`;
|
|
327
|
+
/**
|
|
328
|
+
* The raw stats body: from the cache when fresh, else ONE request shared by every concurrent
|
|
329
|
+
* caller in this process. Only the raw JSON body is cached (JSON-safe); parsing runs per read.
|
|
330
|
+
*/
|
|
331
|
+
export function fetchVariationalStatsRaw(net, deps = DEFAULT_DEPS) {
|
|
332
|
+
const key = statsCacheKey(net);
|
|
333
|
+
const pending = inflight.get(key);
|
|
334
|
+
if (pending !== undefined)
|
|
335
|
+
return pending;
|
|
336
|
+
const p = (async () => {
|
|
337
|
+
const hit = deps.cache.get(key);
|
|
338
|
+
if (hit !== null && hit !== undefined)
|
|
339
|
+
return hit;
|
|
340
|
+
const body = await deps.stats(net);
|
|
341
|
+
// Only a body of the documented shape is shared; drift fails this read and is never cached.
|
|
342
|
+
if (!Array.isArray(body?.listings))
|
|
343
|
+
throw variationalShapeError("payload has no listings array");
|
|
344
|
+
deps.cache.set(key, body, VARIATIONAL_STATS_TTL_MS);
|
|
345
|
+
return body;
|
|
346
|
+
})().finally(() => { inflight.delete(key); });
|
|
347
|
+
inflight.set(key, p);
|
|
348
|
+
return p;
|
|
349
|
+
}
|
|
350
|
+
export async function fetchVariationalListings(net, deps = DEFAULT_DEPS) {
|
|
351
|
+
return parseVariationalStats(await fetchVariationalStatsRaw(net, deps), deps.warn);
|
|
352
|
+
}
|
|
353
|
+
// ── Mapping ──
|
|
354
|
+
/** null when the listing has no funding terms (its interval — part of VenueMarket — is unknown). */
|
|
355
|
+
export function toVariationalVenueMarket(l, warn) {
|
|
356
|
+
if (l.funding === null)
|
|
357
|
+
return null;
|
|
358
|
+
return {
|
|
359
|
+
venue: "variational",
|
|
360
|
+
venueSymbol: l.ticker,
|
|
361
|
+
symbol: l.symbol,
|
|
362
|
+
canonical: l.canonical,
|
|
363
|
+
assetClass: l.assetClass,
|
|
364
|
+
settle: "USDC",
|
|
365
|
+
quoteAsset: "USDC",
|
|
366
|
+
markPrice: l.markPrice,
|
|
367
|
+
// No index / oracle price is published.
|
|
368
|
+
indexPrice: null,
|
|
369
|
+
fundingRate: currentPerpRate(l, warn),
|
|
370
|
+
fundingRateBasis: fundingBasisOf(l.funding),
|
|
371
|
+
settlementIntervalHours: l.funding.intervalSeconds / 3600,
|
|
372
|
+
// Published in USD and single-sided only; base-unit OI is unknown.
|
|
373
|
+
openInterest: null,
|
|
374
|
+
volume24h: l.volume24h,
|
|
375
|
+
tickSize: null,
|
|
376
|
+
stepSize: null,
|
|
377
|
+
minSize: null,
|
|
378
|
+
minNotional: null,
|
|
379
|
+
maxLeverage: null,
|
|
380
|
+
marginTiers: null,
|
|
381
|
+
session: null,
|
|
382
|
+
// Quotes are omitted while a market is closed: no quote → not tradable right now.
|
|
383
|
+
tradable: l.quotes !== null && l.quotes.tiers.length > 0,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
export async function fetchVariationalMarkets(net, deps = DEFAULT_DEPS) {
|
|
387
|
+
const out = [];
|
|
388
|
+
const noFunding = [];
|
|
389
|
+
for (const l of await fetchVariationalListings(net, deps)) {
|
|
390
|
+
const m = toVariationalVenueMarket(l, deps.warn);
|
|
391
|
+
if (m === null)
|
|
392
|
+
noFunding.push(l.ticker);
|
|
393
|
+
else
|
|
394
|
+
out.push(m);
|
|
395
|
+
}
|
|
396
|
+
if (noFunding.length > 0)
|
|
397
|
+
warnOnce(deps.warn, `[variational] ${noFunding.length} market(s) publish no funding terms (interval unknown) and are not shown here: ${noFunding.join(", ")}`);
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
export async function fetchVariationalFundingRates(net, deps = DEFAULT_DEPS) {
|
|
401
|
+
const out = [];
|
|
402
|
+
for (const l of await fetchVariationalListings(net, deps)) {
|
|
403
|
+
const m = toVariationalVenueMarket(l, deps.warn);
|
|
404
|
+
if (m === null || !m.tradable || m.fundingRate === null || l.funding === null)
|
|
405
|
+
continue;
|
|
406
|
+
out.push({
|
|
407
|
+
venue: "variational",
|
|
408
|
+
venueSymbol: m.venueSymbol,
|
|
409
|
+
symbol: m.symbol,
|
|
410
|
+
canonical: m.canonical,
|
|
411
|
+
rate: m.fundingRate,
|
|
412
|
+
basis: m.fundingRateBasis,
|
|
413
|
+
markPrice: m.markPrice,
|
|
414
|
+
nextFundingTime: l.funding.nextFundingTime,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
export const variationalPublicData = {
|
|
420
|
+
fetchMarkets: net => fetchVariationalMarkets(net),
|
|
421
|
+
fetchFundingRates: net => fetchVariationalFundingRates(net),
|
|
422
|
+
};
|
|
423
|
+
/** Resolve a CLI symbol ("btc", "BTC-PERP", "xau-swap") against the parsed listings. */
|
|
424
|
+
export function resolveVariationalListing(listings, input) {
|
|
425
|
+
const up = input.trim().toUpperCase();
|
|
426
|
+
const hit = listings.find(l => l.ticker === up || l.symbol === up);
|
|
427
|
+
if (hit === undefined)
|
|
428
|
+
throw symbolNotFound("variational", input, `Variational has no listed market ${input}`);
|
|
429
|
+
return hit;
|
|
430
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Venue-level data contracts
|
|
2
|
+
* Venue-level data contracts.
|
|
3
3
|
*
|
|
4
4
|
* SSOT Rule #2: every "unknown" value is `T | null`, never optional, never a
|
|
5
5
|
* silently substituted default.
|
|
6
6
|
*/
|
|
7
|
-
import type { ExchangeMarketInfo } from "./interface.js";
|
|
7
|
+
import type { ExchangeMarketInfo, FundingRateKind } from "./interface.js";
|
|
8
8
|
import type { ResolvedSlippage, SlippageSource } from "./slippage.js";
|
|
9
9
|
import type { AppliedBuilderFee } from "./builder-fee.js";
|
|
10
|
-
export type VenueId = "pacifica" | "hyperliquid" | "lighter" | "aster" | "lighter-rh" | "ondo" | "arcus" | "qfex";
|
|
10
|
+
export type VenueId = "pacifica" | "hyperliquid" | "lighter" | "aster" | "lighter-rh" | "ondo" | "arcus" | "qfex" | "risex" | "txflow" | "variational" | "grvt" | "extended";
|
|
11
11
|
export type LegacyVenueId = "pacifica" | "hyperliquid" | "lighter" | "aster";
|
|
12
12
|
export declare const VENUE_IDS: readonly VenueId[];
|
|
13
13
|
export declare const LEGACY_VENUE_IDS: readonly LegacyVenueId[];
|
|
@@ -23,7 +23,12 @@ export type FundingRateBasis = {
|
|
|
23
23
|
};
|
|
24
24
|
/** Convert a raw quoted rate to a per-hour rate. */
|
|
25
25
|
export declare function rateToHourly(rate: number, basis: FundingRateBasis): number;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* "unclassified": listed, but its class is ambiguous (a ticker that names different instruments
|
|
28
|
+
* on different venues: SPX6900 vs the S&P 500, Pacifica BP / CHIP, Lighter BB / BOT / WEN / QNT,
|
|
29
|
+
* Lighter RH USELESS). Its canonical is always null, so it is never matched across venues.
|
|
30
|
+
*/
|
|
31
|
+
export type AssetClass = "crypto" | "equity" | "index" | "etf" | "commodity" | "fx" | "preIpo" | "unclassified";
|
|
27
32
|
export interface MarketSession {
|
|
28
33
|
/** Whether the underlying (e.g. US equity) market is open right now. */
|
|
29
34
|
underlyingOpen: boolean;
|
|
@@ -48,7 +53,7 @@ export interface VenueMarket {
|
|
|
48
53
|
/**
|
|
49
54
|
* Currency prices are quoted in ("USD", "KRW", ...); null = this venue's data path does not
|
|
50
55
|
* record it. A non-USD quote is never traded through perp-cli (sizing, risk limits and
|
|
51
|
-
* notionals assume USD) — see QFEX
|
|
56
|
+
* notionals assume USD) — see QFEX,.
|
|
52
57
|
*/
|
|
53
58
|
quoteAsset: string | null;
|
|
54
59
|
markPrice: string;
|
|
@@ -73,7 +78,18 @@ export interface VenueMarket {
|
|
|
73
78
|
session: MarketSession | null;
|
|
74
79
|
/** false = listed but not tradable right now (offline / paused / corporate action). */
|
|
75
80
|
tradable: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The venue's listing status as the CLI market row shows it (Extended "ACTIVE" / "REDUCE_ONLY",
|
|
83
|
+
* Ondo "active" / …, QFEX "active" / "nonUsdQuote"); absent = the venue states none.
|
|
84
|
+
*/
|
|
85
|
+
status?: string | null;
|
|
76
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Quantity decimals of a lot step, or null when the step is not a power of ten ≤ 1 ("0.001" → 3,
|
|
89
|
+
* "1" → 0; TxFlow "10" / "100" and a "0.5" step → null): a decimals count cannot express them, and
|
|
90
|
+
* 0 next to stepSize "10" read as "any integer". `stepSize` is the authoritative grid.
|
|
91
|
+
*/
|
|
92
|
+
export declare function decimalsOf(step: string): number | null;
|
|
77
93
|
/** Pure mapping used by new-venue adapters to serve the legacy getMarkets(). */
|
|
78
94
|
/** Tick/step/leverage required for order entry. Throws when the venue did not publish them. */
|
|
79
95
|
export declare function requireTradingPrecision(m: VenueMarket): {
|
|
@@ -81,16 +97,35 @@ export declare function requireTradingPrecision(m: VenueMarket): {
|
|
|
81
97
|
stepSize: string;
|
|
82
98
|
maxLeverage: number;
|
|
83
99
|
};
|
|
100
|
+
export declare function fundingRateKindOf(venue: VenueMarket["venue"], fundingRate: string | null): FundingRateKind | null;
|
|
101
|
+
/**
|
|
102
|
+
* THE market-row shape (`perp market list` / `info` / `prices`, ExchangeMarketInfo) of a
|
|
103
|
+
* VenueMarket — the one mapper for the CLI adapters of VenueMarket venues and the keyless MCP
|
|
104
|
+
* `get_markets` public directory rows. It does not require trading precision: a public row
|
|
105
|
+
* without a published tick / lot / leverage maps those to null.
|
|
106
|
+
*/
|
|
107
|
+
export declare function venueMarketRow(m: VenueMarket): ExchangeMarketInfo & {
|
|
108
|
+
tradable: boolean;
|
|
109
|
+
quoteAsset: string | null;
|
|
110
|
+
status: string | null;
|
|
111
|
+
};
|
|
112
|
+
/** venueMarketRow for an adapter that trades the market: tick / lot / leverage must be published. */
|
|
84
113
|
export declare function toLegacyMarketInfo(m: VenueMarket): ExchangeMarketInfo;
|
|
85
114
|
export interface VenueFundingRate {
|
|
86
115
|
venue: VenueId;
|
|
87
116
|
venueSymbol: string;
|
|
88
117
|
symbol: string;
|
|
89
118
|
canonical: string | null;
|
|
90
|
-
|
|
119
|
+
/** null = no funding right now (`marketClosed` true: outside the market's funding hours). */
|
|
120
|
+
rate: string | null;
|
|
91
121
|
basis: FundingRateBasis;
|
|
92
122
|
markPrice: string | null;
|
|
93
123
|
nextFundingTime: number | null;
|
|
124
|
+
/**
|
|
125
|
+
* true = the current funding window is outside the market's funding hours (QFEX market_hours):
|
|
126
|
+
* `rate` is null — the venue's 0 is "no funding", not a 0 rate. Absent = funds around the clock.
|
|
127
|
+
*/
|
|
128
|
+
marketClosed?: boolean;
|
|
94
129
|
}
|
|
95
130
|
/** CLI-level TIF inputs. "TOB" = Pacifica top-of-book, "GTX" = Aster post-only. */
|
|
96
131
|
export type TimeInForce = "GTC" | "GTT" | "IOC" | "FOK" | "ALO" | "TOB" | "GTX";
|
|
@@ -111,7 +146,7 @@ export interface OrderRequest {
|
|
|
111
146
|
/** Required when the venue's orderAck is "async". */
|
|
112
147
|
clientOrderId: string | null;
|
|
113
148
|
/**
|
|
114
|
-
* Resolved slippage bound (resolveSlippage()
|
|
149
|
+
* Resolved slippage bound (resolveSlippage()). Required for
|
|
115
150
|
* type "market"; adapters whose stopMarket / TP-SL legs are protection-priced
|
|
116
151
|
* (Arcus) require it there too. null only for orders that carry no market leg.
|
|
117
152
|
*/
|
|
@@ -138,13 +173,12 @@ export interface OrderResult {
|
|
|
138
173
|
/** Filled base size; null = the venue response does not report it (never "0" as a stand-in). */
|
|
139
174
|
filledSize: string | null;
|
|
140
175
|
avgPrice: string | null;
|
|
141
|
-
/** How the venue adapter actually executed the request
|
|
176
|
+
/** How the venue adapter actually executed the request. */
|
|
142
177
|
execution: OrderExecution;
|
|
143
178
|
venueRaw: unknown;
|
|
144
179
|
}
|
|
145
180
|
/**
|
|
146
|
-
* Transparent record of the venue-specific handling behind a uniform order
|
|
147
|
-
* (design §13.1 #3, §13.2, §13.10 #14). Every field is required; a value the
|
|
181
|
+
* Transparent record of the venue-specific handling behind a uniform order. Every field is required; a value the
|
|
148
182
|
* venue / adapter did not report is null (SSOT Rule #2), never a guess.
|
|
149
183
|
*/
|
|
150
184
|
export interface OrderExecution {
|
|
@@ -229,3 +263,11 @@ export interface LeverageResult {
|
|
|
229
263
|
export declare function orderRefOf(r: Pick<OrderResult, "orderId" | "clientOrderId">): OrderRef;
|
|
230
264
|
/** Parse a CLI order reference: "c:<id>" → clientOrderId, otherwise orderId. */
|
|
231
265
|
export declare function parseOrderRef(s: string): OrderRef;
|
|
266
|
+
/**
|
|
267
|
+
* Next funding settlement of a venue that documents hourly settlement on the hour (UTC): the next
|
|
268
|
+
* top of the hour after `now`. Used only where the venue's docs say so — Hyperliquid ("paid every
|
|
269
|
+
* hour"), Lighter ("at each hour mark"), Pacifica ("at the end of each 1-hour interval") — and
|
|
270
|
+
* their settled history is stamped on the hour (live 2026-10-19: HL 18:00:00.139 … 21:00:00.032,
|
|
271
|
+
* Pacifica 18:00:02.674 … 21:00:02.574). Other venues keep null unless they publish the time.
|
|
272
|
+
*/
|
|
273
|
+
export declare function nextTopOfHourMs(now?: number): number;
|