perp-cli 0.14.2 → 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 +9 -9
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
- package/dist/bot/strategies/funding-arb-utils.js +43 -24
- 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 +23 -12
- 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 +168 -36
- 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/dashboard/ui.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 +9 -4
- package/dist/exchanges/aster.js +125 -55
- 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 -16
- package/dist/exchanges/hyperliquid.js +80 -55
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +97 -8
- package/dist/exchanges/interface.js +19 -2
- 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.d.ts +1 -1
- package/dist/trade-validator.js +9 -4
- 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
|
@@ -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,5 +1,5 @@
|
|
|
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.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { ExchangeMarketInfo } 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[];
|
|
@@ -48,7 +48,7 @@ export interface VenueMarket {
|
|
|
48
48
|
/**
|
|
49
49
|
* Currency prices are quoted in ("USD", "KRW", ...); null = this venue's data path does not
|
|
50
50
|
* record it. A non-USD quote is never traded through perp-cli (sizing, risk limits and
|
|
51
|
-
* notionals assume USD) — see QFEX
|
|
51
|
+
* notionals assume USD) — see QFEX,.
|
|
52
52
|
*/
|
|
53
53
|
quoteAsset: string | null;
|
|
54
54
|
markPrice: string;
|
|
@@ -111,7 +111,7 @@ export interface OrderRequest {
|
|
|
111
111
|
/** Required when the venue's orderAck is "async". */
|
|
112
112
|
clientOrderId: string | null;
|
|
113
113
|
/**
|
|
114
|
-
* Resolved slippage bound (resolveSlippage()
|
|
114
|
+
* Resolved slippage bound (resolveSlippage()). Required for
|
|
115
115
|
* type "market"; adapters whose stopMarket / TP-SL legs are protection-priced
|
|
116
116
|
* (Arcus) require it there too. null only for orders that carry no market leg.
|
|
117
117
|
*/
|
|
@@ -138,13 +138,12 @@ export interface OrderResult {
|
|
|
138
138
|
/** Filled base size; null = the venue response does not report it (never "0" as a stand-in). */
|
|
139
139
|
filledSize: string | null;
|
|
140
140
|
avgPrice: string | null;
|
|
141
|
-
/** How the venue adapter actually executed the request
|
|
141
|
+
/** How the venue adapter actually executed the request. */
|
|
142
142
|
execution: OrderExecution;
|
|
143
143
|
venueRaw: unknown;
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
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
|
|
146
|
+
* Transparent record of the venue-specific handling behind a uniform order. Every field is required; a value the
|
|
148
147
|
* venue / adapter did not report is null (SSOT Rule #2), never a guess.
|
|
149
148
|
*/
|
|
150
149
|
export interface OrderExecution {
|
|
@@ -1,12 +1,12 @@
|
|
|
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
7
|
export const VENUE_IDS = [
|
|
8
8
|
"pacifica", "hyperliquid", "lighter", "aster",
|
|
9
|
-
"lighter-rh", "ondo", "arcus", "qfex",
|
|
9
|
+
"lighter-rh", "ondo", "arcus", "qfex", "risex", "txflow", "variational", "grvt", "extended",
|
|
10
10
|
];
|
|
11
11
|
export const LEGACY_VENUE_IDS = ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
12
12
|
export function isVenueId(s) {
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Venue definitions — the single source of truth for "which venue, on which
|
|
3
3
|
* network, signed how, funded how, supporting what".
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* One adapter class may serve several
|
|
6
6
|
* venues (LighterAdapter serves `lighter` and `lighter-rh`).
|
|
7
7
|
*
|
|
8
8
|
* Venue definition modules must load adapter code lazily (`import()` inside
|
|
9
9
|
* `create`) so that public-data paths never pull in heavy SDKs (lighter WASM).
|
|
10
10
|
*/
|
|
11
|
+
import { PerpError } from "../errors.js";
|
|
11
12
|
import type { ExchangeAdapter } from "./interface.js";
|
|
12
13
|
import type { SpotAdapter } from "./spot-interface.js";
|
|
13
14
|
import type { FundingRateBasis, LegacyVenueId, Network, TimeInForce, VenueFundingRate, VenueId, VenueMarket } from "./venue-types.js";
|
|
@@ -127,11 +128,29 @@ export interface VenueCreateContext {
|
|
|
127
128
|
export declare const READ_ONLY_CREDENTIALS: VenueCredentials;
|
|
128
129
|
export type VenueRole = "marketData" | "account" | "trading";
|
|
129
130
|
export type SymbolIdentity = "baseTicker" | "explicit";
|
|
131
|
+
/**
|
|
132
|
+
* How perp-cli reaches the venue. "official" = a documented public API. "unofficial" = an
|
|
133
|
+
* undocumented API (e.g. the one the venue's own web app uses) that may change or be blocked
|
|
134
|
+
* without notice; every user-facing surface (JSON `venueApi` / `apiStatus`, human footnotes,
|
|
135
|
+
* MCP output, the landing screen, the skill docs) labels data read through it.
|
|
136
|
+
*/
|
|
137
|
+
export type VenueApiStatus = {
|
|
138
|
+
kind: "official";
|
|
139
|
+
} | {
|
|
140
|
+
kind: "unofficial";
|
|
141
|
+
note: string;
|
|
142
|
+
docs: string | null;
|
|
143
|
+
};
|
|
144
|
+
export declare const OFFICIAL_API: VenueApiStatus;
|
|
145
|
+
/** "official" | "unofficial" — the per-row label for mixed-venue tables and JSON. */
|
|
146
|
+
export declare function apiStatusOf(venue: VenueId): VenueApiStatus["kind"];
|
|
147
|
+
/** Human footnote for an unofficial-API venue; null for an official one. */
|
|
148
|
+
export declare function unofficialApiNote(def: VenueDefinition): string | null;
|
|
130
149
|
export interface VenueDefinition {
|
|
131
150
|
id: VenueId;
|
|
132
151
|
aliases: readonly string[];
|
|
133
152
|
displayName: string;
|
|
134
|
-
family: "pacifica" | "hyperliquid" | "lighter" | "aster" | "ondo" | "arcus" | "qfex";
|
|
153
|
+
family: "pacifica" | "hyperliquid" | "lighter" | "aster" | "ondo" | "arcus" | "qfex" | "risex" | "txflow" | "variational" | "grvt" | "extended";
|
|
135
154
|
masterKeyKind: MasterKeyKind;
|
|
136
155
|
agentKeyKind: AgentKeyKind;
|
|
137
156
|
secretStorage: SecretStorage;
|
|
@@ -143,12 +162,22 @@ export interface VenueDefinition {
|
|
|
143
162
|
settlementIntervalHours: number | "perMarket";
|
|
144
163
|
/**
|
|
145
164
|
* false = the unit / meaning of the venue's published funding rate is not verified against
|
|
146
|
-
* settled funding payments yet
|
|
165
|
+
* settled funding payments yet. Such a venue is left out of cross-venue
|
|
147
166
|
* funding comparison (fetchVenuesRates drops it unless includeUnverified is passed), its
|
|
148
167
|
* adapter reports fundingRate null to bots, and funding-arb strategies refuse it as a leg.
|
|
149
168
|
*/
|
|
150
169
|
fundingVerified: boolean;
|
|
151
170
|
roles: readonly VenueRole[];
|
|
171
|
+
/**
|
|
172
|
+
* Roles that are implemented but switched off in this release (phased venue rollout, e.g.
|
|
173
|
+
* RISEx trading in 0.15.0). They are NOT in `roles`; roleUnsupported() uses `reason` /
|
|
174
|
+
* `remediation` for them. null = nothing held back.
|
|
175
|
+
*/
|
|
176
|
+
gatedRoles: {
|
|
177
|
+
roles: readonly VenueRole[];
|
|
178
|
+
reason: string;
|
|
179
|
+
remediation: string;
|
|
180
|
+
} | null;
|
|
152
181
|
/** null when the venue has no trading role. */
|
|
153
182
|
capabilities: VenueCapabilities | null;
|
|
154
183
|
rateLimit: RateLimitPolicy;
|
|
@@ -159,11 +188,10 @@ export interface VenueDefinition {
|
|
|
159
188
|
publicData: VenuePublicData;
|
|
160
189
|
/** null when the venue cannot produce an adapter (public data only). */
|
|
161
190
|
create: ((ctx: VenueCreateContext) => Promise<ExchangeAdapter>) | null;
|
|
162
|
-
/** null = venue has no spot trading
|
|
191
|
+
/** null = venue has no spot trading. */
|
|
163
192
|
spot: VenueSpotDefinition | null;
|
|
164
193
|
/**
|
|
165
|
-
* Largest slippage bound (bps) a market-type order may carry on this venue
|
|
166
|
-
* (design §13.10 #1). resolveSlippage() rejects anything above it with
|
|
194
|
+
* Largest slippage bound (bps) a market-type order may carry on this venue. resolveSlippage() rejects anything above it with
|
|
167
195
|
* INVALID_PARAMS before sending — never clamps.
|
|
168
196
|
*/
|
|
169
197
|
maxSlippageBps: number;
|
|
@@ -173,6 +201,13 @@ export interface VenueDefinition {
|
|
|
173
201
|
* settings.builder.<venue> and the approval is confirmed.
|
|
174
202
|
*/
|
|
175
203
|
builderFee: BuilderFeeCapability | null;
|
|
204
|
+
/** Official documented API, or an unofficial one that is labelled everywhere (VenueApiStatus). */
|
|
205
|
+
apiStatus: VenueApiStatus;
|
|
206
|
+
/**
|
|
207
|
+
* Remediation of every role refusal (roleUnsupported) on a venue that is read-only in this
|
|
208
|
+
* version (Variational, GRVT, Extended). Absent = the generic market-data-only remediation.
|
|
209
|
+
*/
|
|
210
|
+
readOnlyRemediation?: string;
|
|
176
211
|
}
|
|
177
212
|
export declare function registerVenue(def: VenueDefinition): void;
|
|
178
213
|
/** Resolve a venue id or alias. Throws UNKNOWN_VENUE. */
|
|
@@ -182,6 +217,14 @@ export declare function getVenue(nameOrAlias: string): VenueDefinition;
|
|
|
182
217
|
export declare function listVenues(): VenueDefinition[];
|
|
183
218
|
export declare function listVenuesWithRole(role: VenueRole): VenueDefinition[];
|
|
184
219
|
export declare function networkConfig(def: VenueDefinition, net: Network): VenueNetworkConfig;
|
|
220
|
+
/**
|
|
221
|
+
* The uniform refusal for any account / trading / funds / agent / builder / bot action on a venue
|
|
222
|
+
* whose roles do not include `role` (a market-data-only venue such as TxFlow). Raised before any
|
|
223
|
+
* credential is resolved or any request is sent.
|
|
224
|
+
*/
|
|
225
|
+
export declare function roleUnsupported(def: VenueDefinition, feature: string, role?: VenueRole): PerpError;
|
|
226
|
+
/** Throws roleUnsupported() unless the venue has `role`. */
|
|
227
|
+
export declare function requireRole(def: VenueDefinition, role: VenueRole, feature: string): void;
|
|
185
228
|
export declare function requireCapabilities(def: VenueDefinition): VenueCapabilities;
|
|
186
229
|
/**
|
|
187
230
|
* A venue reference as it appears in stored data and funding keys:
|
|
@@ -194,7 +237,7 @@ export interface VenueRef {
|
|
|
194
237
|
export declare function parseVenueRef(ref: string): VenueRef;
|
|
195
238
|
/**
|
|
196
239
|
* Funding basis for a venue ref. HIP-3 dexes use the Hyperliquid core basis
|
|
197
|
-
* (current behavior preserved;
|
|
240
|
+
* (current behavior preserved; verify against live data).
|
|
198
241
|
* Throws for venues whose basis is per-market; callers must use the market's value.
|
|
199
242
|
*/
|
|
200
243
|
export declare function fundingBasisOf(ref: string): FundingRateBasis;
|
|
@@ -202,7 +245,7 @@ export declare function settlementIntervalOf(ref: string): number;
|
|
|
202
245
|
/**
|
|
203
246
|
* Refuse a venue whose funding rate is unverified (VenueDefinition.fundingVerified false) as a
|
|
204
247
|
* leg of a funding strategy (arb scan ranking, arb exec / auto, funding bots). Uniform
|
|
205
|
-
* CAPABILITY_UNSUPPORTED
|
|
248
|
+
* CAPABILITY_UNSUPPORTED. Unknown venue refs throw UNKNOWN_VENUE.
|
|
206
249
|
*/
|
|
207
250
|
export declare function assertFundingArbVenue(ref: string): void;
|
|
208
251
|
export declare function isLegacyVenue(def: VenueDefinition): def is VenueDefinition & {
|