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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended public-only adapter — official API, MARKET DATA ONLY in this version (venues/extended.ts).
|
|
3
|
+
*
|
|
4
|
+
* Market data (markets, orderbook, trades, funding history, candles) is implemented with the
|
|
5
|
+
* documented public endpoints (https://api.docs.extended.exchange/ "Public REST-API"). Every
|
|
6
|
+
* account / trading method refuses with CAPABILITY_UNSUPPORTED before any request — there is no
|
|
7
|
+
* Extended account, order or signing code.
|
|
8
|
+
*
|
|
9
|
+
* A symbol is resolved first (resolveExtendedMarketRemote: `?market=<name>` reads, the full listing
|
|
10
|
+
* only when no wire-name candidate matches), so an unlisted symbol is SYMBOL_NOT_FOUND before any
|
|
11
|
+
* per-market request (the orderbook of an unknown market answers `{"status":"OK"}` without data,
|
|
12
|
+
* its trades `[]` — live 2026-10-19).
|
|
13
|
+
*/
|
|
14
|
+
import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade } from "../interface.js";
|
|
15
|
+
import { type VenueDefinition } from "../venue.js";
|
|
16
|
+
import { type AssetClass, type Network } from "../venue-types.js";
|
|
17
|
+
/** CLI candle interval → Extended ISO-8601 duration (docs "Get candles history" / candles stream). */
|
|
18
|
+
export declare const EXTENDED_KLINE_INTERVALS: Readonly<Record<string, {
|
|
19
|
+
wire: string;
|
|
20
|
+
ms: number;
|
|
21
|
+
}>>;
|
|
22
|
+
/** Documented per-request maximum of the candles and funding-history endpoints (accepted `limit`). */
|
|
23
|
+
export declare const EXTENDED_MAX_RECORDS = 10000;
|
|
24
|
+
/**
|
|
25
|
+
* Real server caps, measured live 2026-10-19 (BTC-USD; no pagination field, no error): candles
|
|
26
|
+
* return at most 2800 rows on every interval with that much history (PT1M … PT4H: 2800; PT8H 2445,
|
|
27
|
+
* PT12H 1631, PT24H 816, P7D 118, P30D 28 = the whole history since 2024-07), funding history at
|
|
28
|
+
* most 1000 (the newest). Both page backwards with `endTime` = before the oldest row and stop only
|
|
29
|
+
* on an empty page or at the window start — a short page is never taken as the end.
|
|
30
|
+
*/
|
|
31
|
+
export declare const EXTENDED_CANDLES_PAGE_CAP = 2800;
|
|
32
|
+
export declare const EXTENDED_FUNDING_PAGE_CAP = 1000;
|
|
33
|
+
export declare const EXTENDED_TRADES_CAP = 50;
|
|
34
|
+
/** Legacy market row plus the Extended fields shown in JSON (asset class, precision, schedule). */
|
|
35
|
+
export type ExtendedMarketInfo = ExchangeMarketInfo & {
|
|
36
|
+
venueSymbol: string;
|
|
37
|
+
canonical: string | null;
|
|
38
|
+
assetClass: AssetClass;
|
|
39
|
+
tickSize: string;
|
|
40
|
+
minSize: string;
|
|
41
|
+
tradable: boolean;
|
|
42
|
+
status: "ACTIVE" | "REDUCE_ONLY";
|
|
43
|
+
isRfq: boolean;
|
|
44
|
+
tradingHours: string;
|
|
45
|
+
isOffHours: boolean;
|
|
46
|
+
/** `fundingRate` is the current hour's accruing rate (fraction per hour), applied at nextFundingTime. */
|
|
47
|
+
fundingRateKind: "accruing";
|
|
48
|
+
nextFundingTime: number | null;
|
|
49
|
+
};
|
|
50
|
+
/** One applied hourly funding print (fraction per hour) with its normalization. */
|
|
51
|
+
export interface ExtendedFundingPrint {
|
|
52
|
+
time: number;
|
|
53
|
+
rate: string;
|
|
54
|
+
price: null;
|
|
55
|
+
rateKind: "lastSettled";
|
|
56
|
+
intervalHours: 1;
|
|
57
|
+
/** rate × 24 × 365 × 100 (APR %, simple). */
|
|
58
|
+
annualizedPct: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class ExtendedPublicAdapter implements ExchangeAdapter {
|
|
61
|
+
readonly venue: VenueDefinition;
|
|
62
|
+
readonly network: Network;
|
|
63
|
+
readonly name: "extended";
|
|
64
|
+
readonly aliases: readonly ["ext", "x10"];
|
|
65
|
+
private readonly client;
|
|
66
|
+
constructor(venue: VenueDefinition, network: Network);
|
|
67
|
+
/** Per-market reads resolve the symbol with the small `?market=` read (public.ts), not the full listing. */
|
|
68
|
+
private market;
|
|
69
|
+
getMarkets(): Promise<ExtendedMarketInfo[]>;
|
|
70
|
+
getOrderbook(symbol: string): Promise<{
|
|
71
|
+
bids: [string, string][];
|
|
72
|
+
asks: [string, string][];
|
|
73
|
+
}>;
|
|
74
|
+
getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Applied (settled) hourly rates, newest first — fraction per hour; no oracle price published.
|
|
77
|
+
* Paged backwards in pages of EXTENDED_FUNDING_PAGE_CAP until `limit` rows or the window start.
|
|
78
|
+
*/
|
|
79
|
+
getFundingHistory(symbol: string, limit?: number): Promise<ExtendedFundingPrint[]>;
|
|
80
|
+
/** Candles paged backwards from `endTime` in pages of EXTENDED_CANDLES_PAGE_CAP (newest first on the wire). */
|
|
81
|
+
getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
|
|
82
|
+
private refuse;
|
|
83
|
+
getBalance(): Promise<ExchangeBalance>;
|
|
84
|
+
getPositions(): Promise<ExchangePosition[]>;
|
|
85
|
+
getOpenOrders(): Promise<ExchangeOrder[]>;
|
|
86
|
+
getOrderHistory(): Promise<ExchangeOrder[]>;
|
|
87
|
+
getTradeHistory(): Promise<ExchangeTrade[]>;
|
|
88
|
+
getFundingPayments(): Promise<ExchangeFundingPayment[]>;
|
|
89
|
+
marketOrder(): Promise<unknown>;
|
|
90
|
+
limitOrder(): Promise<unknown>;
|
|
91
|
+
editOrder(): Promise<unknown>;
|
|
92
|
+
cancelOrder(): Promise<unknown>;
|
|
93
|
+
cancelAllOrders(): Promise<unknown>;
|
|
94
|
+
setLeverage(): Promise<unknown>;
|
|
95
|
+
stopOrder(): Promise<unknown>;
|
|
96
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended public-only adapter — official API, MARKET DATA ONLY in this version (venues/extended.ts).
|
|
3
|
+
*
|
|
4
|
+
* Market data (markets, orderbook, trades, funding history, candles) is implemented with the
|
|
5
|
+
* documented public endpoints (https://api.docs.extended.exchange/ "Public REST-API"). Every
|
|
6
|
+
* account / trading method refuses with CAPABILITY_UNSUPPORTED before any request — there is no
|
|
7
|
+
* Extended account, order or signing code.
|
|
8
|
+
*
|
|
9
|
+
* A symbol is resolved first (resolveExtendedMarketRemote: `?market=<name>` reads, the full listing
|
|
10
|
+
* only when no wire-name candidate matches), so an unlisted symbol is SYMBOL_NOT_FOUND before any
|
|
11
|
+
* per-market request (the orderbook of an unknown market answers `{"status":"OK"}` without data,
|
|
12
|
+
* its trades `[]` — live 2026-10-19).
|
|
13
|
+
*/
|
|
14
|
+
import { PerpError } from "../../errors.js";
|
|
15
|
+
import { annualizeHourlyRate } from "../../funding/normalize.js";
|
|
16
|
+
import { roleUnsupported } from "../venue.js";
|
|
17
|
+
import { toLegacyMarketInfo } from "../venue-types.js";
|
|
18
|
+
import { extendedShapeError } from "./client.js";
|
|
19
|
+
import { extendedClient, fetchExtendedMarketRows, resolveExtendedMarketRemote } from "./public.js";
|
|
20
|
+
/** CLI candle interval → Extended ISO-8601 duration (docs "Get candles history" / candles stream). */
|
|
21
|
+
export const EXTENDED_KLINE_INTERVALS = {
|
|
22
|
+
"1m": { wire: "PT1M", ms: 60_000 },
|
|
23
|
+
"5m": { wire: "PT5M", ms: 300_000 },
|
|
24
|
+
"15m": { wire: "PT15M", ms: 900_000 },
|
|
25
|
+
"30m": { wire: "PT30M", ms: 1_800_000 },
|
|
26
|
+
"1h": { wire: "PT1H", ms: 3_600_000 },
|
|
27
|
+
"2h": { wire: "PT2H", ms: 7_200_000 },
|
|
28
|
+
"4h": { wire: "PT4H", ms: 14_400_000 },
|
|
29
|
+
"8h": { wire: "PT8H", ms: 28_800_000 },
|
|
30
|
+
"12h": { wire: "PT12H", ms: 43_200_000 },
|
|
31
|
+
"1d": { wire: "PT24H", ms: 86_400_000 },
|
|
32
|
+
"1w": { wire: "P7D", ms: 604_800_000 },
|
|
33
|
+
// P30D answers calendar-month candles (live 2026-10-19: opens on the 1st, 00:00 UTC); ms is
|
|
34
|
+
// the nominal 30 d used only to size requests.
|
|
35
|
+
"1M": { wire: "P30D", ms: 2_592_000_000 },
|
|
36
|
+
};
|
|
37
|
+
/** Documented per-request maximum of the candles and funding-history endpoints (accepted `limit`). */
|
|
38
|
+
export const EXTENDED_MAX_RECORDS = 10_000;
|
|
39
|
+
/**
|
|
40
|
+
* Real server caps, measured live 2026-10-19 (BTC-USD; no pagination field, no error): candles
|
|
41
|
+
* return at most 2800 rows on every interval with that much history (PT1M … PT4H: 2800; PT8H 2445,
|
|
42
|
+
* PT12H 1631, PT24H 816, P7D 118, P30D 28 = the whole history since 2024-07), funding history at
|
|
43
|
+
* most 1000 (the newest). Both page backwards with `endTime` = before the oldest row and stop only
|
|
44
|
+
* on an empty page or at the window start — a short page is never taken as the end.
|
|
45
|
+
*/
|
|
46
|
+
export const EXTENDED_CANDLES_PAGE_CAP = 2800;
|
|
47
|
+
export const EXTENDED_FUNDING_PAGE_CAP = 1000;
|
|
48
|
+
const EXTENDED_MAX_PAGES = 50;
|
|
49
|
+
export const EXTENDED_TRADES_CAP = 50;
|
|
50
|
+
const DEC = /^-?\d+(\.\d+)?$/;
|
|
51
|
+
function dec(v, what) {
|
|
52
|
+
if (typeof v !== "string" || !DEC.test(v))
|
|
53
|
+
throw extendedShapeError(`${what} is not a decimal string`);
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
function ms(v, what) {
|
|
57
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v <= 0)
|
|
58
|
+
throw extendedShapeError(`${what} is not a ms timestamp`);
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
export class ExtendedPublicAdapter {
|
|
62
|
+
venue;
|
|
63
|
+
network;
|
|
64
|
+
name = "extended";
|
|
65
|
+
aliases = ["ext", "x10"];
|
|
66
|
+
client;
|
|
67
|
+
constructor(venue, network) {
|
|
68
|
+
this.venue = venue;
|
|
69
|
+
this.network = network;
|
|
70
|
+
this.client = extendedClient(network);
|
|
71
|
+
}
|
|
72
|
+
/** Per-market reads resolve the symbol with the small `?market=` read (public.ts), not the full listing. */
|
|
73
|
+
async market(symbol) {
|
|
74
|
+
return resolveExtendedMarketRemote(this.client, symbol);
|
|
75
|
+
}
|
|
76
|
+
// ── Market data ──
|
|
77
|
+
async getMarkets() {
|
|
78
|
+
const rows = await fetchExtendedMarketRows(this.client);
|
|
79
|
+
return rows.map(({ meta, market, nextFundingTime }) => ({
|
|
80
|
+
...toLegacyMarketInfo(market),
|
|
81
|
+
venueSymbol: market.venueSymbol,
|
|
82
|
+
canonical: market.canonical,
|
|
83
|
+
assetClass: market.assetClass,
|
|
84
|
+
tickSize: market.tickSize,
|
|
85
|
+
minSize: market.minSize,
|
|
86
|
+
tradable: market.tradable,
|
|
87
|
+
status: meta.status,
|
|
88
|
+
isRfq: meta.isRfq,
|
|
89
|
+
tradingHours: meta.tradingHours,
|
|
90
|
+
isOffHours: meta.isOffHours,
|
|
91
|
+
fundingRateKind: "accruing",
|
|
92
|
+
nextFundingTime,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
async getOrderbook(symbol) {
|
|
96
|
+
const m = await this.market(symbol);
|
|
97
|
+
const body = await this.client.get(`/info/markets/${encodeURIComponent(m.name)}/orderbook`);
|
|
98
|
+
const data = body.data;
|
|
99
|
+
if (data === undefined || data === null || !Array.isArray(data.bid) || !Array.isArray(data.ask)) {
|
|
100
|
+
throw extendedShapeError(`orderbook of ${m.name} has no bid / ask lists`);
|
|
101
|
+
}
|
|
102
|
+
const side = (rows, what) => rows.map(r => {
|
|
103
|
+
const x = r;
|
|
104
|
+
return [dec(x.price, `${what} price`), dec(x.qty, `${what} qty`)];
|
|
105
|
+
});
|
|
106
|
+
return { bids: side(data.bid, "orderbook bid"), asks: side(data.ask, "orderbook ask") };
|
|
107
|
+
}
|
|
108
|
+
async getRecentTrades(symbol, limit = 50) {
|
|
109
|
+
// The endpoint takes no limit and answers the last 50 trades (live 2026-10-19, `limit=500` → 50).
|
|
110
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > EXTENDED_TRADES_CAP) {
|
|
111
|
+
throw new PerpError("INVALID_PARAMS", `Extended: trades limit must be 1..${EXTENDED_TRADES_CAP} (got ${limit})`, { remediation: `Extended publishes only the last ${EXTENDED_TRADES_CAP} trades` });
|
|
112
|
+
}
|
|
113
|
+
const m = await this.market(symbol);
|
|
114
|
+
const body = await this.client.get(`/info/markets/${encodeURIComponent(m.name)}/trades`);
|
|
115
|
+
if (!Array.isArray(body.data))
|
|
116
|
+
throw extendedShapeError(`trades of ${m.name} is not a list`);
|
|
117
|
+
const trades = body.data.map(t => {
|
|
118
|
+
const x = t;
|
|
119
|
+
if (x.S !== "BUY" && x.S !== "SELL")
|
|
120
|
+
throw extendedShapeError(`trade side ${String(x.S)}`);
|
|
121
|
+
// Public tape carries no fee.
|
|
122
|
+
return { time: ms(x.T, "trade T"), symbol: m.symbol, side: x.S === "BUY" ? "buy" : "sell", price: dec(x.p, "trade p"), size: dec(x.q, "trade q"), fee: null };
|
|
123
|
+
});
|
|
124
|
+
// Newest first, like every other venue's tape.
|
|
125
|
+
return trades.sort((a, b) => b.time - a.time).slice(0, limit);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Applied (settled) hourly rates, newest first — fraction per hour; no oracle price published.
|
|
129
|
+
* Paged backwards in pages of EXTENDED_FUNDING_PAGE_CAP until `limit` rows or the window start.
|
|
130
|
+
*/
|
|
131
|
+
async getFundingHistory(symbol, limit = 10) {
|
|
132
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > EXTENDED_MAX_RECORDS) {
|
|
133
|
+
throw new PerpError("INVALID_PARAMS", `Extended: funding history limit must be 1..${EXTENDED_MAX_RECORDS} (got ${limit})`);
|
|
134
|
+
}
|
|
135
|
+
const m = await this.market(symbol);
|
|
136
|
+
const end = Date.now();
|
|
137
|
+
// One record per hour; two spare hours cover the in-progress hour and clock skew.
|
|
138
|
+
const start = end - (limit + 2) * 3_600_000;
|
|
139
|
+
const byTime = new Map();
|
|
140
|
+
let cursor = end;
|
|
141
|
+
for (let page = 0; byTime.size < limit; page++) {
|
|
142
|
+
if (page >= EXTENDED_MAX_PAGES)
|
|
143
|
+
throw new PerpError("EXCHANGE_ERROR", `Extended: funding history of ${m.name} did not end after ${EXTENDED_MAX_PAGES} pages`);
|
|
144
|
+
const want = Math.min(EXTENDED_FUNDING_PAGE_CAP, limit - byTime.size);
|
|
145
|
+
const body = await this.client.get(`/info/${encodeURIComponent(m.name)}/funding`, [
|
|
146
|
+
["startTime", String(start)], ["endTime", String(cursor)], ["limit", String(want)],
|
|
147
|
+
]);
|
|
148
|
+
if (!Array.isArray(body.data))
|
|
149
|
+
throw extendedShapeError(`funding history of ${m.name} is not a list`);
|
|
150
|
+
const rows = body.data.map(r => {
|
|
151
|
+
const x = r;
|
|
152
|
+
const rate = dec(x.f, "funding f");
|
|
153
|
+
return { time: ms(x.T, "funding T"), rate, price: null, rateKind: "lastSettled", intervalHours: 1, annualizedPct: annualizeHourlyRate(Number(rate)) };
|
|
154
|
+
});
|
|
155
|
+
for (const r of rows)
|
|
156
|
+
byTime.set(r.time, r);
|
|
157
|
+
// Only an empty page ends the window (a short page is not trusted as the end).
|
|
158
|
+
if (rows.length === 0)
|
|
159
|
+
break;
|
|
160
|
+
const oldest = Math.min(...rows.map(r => r.time));
|
|
161
|
+
if (oldest <= start)
|
|
162
|
+
break;
|
|
163
|
+
if (oldest >= cursor)
|
|
164
|
+
throw extendedShapeError(`funding history of ${m.name} did not move back past ${new Date(cursor).toISOString()}`);
|
|
165
|
+
cursor = oldest - 1;
|
|
166
|
+
}
|
|
167
|
+
return [...byTime.values()].sort((a, b) => b.time - a.time).slice(0, limit);
|
|
168
|
+
}
|
|
169
|
+
/** Candles paged backwards from `endTime` in pages of EXTENDED_CANDLES_PAGE_CAP (newest first on the wire). */
|
|
170
|
+
async getKlines(symbol, interval, startTime, endTime) {
|
|
171
|
+
const iv = Object.prototype.hasOwnProperty.call(EXTENDED_KLINE_INTERVALS, interval) ? EXTENDED_KLINE_INTERVALS[interval] : undefined;
|
|
172
|
+
if (iv === undefined) {
|
|
173
|
+
throw new PerpError("INVALID_PARAMS", `Extended: unsupported candle interval "${interval}"`, {
|
|
174
|
+
remediation: `Use one of: ${Object.keys(EXTENDED_KLINE_INTERVALS).join(", ")}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (!(endTime > startTime))
|
|
178
|
+
throw new PerpError("INVALID_PARAMS", "Extended: candle end time must be after the start time");
|
|
179
|
+
const bars = Math.ceil((endTime - startTime) / iv.ms) + 1;
|
|
180
|
+
const pages = Math.ceil(bars / EXTENDED_CANDLES_PAGE_CAP);
|
|
181
|
+
if (pages > EXTENDED_MAX_PAGES) {
|
|
182
|
+
throw new PerpError("INVALID_PARAMS", `Extended: ${interval} candles over this range need ${pages} requests (max ${EXTENDED_MAX_PAGES})`, {
|
|
183
|
+
remediation: "Narrow --start / --end or use a longer interval",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const m = await this.market(symbol);
|
|
187
|
+
const byTime = new Map();
|
|
188
|
+
// `endTime` is exclusive on the wire (a bar opening exactly at it is not returned): +1 ms makes
|
|
189
|
+
// `--end` inclusive like every other venue.
|
|
190
|
+
let cursor = endTime + 1;
|
|
191
|
+
for (let page = 0; page < EXTENDED_MAX_PAGES + 1; page++) {
|
|
192
|
+
const want = Math.min(EXTENDED_CANDLES_PAGE_CAP, Math.ceil((cursor - startTime) / iv.ms) + 1);
|
|
193
|
+
const body = await this.client.get(`/info/candles/${encodeURIComponent(m.name)}/trades`, [
|
|
194
|
+
["interval", iv.wire], ["limit", String(want)], ["endTime", String(cursor)],
|
|
195
|
+
]);
|
|
196
|
+
if (!Array.isArray(body.data))
|
|
197
|
+
throw extendedShapeError(`candles of ${m.name} is not a list`);
|
|
198
|
+
const rows = body.data.map(c => {
|
|
199
|
+
const x = c;
|
|
200
|
+
return {
|
|
201
|
+
time: ms(x.T, "candle T"),
|
|
202
|
+
open: dec(x.o, "candle o"), high: dec(x.h, "candle h"), low: dec(x.l, "candle l"), close: dec(x.c, "candle c"),
|
|
203
|
+
volume: dec(x.v, "candle v"),
|
|
204
|
+
quoteVolume: null,
|
|
205
|
+
trades: null,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
for (const k of rows)
|
|
209
|
+
if (k.time >= startTime && k.time <= endTime)
|
|
210
|
+
byTime.set(k.time, k);
|
|
211
|
+
// Only an EMPTY page ends the history: a short page is not trusted as the end (the cap is
|
|
212
|
+
// per interval and was measured, not documented), it just costs one more request.
|
|
213
|
+
if (rows.length === 0)
|
|
214
|
+
break;
|
|
215
|
+
const oldest = Math.min(...rows.map(k => k.time));
|
|
216
|
+
if (oldest <= startTime)
|
|
217
|
+
break;
|
|
218
|
+
if (oldest >= cursor)
|
|
219
|
+
throw extendedShapeError(`candles of ${m.name} did not move back past ${new Date(cursor).toISOString()}`);
|
|
220
|
+
cursor = oldest - 1;
|
|
221
|
+
}
|
|
222
|
+
return [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
223
|
+
}
|
|
224
|
+
// ── Account / trading: refused (market data only in this version) ──
|
|
225
|
+
refuse(feature) {
|
|
226
|
+
throw roleUnsupported(this.venue, feature);
|
|
227
|
+
}
|
|
228
|
+
async getBalance() { return this.refuse("account"); }
|
|
229
|
+
async getPositions() { return this.refuse("account"); }
|
|
230
|
+
async getOpenOrders() { return this.refuse("account"); }
|
|
231
|
+
async getOrderHistory() { return this.refuse("account"); }
|
|
232
|
+
async getTradeHistory() { return this.refuse("account"); }
|
|
233
|
+
async getFundingPayments() { return this.refuse("account"); }
|
|
234
|
+
async marketOrder() { return this.refuse("trading"); }
|
|
235
|
+
async limitOrder() { return this.refuse("trading"); }
|
|
236
|
+
async editOrder() { return this.refuse("trading"); }
|
|
237
|
+
async cancelOrder() { return this.refuse("trading"); }
|
|
238
|
+
async cancelAllOrders() { return this.refuse("trading"); }
|
|
239
|
+
async setLeverage() { return this.refuse("trading"); }
|
|
240
|
+
async stopOrder() { return this.refuse("trading"); }
|
|
241
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended public REST transport (official API, https://api.docs.extended.exchange/).
|
|
3
|
+
*
|
|
4
|
+
* Every response is `{status:"OK", data, pagination?}` or `{status:"ERROR", error:{code, message}}`
|
|
5
|
+
* (live 2026-10-19: an unknown market on /info/markets?market= or /info/<m>/funding is HTTP 400
|
|
6
|
+
* `{"code":1001,"message":"Market not found"}`). A User-Agent header is mandatory (docs
|
|
7
|
+
* "Mandatory headers"; without one the API answers 403). GET only — there is no write path.
|
|
8
|
+
*/
|
|
9
|
+
import { PerpError } from "../../errors.js";
|
|
10
|
+
import type { RateLimitPolicy } from "../venue.js";
|
|
11
|
+
export declare const EXTENDED_HTTP_TIMEOUT_MS = 15000;
|
|
12
|
+
export declare const EXTENDED_USER_AGENT = "perp-cli";
|
|
13
|
+
/** Extended's error code for an unknown market (live 2026-10-19). */
|
|
14
|
+
export declare const EXTENDED_MARKET_NOT_FOUND = 1001;
|
|
15
|
+
/** A body that does not match the documented shape: an explicit EXCHANGE_ERROR, never a guess. */
|
|
16
|
+
export declare function extendedShapeError(what: string): PerpError;
|
|
17
|
+
export interface ExtendedErrorBody {
|
|
18
|
+
code: number | string | null;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class ExtendedClient {
|
|
22
|
+
private readonly restUrl;
|
|
23
|
+
private readonly rateLimit;
|
|
24
|
+
/** Limiter bucket: one per network (the documented limit is per IP, per host). */
|
|
25
|
+
private readonly bucket;
|
|
26
|
+
constructor(restUrl: string, rateLimit: RateLimitPolicy,
|
|
27
|
+
/** Limiter bucket: one per network (the documented limit is per IP, per host). */
|
|
28
|
+
bucket: string);
|
|
29
|
+
/**
|
|
30
|
+
* GET <rest><path>; returns the parsed body (`status: "OK"`). Every failure is a PerpError:
|
|
31
|
+
* transport → EXCHANGE_UNREACHABLE, 429 → RATE_LIMITED, `{status:"ERROR"}` / non-2xx /
|
|
32
|
+
* non-JSON → EXCHANGE_ERROR (an unknown market keeps the venue's 1001 code in details).
|
|
33
|
+
*/
|
|
34
|
+
get<T = unknown>(path: string, query?: ReadonlyArray<[string, string]>): Promise<{
|
|
35
|
+
status: "OK";
|
|
36
|
+
data?: T;
|
|
37
|
+
} & Record<string, unknown>>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended public REST transport (official API, https://api.docs.extended.exchange/).
|
|
3
|
+
*
|
|
4
|
+
* Every response is `{status:"OK", data, pagination?}` or `{status:"ERROR", error:{code, message}}`
|
|
5
|
+
* (live 2026-10-19: an unknown market on /info/markets?market= or /info/<m>/funding is HTTP 400
|
|
6
|
+
* `{"code":1001,"message":"Market not found"}`). A User-Agent header is mandatory (docs
|
|
7
|
+
* "Mandatory headers"; without one the API answers 403). GET only — there is no write path.
|
|
8
|
+
*/
|
|
9
|
+
import { PerpError, extractErrorMessage } from "../../errors.js";
|
|
10
|
+
import { acquireRate, rateLimitedError } from "../rate-limiter.js";
|
|
11
|
+
export const EXTENDED_HTTP_TIMEOUT_MS = 15_000;
|
|
12
|
+
export const EXTENDED_USER_AGENT = "perp-cli";
|
|
13
|
+
/** Extended's error code for an unknown market (live 2026-10-19). */
|
|
14
|
+
export const EXTENDED_MARKET_NOT_FOUND = 1001;
|
|
15
|
+
/** A body that does not match the documented shape: an explicit EXCHANGE_ERROR, never a guess. */
|
|
16
|
+
export function extendedShapeError(what) {
|
|
17
|
+
return new PerpError("EXCHANGE_ERROR", `Extended API response did not match the documented shape: ${what}`, {
|
|
18
|
+
exchange: "extended",
|
|
19
|
+
remediation: "Extended changed its public API; perp-cli needs an update to read it",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function errorBodyOf(parsed) {
|
|
23
|
+
if (parsed === null || typeof parsed !== "object")
|
|
24
|
+
return null;
|
|
25
|
+
const p = parsed;
|
|
26
|
+
if (p.status !== "ERROR")
|
|
27
|
+
return null;
|
|
28
|
+
const e = p.error;
|
|
29
|
+
const code = typeof e?.code === "number" || typeof e?.code === "string" ? e.code : null;
|
|
30
|
+
return { code, message: typeof e?.message === "string" ? e.message : JSON.stringify(p.error === undefined ? null : p.error) };
|
|
31
|
+
}
|
|
32
|
+
export class ExtendedClient {
|
|
33
|
+
restUrl;
|
|
34
|
+
rateLimit;
|
|
35
|
+
bucket;
|
|
36
|
+
constructor(restUrl, rateLimit,
|
|
37
|
+
/** Limiter bucket: one per network (the documented limit is per IP, per host). */
|
|
38
|
+
bucket) {
|
|
39
|
+
this.restUrl = restUrl;
|
|
40
|
+
this.rateLimit = rateLimit;
|
|
41
|
+
this.bucket = bucket;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* GET <rest><path>; returns the parsed body (`status: "OK"`). Every failure is a PerpError:
|
|
45
|
+
* transport → EXCHANGE_UNREACHABLE, 429 → RATE_LIMITED, `{status:"ERROR"}` / non-2xx /
|
|
46
|
+
* non-JSON → EXCHANGE_ERROR (an unknown market keeps the venue's 1001 code in details).
|
|
47
|
+
*/
|
|
48
|
+
async get(path, query = []) {
|
|
49
|
+
await acquireRate(this.bucket, this.rateLimit, path);
|
|
50
|
+
const qs = query.length > 0 ? `?${query.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")}` : "";
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(`${this.restUrl}${path}${qs}`, {
|
|
54
|
+
headers: { accept: "application/json", "user-agent": EXTENDED_USER_AGENT },
|
|
55
|
+
signal: AbortSignal.timeout(EXTENDED_HTTP_TIMEOUT_MS),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `Extended ${path}: ${extractErrorMessage(e)}`, { exchange: "extended" });
|
|
60
|
+
}
|
|
61
|
+
const text = await res.text().catch((e) => {
|
|
62
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `Extended ${path}: response body unreadable (${extractErrorMessage(e)})`, { exchange: "extended" });
|
|
63
|
+
});
|
|
64
|
+
if (res.status === 429)
|
|
65
|
+
throw rateLimitedError("extended", res.headers.get("retry-after"), text);
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
parsed = undefined;
|
|
72
|
+
}
|
|
73
|
+
const err = errorBodyOf(parsed);
|
|
74
|
+
if (err !== null || !res.ok) {
|
|
75
|
+
const msg = err !== null ? `${err.code === null ? "" : `${err.code} `}${err.message}` : `HTTP ${res.status} ${text.slice(0, 200)}`;
|
|
76
|
+
throw new PerpError("EXCHANGE_ERROR", `Extended ${path}: ${msg}`, {
|
|
77
|
+
exchange: "extended", status: res.status,
|
|
78
|
+
...(err !== null && err.code !== null ? { venueCode: err.code } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (parsed === undefined)
|
|
82
|
+
throw extendedShapeError(`${path} returned a non-JSON body`);
|
|
83
|
+
if (parsed === null || typeof parsed !== "object" || parsed.status !== "OK") {
|
|
84
|
+
throw extendedShapeError(`${path} has no status "OK"`);
|
|
85
|
+
}
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended public market data — GET /api/v1/info/markets (one call serves markets and funding).
|
|
3
|
+
* Docs: https://api.docs.extended.exchange/ ("Get markets").
|
|
4
|
+
*
|
|
5
|
+
* Mapping:
|
|
6
|
+
* - Only `type: "PERPETUAL"` rows with `status` ACTIVE or REDUCE_ONLY are markets here. The
|
|
7
|
+
* `active` flag is NOT a listing state (live 2026-10-19: 54 DELISTED and 12 PRELISTED rows have
|
|
8
|
+
* `active: true`); SPOT rows carry perp fields as 0 (docs) and are excluded.
|
|
9
|
+
* - venueSymbol = `name` ("BTC-USD", "TSLA_24_5-USD", "SPX500m-USD"), the wire id of every
|
|
10
|
+
* per-market endpoint. symbol = `assetName` upper-cased with the `_24_5` naming suffix removed
|
|
11
|
+
* ("TSLA_24_5" → "TSLA", "SPX500m" → "SPX500M"). Two listed markets with one symbol: the single
|
|
12
|
+
* ACTIVE one is kept, else both are skipped (warned).
|
|
13
|
+
* - tradable = status ACTIVE && !isOffHours (docs "Trading hours": while isOffHours, order
|
|
14
|
+
* placement and execution are unavailable). REDUCE_ONLY rows are listed, not tradable.
|
|
15
|
+
* - tick = tradingConfig.minPriceChange, step = minOrderSizeChange, min size = minOrderSize,
|
|
16
|
+
* maxLeverage = tradingConfig.maxLeverage; marginTiers from riskFactorConfig (rows available to
|
|
17
|
+
* users; leverage = 1 / riskFactor — the first tier equals maxLeverage on every live market).
|
|
18
|
+
* - open interest = marketStats.openInterestBase (base units); 24h volume = dailyVolume (USD).
|
|
19
|
+
*
|
|
20
|
+
* Asset class (`category` / `subCategory`): Crypto → crypto; RWA → Equity: equity, Commodity:
|
|
21
|
+
* commodity, FX: fx, Pre-market: preIpo, ETF/Index: etf for the ETFs in EXTENDED_ETFS, else index.
|
|
22
|
+
* Any other category / subCategory is skipped with a warning (never guessed).
|
|
23
|
+
*
|
|
24
|
+
* Canonical (cross-venue key, canonical-symbols.ts): crypto and equity / ETF by ticker identity;
|
|
25
|
+
* index / commodity / fx / preIpo only through EXPLICIT_CANONICAL. An RWA ticker that is also a
|
|
26
|
+
* crypto ticker is NEVER matched (canonical null): EXTENDED_RWA_CRYPTO_COLLISIONS (PURR =
|
|
27
|
+
* Hyperliquid Strategies Inc. vs the PURR token; QNT = Quantinuum vs Quant; BB = BlackBerry vs
|
|
28
|
+
* BounceBit; ON = ON Semiconductor vs the ON token; ADI = Analog Devices vs the ADI token). An RWA
|
|
29
|
+
* ticker equal to one of Extended's own crypto symbols is never matched either: symbols are unique per
|
|
30
|
+
* listing (dedupeSymbols). The crypto SPX market (SPX6900) is ambiguous with the S&P 500 index
|
|
31
|
+
* canonical "SPX" and is never matched (EXTENDED_AMBIGUOUS_CRYPTO, as Hyperliquid's SPX). XIAOMI is
|
|
32
|
+
* not matched (EXTENDED_UNMATCHED_EQUITIES): ~7.8× the Lighter / Aster XIAOMI price on 2026-10-19,
|
|
33
|
+
* i.e. a different unit (HKD-per-share vs a USD-quoted product), not the same contract.
|
|
34
|
+
*
|
|
35
|
+
* Funding (docs "Get funding rates history" + docs.extended.exchange funding-payments):
|
|
36
|
+
* - applied once per hour ("While the funding rate is calculated every minute, it is only applied
|
|
37
|
+
* once per hour"); payment = position size × mark price × (−rate).
|
|
38
|
+
* - marketStats.fundingRate is a FRACTION per hour (crypto interest component 0.01 % / 8 h =
|
|
39
|
+
* 0.0000125 / h, published as "0.000013"; RWA 0.0035 % / 8 h → "0.000004").
|
|
40
|
+
* - it is the CURRENT hour's rate, recomputed every minute (accruing): it can differ from the last
|
|
41
|
+
* applied print in /info/<market>/funding mid-hour, and becomes that print at the hour.
|
|
42
|
+
* - marketStats.nextFundingRate is (despite its name) the timestamp (ms) of the next application.
|
|
43
|
+
* - rates are published ROUNDED TO 6 DECIMALS (resolution 0.000001 / h ≈ 0.876 % APR): the
|
|
44
|
+
* 0.0000125 baseline reads "0.000013", and a rate below 0.0000005 / h reads "0.000000".
|
|
45
|
+
* There is no more precise public source (checked live 2026-10-19): /info/markets (407
|
|
46
|
+
* markets), /info/markets/<m>/stats and the applied history /info/<m>/funding (`f`, 48 h of
|
|
47
|
+
* ETH / SOL) all carry at most 6 decimals, so the adapter keeps this one read.
|
|
48
|
+
* - `_24_5` RWA equities print 0 while their underlying is closed (TSLA_24_5-USD: 34 of 43
|
|
49
|
+
* weekend prints were 0), so an hourly rate × 8760 overstates them — the venue is
|
|
50
|
+
* fundingVerified false (venues/extended.ts).
|
|
51
|
+
*/
|
|
52
|
+
import { type VenuePublicData } from "../venue.js";
|
|
53
|
+
import type { AssetClass, FundingRateBasis, Network, VenueFundingRate, VenueMarket } from "../venue-types.js";
|
|
54
|
+
import { ExtendedClient } from "./client.js";
|
|
55
|
+
export declare const EXTENDED_FUNDING_BASIS: FundingRateBasis;
|
|
56
|
+
/** RWA "ETF/Index" markets that are ETFs (the rest of that subCategory are indices). */
|
|
57
|
+
export declare const EXTENDED_ETFS: ReadonlySet<string>;
|
|
58
|
+
/**
|
|
59
|
+
* RWA tickers that are also crypto tickers on other venues (live 2026-10-19) — never matched.
|
|
60
|
+
* ADI: Analog Devices vs Lighter's ADI token. BOT: RoboStrategy (PRELISTED on 2026-10-19) vs the BOT
|
|
61
|
+
* token (Lighter LIGHTER_AMBIGUOUS). WEN (also Lighter-ambiguous) is not listed on Extended.
|
|
62
|
+
*/
|
|
63
|
+
export declare const EXTENDED_RWA_CRYPTO_COLLISIONS: ReadonlySet<string>;
|
|
64
|
+
/** RWA markets whose product differs from the same ticker elsewhere (XIAOMI: ~7.8× Lighter / Aster). */
|
|
65
|
+
export declare const EXTENDED_UNMATCHED_EQUITIES: ReadonlySet<string>;
|
|
66
|
+
/** Crypto tickers that name an index elsewhere — never matched (SPX-USD = SPX6900, not the S&P 500). */
|
|
67
|
+
export declare const EXTENDED_AMBIGUOUS_CRYPTO: ReadonlySet<string>;
|
|
68
|
+
export interface ExtendedMarketMeta {
|
|
69
|
+
/** Wire id ("BTC-USD"). */
|
|
70
|
+
name: string;
|
|
71
|
+
symbol: string;
|
|
72
|
+
assetClass: AssetClass;
|
|
73
|
+
canonical: string | null;
|
|
74
|
+
status: "ACTIVE" | "REDUCE_ONLY";
|
|
75
|
+
isRfq: boolean;
|
|
76
|
+
isOffHours: boolean;
|
|
77
|
+
tradingHours: string;
|
|
78
|
+
}
|
|
79
|
+
export interface ExtendedMarketRow {
|
|
80
|
+
meta: ExtendedMarketMeta;
|
|
81
|
+
market: VenueMarket;
|
|
82
|
+
nextFundingTime: number | null;
|
|
83
|
+
}
|
|
84
|
+
/** "TSLA_24_5" → "TSLA"; "SPX500m" → "SPX500M". */
|
|
85
|
+
export declare function extendedSymbolOf(assetName: string): string;
|
|
86
|
+
/** Asset class from category / subCategory; null = unknown (the row is skipped with a warning). */
|
|
87
|
+
export declare function extendedAssetClass(category: string, subCategory: string | null, symbol: string): AssetClass | null;
|
|
88
|
+
/** Canonical key, or null when the market must not be matched across venues. */
|
|
89
|
+
export declare function extendedCanonical(symbol: string, assetClass: AssetClass): string | null;
|
|
90
|
+
/**
|
|
91
|
+
* Parse /info/markets `data` into listed perpetual markets. A row that does not match the
|
|
92
|
+
* documented shape is skipped with a warning naming it (one drifted row never hides the others);
|
|
93
|
+
* rows of an unknown category, without a mark price, or sharing a symbol (dedupeSymbols) are
|
|
94
|
+
* skipped with a warning too. EXCHANGE_ERROR only for envelope drift (`data` not a list) or when
|
|
95
|
+
* malformed rows leave no market at all.
|
|
96
|
+
*/
|
|
97
|
+
export declare function parseExtendedMarkets(data: unknown, warn: (msg: string) => void): ExtendedMarketRow[];
|
|
98
|
+
export declare function extendedClient(net: Network): ExtendedClient;
|
|
99
|
+
export interface ExtendedPublicDeps {
|
|
100
|
+
warn: (msg: string) => void;
|
|
101
|
+
}
|
|
102
|
+
export declare function fetchExtendedMarketRows(client: ExtendedClient, deps?: ExtendedPublicDeps): Promise<ExtendedMarketRow[]>;
|
|
103
|
+
export declare function fetchExtendedMarkets(net: Network, deps?: ExtendedPublicDeps): Promise<VenueMarket[]>;
|
|
104
|
+
/** Funding rows: the current hour's accruing rate (fraction per hour); a market without one has no row. */
|
|
105
|
+
export declare function toExtendedFundingRates(rows: readonly ExtendedMarketRow[]): VenueFundingRate[];
|
|
106
|
+
export declare function fetchExtendedFundingRates(net: Network, deps?: ExtendedPublicDeps): Promise<VenueFundingRate[]>;
|
|
107
|
+
export declare const extendedPublicData: VenuePublicData;
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a CLI symbol against the listed markets: the wire name ("BTC-USD", "TSLA_24_5-USD")
|
|
110
|
+
* first, then the display symbol ("tsla", "BTC-PERP"), then "<symbol>-USD" ("TSLA-USD").
|
|
111
|
+
* Unlisted → SYMBOL_NOT_FOUND.
|
|
112
|
+
*/
|
|
113
|
+
export declare function resolveExtendedMarket(markets: readonly ExtendedMarketMeta[], input: string): ExtendedMarketMeta;
|
|
114
|
+
/** Wire names a bare CLI symbol may be ("btc" → BTC-USD, BTC_24_5-USD): every name whose row maps to that symbol. */
|
|
115
|
+
export declare function extendedWireCandidates(symbol: string): string[];
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a CLI symbol for a per-market read without the ~1 MB full listing, by the SAME rules as
|
|
118
|
+
* the listing (parseExtendedMarkets → dedupeSymbols, with its warning):
|
|
119
|
+
* 1. The symbol's wire names are read with `GET /info/markets?market=<name>` (2–3 KB each;
|
|
120
|
+
* case-sensitive; unknown = HTTP 400 code 1001): for a bare symbol `<S>-USD` and
|
|
121
|
+
* `<S>_24_5-USD`; for a wire name ("NOW-USD") that name first, then the other names of the
|
|
122
|
+
* symbol its row maps to — so a ticker migration (NOW-USD + NOW_24_5-USD) is deduplicated here
|
|
123
|
+
* exactly as in `market list`.
|
|
124
|
+
* 2. Every returned row goes through parseExtendedMarkets: not a listed perpetual, unpriced, or
|
|
125
|
+
* losing the duplicate-symbol rule → not a market. resolveExtendedMarket then decides;
|
|
126
|
+
* nothing left → SYMBOL_NOT_FOUND.
|
|
127
|
+
* 3. Only when EVERY name answered 1001 (no row at all — e.g. "spx500m" for SPX500m-USD) does the
|
|
128
|
+
* full listing decide.
|
|
129
|
+
* A malformed row in a `?market=` reply is EXCHANGE_ERROR when it leaves no row to parse (the
|
|
130
|
+
* market asked for is unreadable — no fallback to the listing); next to a valid row it is skipped
|
|
131
|
+
* with a warning, as in the listing. Any error other than 1001 is thrown as is.
|
|
132
|
+
*/
|
|
133
|
+
export declare function resolveExtendedMarketRemote(client: ExtendedClient, input: string, deps?: ExtendedPublicDeps): Promise<ExtendedMarketMeta>;
|