perp-cli 0.15.0 → 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.
Files changed (154) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +129 -535
  4. package/README.zh.md +243 -0
  5. package/dist/api/public/lighter.d.ts +5 -0
  6. package/dist/api/public/lighter.js +5 -0
  7. package/dist/api/public/pacifica.js +2 -2
  8. package/dist/arb/sizing.js +4 -2
  9. package/dist/arb/state.d.ts +6 -0
  10. package/dist/balance-by-address.d.ts +39 -0
  11. package/dist/balance-by-address.js +44 -0
  12. package/dist/bot/quote-rules.js +1 -1
  13. package/dist/bot/strategies/funding-arb-strategy.js +10 -3
  14. package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
  15. package/dist/bot/strategies/funding-arb-utils.js +48 -15
  16. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  17. package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
  18. package/dist/bot/strategies/spot-perp-executor.js +2 -2
  19. package/dist/cli-program.js +1 -1
  20. package/dist/commands/account.js +168 -24
  21. package/dist/commands/arb/index.js +19 -77
  22. package/dist/commands/arb-auto.d.ts +24 -0
  23. package/dist/commands/arb-auto.js +155 -81
  24. package/dist/commands/arb.d.ts +8 -0
  25. package/dist/commands/arb.js +49 -23
  26. package/dist/commands/backtest.js +23 -21
  27. package/dist/commands/market.d.ts +48 -0
  28. package/dist/commands/market.js +241 -78
  29. package/dist/commands/options.d.ts +5 -0
  30. package/dist/commands/options.js +20 -3
  31. package/dist/commands/pacifica-earn.js +8 -21
  32. package/dist/commands/portfolio.d.ts +16 -0
  33. package/dist/commands/portfolio.js +51 -8
  34. package/dist/commands/trade.js +4 -4
  35. package/dist/cross-chain-margin.d.ts +3 -1
  36. package/dist/cross-chain-margin.js +4 -2
  37. package/dist/dashboard/server.js +2 -2
  38. package/dist/dashboard/ui.js +9 -6
  39. package/dist/errors.d.ts +5 -0
  40. package/dist/errors.js +10 -0
  41. package/dist/exchanges/address.d.ts +8 -0
  42. package/dist/exchanges/address.js +47 -0
  43. package/dist/exchanges/arcus/adapter.d.ts +10 -6
  44. package/dist/exchanges/arcus/adapter.js +33 -6
  45. package/dist/exchanges/arcus/public.js +3 -2
  46. package/dist/exchanges/aster.d.ts +27 -7
  47. package/dist/exchanges/aster.js +124 -42
  48. package/dist/exchanges/canonical-symbols.d.ts +7 -0
  49. package/dist/exchanges/canonical-symbols.js +20 -0
  50. package/dist/exchanges/capabilities.d.ts +2 -0
  51. package/dist/exchanges/capabilities.js +2 -1
  52. package/dist/exchanges/derive/options.d.ts +5 -0
  53. package/dist/exchanges/derive/options.js +20 -1
  54. package/dist/exchanges/extended/adapter.d.ts +16 -2
  55. package/dist/exchanges/extended/adapter.js +96 -34
  56. package/dist/exchanges/extended/public.d.ts +3 -0
  57. package/dist/exchanges/extended/public.js +9 -2
  58. package/dist/exchanges/grvt/adapter.d.ts +16 -2
  59. package/dist/exchanges/grvt/adapter.js +98 -21
  60. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  61. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  62. package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
  63. package/dist/exchanges/hyperliquid-spot.js +20 -12
  64. package/dist/exchanges/hyperliquid.d.ts +180 -13
  65. package/dist/exchanges/hyperliquid.js +487 -185
  66. package/dist/exchanges/interface.d.ts +64 -8
  67. package/dist/exchanges/interface.js +31 -0
  68. package/dist/exchanges/lighter-public.d.ts +66 -11
  69. package/dist/exchanges/lighter-public.js +249 -81
  70. package/dist/exchanges/lighter-spot.d.ts +7 -1
  71. package/dist/exchanges/lighter-spot.js +99 -90
  72. package/dist/exchanges/lighter.d.ts +70 -11
  73. package/dist/exchanges/lighter.js +285 -92
  74. package/dist/exchanges/ondo/adapter.d.ts +34 -8
  75. package/dist/exchanges/ondo/adapter.js +73 -16
  76. package/dist/exchanges/ondo/public.d.ts +27 -2
  77. package/dist/exchanges/ondo/public.js +63 -31
  78. package/dist/exchanges/outcome-interface.d.ts +4 -0
  79. package/dist/exchanges/pacifica-spot.d.ts +6 -0
  80. package/dist/exchanges/pacifica-spot.js +23 -15
  81. package/dist/exchanges/pacifica-vaults.d.ts +9 -0
  82. package/dist/exchanges/pacifica-vaults.js +14 -1
  83. package/dist/exchanges/pacifica.d.ts +77 -6
  84. package/dist/exchanges/pacifica.js +321 -130
  85. package/dist/exchanges/public-balances.d.ts +25 -0
  86. package/dist/exchanges/public-balances.js +158 -0
  87. package/dist/exchanges/public-positions.d.ts +43 -0
  88. package/dist/exchanges/public-positions.js +154 -0
  89. package/dist/exchanges/qfex/adapter.d.ts +52 -9
  90. package/dist/exchanges/qfex/adapter.js +74 -9
  91. package/dist/exchanges/qfex/public.d.ts +62 -9
  92. package/dist/exchanges/qfex/public.js +128 -22
  93. package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
  94. package/dist/exchanges/risex/adapter.d.ts +19 -7
  95. package/dist/exchanges/risex/adapter.js +58 -12
  96. package/dist/exchanges/spot-factory.js +1 -1
  97. package/dist/exchanges/txflow/adapter.d.ts +14 -7
  98. package/dist/exchanges/txflow/adapter.js +51 -19
  99. package/dist/exchanges/txflow/client.js +3 -1
  100. package/dist/exchanges/txflow/public.d.ts +12 -1
  101. package/dist/exchanges/txflow/public.js +82 -11
  102. package/dist/exchanges/unknown-symbol.d.ts +6 -1
  103. package/dist/exchanges/unknown-symbol.js +7 -2
  104. package/dist/exchanges/variational/adapter.d.ts +2 -6
  105. package/dist/exchanges/venue-types.d.ts +46 -3
  106. package/dist/exchanges/venue-types.js +80 -9
  107. package/dist/exchanges/venue.d.ts +99 -3
  108. package/dist/exchanges/venue.js +72 -0
  109. package/dist/exchanges/venues/hyperliquid.js +9 -0
  110. package/dist/exchanges/venues/lighter-rh.js +13 -3
  111. package/dist/exchanges/venues/lighter.js +13 -3
  112. package/dist/exchanges/venues/pacifica.js +9 -0
  113. package/dist/exchanges/venues/risex.js +15 -0
  114. package/dist/funding/legacy-public.d.ts +5 -3
  115. package/dist/funding/legacy-public.js +33 -37
  116. package/dist/funding/rates.d.ts +54 -5
  117. package/dist/funding/rates.js +49 -13
  118. package/dist/liquidity.d.ts +3 -1
  119. package/dist/liquidity.js +8 -4
  120. package/dist/mcp-pacifica-earn.js +11 -5
  121. package/dist/mcp-server.js +104 -15
  122. package/dist/pacifica/client.d.ts +2 -1
  123. package/dist/pacifica/client.js +11 -0
  124. package/dist/positions-by-address.d.ts +34 -0
  125. package/dist/positions-by-address.js +46 -0
  126. package/dist/strategies/funding-arb.js +35 -62
  127. package/dist/utils.d.ts +11 -1
  128. package/dist/venue-health.js +1 -1
  129. package/dist/ws/arcus-market.d.ts +9 -0
  130. package/dist/ws/arcus-market.js +9 -0
  131. package/dist/ws/aster-market.d.ts +33 -0
  132. package/dist/ws/aster-market.js +30 -0
  133. package/dist/ws/index.d.ts +30 -9
  134. package/dist/ws/index.js +180 -15
  135. package/dist/ws/lighter-market.d.ts +14 -0
  136. package/dist/ws/lighter-market.js +13 -0
  137. package/dist/ws/node.d.ts +30 -9
  138. package/dist/ws/node.js +204 -16
  139. package/dist/ws/unclassified.d.ts +16 -0
  140. package/dist/ws/unclassified.js +43 -0
  141. package/package.json +15 -2
  142. package/skills/perp-cli/SKILL.md +97 -33
  143. package/skills/perp-cli/package.json +15 -2
  144. package/skills/perp-cli/references/agent-operations.md +15 -12
  145. package/skills/perp-cli/references/commands.md +67 -12
  146. package/skills/perp-cli/references/strategies.md +38 -3
  147. package/skills/perp-cli/references/ws-library.md +55 -24
  148. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  149. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  150. package/skills/perp-cli/scripts/preflight.sh +27 -19
  151. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  152. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  153. package/dist/cli-spec.d.ts +0 -46
  154. package/dist/cli-spec.js +0 -81
@@ -18,7 +18,7 @@
18
18
  * Docs: https://docs.asterdex.com/product/aster-perpetuals/api/api-documentation
19
19
  * Base: https://fapi.asterdex.com
20
20
  */
21
- import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrder, ExchangeBalance, ExchangeTrade, ExchangePublicTrade, ExchangeFundingPayment, ExchangeKline } from "./interface.js";
21
+ import type { ExchangeAdapter, ExchangeMarketInfo, ExchangePosition, ExchangeOrder, ExchangeBalance, ExchangeTrade, ExchangePublicTrade, ExchangeFundingPayment, ExchangeKline, FundingHistoryRow } from "./interface.js";
22
22
  import type { ResolvedSlippage } from "./slippage.js";
23
23
  import type { EvmSigner } from "../signer/interface.js";
24
24
  import type { AgentMeta } from "../settings.js";
@@ -68,6 +68,11 @@ type ResolvedSigner = {
68
68
  * that the read refuses (EXCHANGE_ERROR) rather than returning a partial history.
69
69
  */
70
70
  export declare const ASTER_INCOME_MAX_PAGES = 60;
71
+ /**
72
+ * Kline intervals verified live 2026-10-19 (/fapi/v3/klines BTCUSDT: each answers bars of that
73
+ * length; "7m" / "2d" → -1120 "Invalid interval."). Anything else is refused before a request.
74
+ */
75
+ export declare const ASTER_KLINE_INTERVALS: ReadonlySet<string>;
71
76
  export declare class AsterAdapter implements ExchangeAdapter {
72
77
  readonly name = "aster";
73
78
  readonly chain = "bnb";
@@ -89,6 +94,8 @@ export declare class AsterAdapter implements ExchangeAdapter {
89
94
  /** API symbol → price filters; null = listed but PRICE_FILTER / PERCENT_PRICE unreadable. */
90
95
  private _priceFilters;
91
96
  private _priceFiltersLoadedAt;
97
+ /** The in-flight exchangeInfo load shared by concurrent first uses (_ensureDirectory). */
98
+ private _directoryLoad;
92
99
  private _accountCache;
93
100
  private _positionsCache;
94
101
  private _ordersCache;
@@ -135,7 +142,12 @@ export declare class AsterAdapter implements ExchangeAdapter {
135
142
  * agent. Throws exactly like signed calls when no signer is usable.
136
143
  */
137
144
  activeAgentMeta(): AgentMeta | null;
138
- /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
145
+ /**
146
+ * CLI symbol → Aster API symbol through the LOADED market directory (exchangeInfo): the exact
147
+ * venue symbol first (BTCUSDT, BTCUSD1, BTCU — a non-USDT market's CLI symbol is its venue
148
+ * symbol, asterCliSymbol), then the USDT market of a base (ETH → ETHUSDT, USD1 → USD1USDT),
149
+ * else SYMBOL_NOT_FOUND — never a guessed suffix. Callers load the directory first (_apiSymbol).
150
+ */
139
151
  private _toApi;
140
152
  /** Aster API symbol → CLI symbol (ETHUSDT → ETH) */
141
153
  private _fromApi;
@@ -159,6 +171,18 @@ export declare class AsterAdapter implements ExchangeAdapter {
159
171
  /** Parse PRICE_FILTER + PERCENT_PRICE for every exchangeInfo symbol into the cache. */
160
172
  private _ingestPriceFilters;
161
173
  /** PRICE_FILTER / PERCENT_PRICE of an API symbol (exchangeInfo, cached CACHE_TTL). */
174
+ /** exchangeInfo symbols (the market directory) loaded / refreshed per CACHE_TTL. */
175
+ /**
176
+ * exchangeInfo symbols (the market directory), refreshed per CACHE_TTL. Concurrent first uses
177
+ * share ONE in-flight request; a failed load is not cached (the next call retries).
178
+ */
179
+ private _ensureDirectory;
180
+ /**
181
+ * CLI symbol → Aster API symbol, resolved through the market directory (_toApi), loaded on first
182
+ * use and cached CACHE_TTL — so the answer never depends on what was read before (a cold
183
+ * adapter resolves BTCU, USD1 → USD1USDT, an unknown symbol is SYMBOL_NOT_FOUND).
184
+ */
185
+ private _apiSymbol;
162
186
  private _symbolPriceFilters;
163
187
  /**
164
188
  * Declared protection-price reference: the venue mark price from
@@ -191,11 +215,7 @@ export declare class AsterAdapter implements ExchangeAdapter {
191
215
  asks: [string, string][];
192
216
  }>;
193
217
  getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
194
- getFundingHistory(symbol: string, limit?: number): Promise<{
195
- time: number;
196
- rate: string;
197
- price: string | null;
198
- }[]>;
218
+ getFundingHistory(symbol: string, limit?: number): Promise<FundingHistoryRow[]>;
199
219
  /**
200
220
  * Candles over [startTime, endTime]. GET /fapi/v3/klines returns at most 1500 bars per request
201
221
  * (1501 → -1130), so the range is read in pages that start after the last bar returned — a long
@@ -19,6 +19,8 @@
19
19
  * Base: https://fapi.asterdex.com
20
20
  */
21
21
  import { nonEmptyOrListed } from "./unknown-symbol.js";
22
+ import { asterCliSymbol, isAsterListedPerpetual } from "../ws/aster-market.js";
23
+ import { decimalsOf } from "./venue-types.js";
22
24
  import { accountCacheEpoch } from "../cache.js";
23
25
  import { isExpired } from "../agent-wallet/expiry.js";
24
26
  import { classifyError, DEFINITE_ORDER_FAILURE_CODES, isTransportFailure, orderNotSent, orderOutcomeUnknown, PerpError, sendFailure } from "../errors.js";
@@ -174,6 +176,11 @@ export const ASTER_INCOME_MAX_PAGES = 60;
174
176
  const ASTER_KLINE_PAGE = 1500;
175
177
  /** Page cap for one range (150 000 bars). */
176
178
  const ASTER_KLINE_MAX_PAGES = 100;
179
+ /**
180
+ * Kline intervals verified live 2026-10-19 (/fapi/v3/klines BTCUSDT: each answers bars of that
181
+ * length; "7m" / "2d" → -1120 "Invalid interval."). Anything else is refused before a request.
182
+ */
183
+ export const ASTER_KLINE_INTERVALS = new Set(["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1M"]);
177
184
  export class AsterAdapter {
178
185
  name = "aster";
179
186
  chain = "bnb";
@@ -197,6 +204,8 @@ export class AsterAdapter {
197
204
  /** API symbol → price filters; null = listed but PRICE_FILTER / PERCENT_PRICE unreadable. */
198
205
  _priceFilters = new Map();
199
206
  _priceFiltersLoadedAt = 0;
207
+ /** The in-flight exchangeInfo load shared by concurrent first uses (_ensureDirectory). */
208
+ _directoryLoad = null;
200
209
  _accountCache = null;
201
210
  _positionsCache = null;
202
211
  _ordersCache = null;
@@ -327,12 +336,23 @@ export class AsterAdapter {
327
336
  return this._agentMeta;
328
337
  }
329
338
  // ── Symbol helpers ──
330
- /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
339
+ /**
340
+ * CLI symbol → Aster API symbol through the LOADED market directory (exchangeInfo): the exact
341
+ * venue symbol first (BTCUSDT, BTCUSD1, BTCU — a non-USDT market's CLI symbol is its venue
342
+ * symbol, asterCliSymbol), then the USDT market of a base (ETH → ETHUSDT, USD1 → USD1USDT),
343
+ * else SYMBOL_NOT_FOUND — never a guessed suffix. Callers load the directory first (_apiSymbol).
344
+ */
331
345
  _toApi(symbol) {
332
346
  const s = symbol.toUpperCase().replace(/-PERP$/, "");
333
- if (s.endsWith("USDT") || s.endsWith("BUSD"))
347
+ // A bare base names its USDT market first (X → XUSDT), even if a non-USDT venue symbol is
348
+ // spelled X too; an exact venue symbol otherwise (BTCUSD1, BTCU, XUSDT).
349
+ if (this._priceFilters.has(`${s}USDT`))
350
+ return `${s}USDT`;
351
+ if (this._priceFilters.has(s))
334
352
  return s;
335
- return `${s}USDT`;
353
+ throw new PerpError("SYMBOL_NOT_FOUND", `Aster has no market ${s} (neither the venue symbol ${s} nor ${s}USDT is listed)`, {
354
+ exchange: "aster", remediation: "List markets: perp -e aster market list",
355
+ });
336
356
  }
337
357
  /** Aster API symbol → CLI symbol (ETHUSDT → ETH) */
338
358
  _fromApi(symbol) {
@@ -408,10 +428,9 @@ export class AsterAdapter {
408
428
  }
409
429
  const premiumMap = new Map(premiums.map(p => [String(p.symbol), p]));
410
430
  this._ingestPriceFilters(info.symbols);
411
- // USDT-quoted perps only: the CLI symbol mapping (ETH ↔ ETHUSDT) cannot address the USD1 / U
412
- // quoted markets (live 2026-10-13: 22 USD1 + 2 U), so listing them would offer untradeable rows.
413
- const tradingSymbols = info.symbols
414
- .filter((s) => String(s.contractType) === "PERPETUAL" && String(s.status) === "TRADING" && String(s.quoteAsset) === "USDT");
431
+ // Every listed perpetual, any quote (the WS directory's set): USDT-quoted ones by base (ETH),
432
+ // USD1 / U quoted ones by venue symbol (BTCUSD1, BTCU — asterCliSymbol), with quoteAsset.
433
+ const tradingSymbols = info.symbols.filter((s) => isAsterListedPerpetual(s));
415
434
  const result = tradingSymbols.flatMap((s) => {
416
435
  const sym = String(s.symbol);
417
436
  const ticker = tickerMap.get(sym);
@@ -423,17 +442,24 @@ export class AsterAdapter {
423
442
  const maxLev = null;
424
443
  const lotFilter = s.filters
425
444
  ?.find(f => f.filterType === "LOT_SIZE");
445
+ const cliSymbol = asterCliSymbol({ symbol: sym, baseAsset: String(s.baseAsset), quoteAsset: s.quoteAsset });
426
446
  const fundingHours = this._fundingHoursCache.get(this._fromApi(sym));
427
447
  return [{
428
- symbol: this._fromApi(sym),
448
+ symbol: cliSymbol,
449
+ venueSymbol: sym,
450
+ quoteAsset: typeof s.quoteAsset === "string" ? s.quoteAsset : null,
429
451
  markPrice: String(premium.markPrice),
430
452
  indexPrice: premium?.indexPrice != null ? String(premium.indexPrice) : null,
431
453
  fundingRate: premium?.lastFundingRate != null ? String(premium.lastFundingRate) : null,
454
+ // premiumIndex lastFundingRate moves during the period (same value as WS markPrice@arr `r`: accruing).
455
+ fundingRateKind: premium?.lastFundingRate != null ? "accruing" : null,
432
456
  volume24h: ticker?.quoteVolume != null ? String(ticker.quoteVolume) : null,
433
457
  // /fapi/v3/exchangeInfo and ticker/24hr carry no open interest.
434
458
  openInterest: null,
435
459
  maxLeverage: maxLev,
436
- sizeDecimals: s.quantityPrecision != null ? Number(s.quantityPrecision) : null,
460
+ // The LOT_SIZE step is the authoritative grid (quantityPrecision can disagree with it: CFX
461
+ // step "1", precision 1); its decimals, the public directory's rule (venueMarketRow).
462
+ sizeDecimals: lotFilter?.stepSize != null ? decimalsOf(String(lotFilter.stepSize)) : s.quantityPrecision != null ? Number(s.quantityPrecision) : null,
437
463
  stepSize: lotFilter?.stepSize != null ? String(lotFilter.stepSize) : null,
438
464
  fundingHours: fundingHours ?? null,
439
465
  marginTiers: null,
@@ -468,14 +494,36 @@ export class AsterAdapter {
468
494
  this._priceFiltersLoadedAt = Date.now();
469
495
  }
470
496
  /** PRICE_FILTER / PERCENT_PRICE of an API symbol (exchangeInfo, cached CACHE_TTL). */
471
- async _symbolPriceFilters(apiSymbol) {
472
- if (!this._priceFiltersLoadedAt || Date.now() - this._priceFiltersLoadedAt >= AsterAdapter.CACHE_TTL) {
473
- const info = await this._publicGet("/fapi/v3/exchangeInfo");
474
- if (!Array.isArray(info?.symbols)) {
475
- throw new PerpError("EXCHANGE_ERROR", "Aster /fapi/v3/exchangeInfo returned no symbols", { exchange: "aster" });
476
- }
477
- this._ingestPriceFilters(info.symbols);
497
+ /** exchangeInfo symbols (the market directory) loaded / refreshed per CACHE_TTL. */
498
+ /**
499
+ * exchangeInfo symbols (the market directory), refreshed per CACHE_TTL. Concurrent first uses
500
+ * share ONE in-flight request; a failed load is not cached (the next call retries).
501
+ */
502
+ async _ensureDirectory() {
503
+ if (this._priceFiltersLoadedAt && Date.now() - this._priceFiltersLoadedAt < AsterAdapter.CACHE_TTL)
504
+ return;
505
+ if (this._directoryLoad === null) {
506
+ this._directoryLoad = (async () => {
507
+ const info = await this._publicGet("/fapi/v3/exchangeInfo");
508
+ if (!Array.isArray(info?.symbols)) {
509
+ throw new PerpError("EXCHANGE_ERROR", "Aster /fapi/v3/exchangeInfo returned no symbols", { exchange: "aster" });
510
+ }
511
+ this._ingestPriceFilters(info.symbols);
512
+ })().finally(() => { this._directoryLoad = null; });
478
513
  }
514
+ return this._directoryLoad;
515
+ }
516
+ /**
517
+ * CLI symbol → Aster API symbol, resolved through the market directory (_toApi), loaded on first
518
+ * use and cached CACHE_TTL — so the answer never depends on what was read before (a cold
519
+ * adapter resolves BTCU, USD1 → USD1USDT, an unknown symbol is SYMBOL_NOT_FOUND).
520
+ */
521
+ async _apiSymbol(symbol) {
522
+ await this._ensureDirectory();
523
+ return this._toApi(symbol);
524
+ }
525
+ async _symbolPriceFilters(apiSymbol) {
526
+ await this._ensureDirectory();
479
527
  if (!this._priceFilters.has(apiSymbol)) {
480
528
  throw new PerpError("SYMBOL_NOT_FOUND", `Aster market ${apiSymbol} not found in exchangeInfo`, { exchange: "aster", remediation: "List markets: perp -e aster market list" });
481
529
  }
@@ -491,7 +539,7 @@ export class AsterAdapter {
491
539
  * No runtime substitute (no mid / last fallback): unavailable → PRICE_STALE.
492
540
  */
493
541
  async referencePrice(symbol) {
494
- const apiSymbol = this._toApi(symbol);
542
+ const apiSymbol = await this._apiSymbol(symbol);
495
543
  let mark;
496
544
  try {
497
545
  const res = await this._publicGet("/fapi/v3/premiumIndex", { symbol: apiSymbol });
@@ -536,7 +584,7 @@ export class AsterAdapter {
536
584
  * against PRICE_FILTER / PERCENT_PRICE. appliedBps = the bound after tick alignment.
537
585
  */
538
586
  async computeProtectionPrice(symbol, side, slippage) {
539
- const apiSymbol = this._toApi(symbol);
587
+ const apiSymbol = await this._apiSymbol(symbol);
540
588
  const f = await this._symbolPriceFilters(apiSymbol);
541
589
  const ref = await this.referencePrice(symbol);
542
590
  const protectionPrice = alignProtectionPrice(slippagePrice(ref, side, slippage.bps), f.tickSize, side);
@@ -549,7 +597,7 @@ export class AsterAdapter {
549
597
  return asterIocOutcome(raw);
550
598
  }
551
599
  async getOrderbook(symbol) {
552
- const res = await this._publicGet("/fapi/v3/depth", { symbol: this._toApi(symbol), limit: "50" });
600
+ const res = await this._publicGet("/fapi/v3/depth", { symbol: await this._apiSymbol(symbol), limit: "50" });
553
601
  return {
554
602
  bids: res?.bids ?? [],
555
603
  asks: res?.asks ?? [],
@@ -557,26 +605,31 @@ export class AsterAdapter {
557
605
  }
558
606
  async getRecentTrades(symbol, limit = 20) {
559
607
  const trades = await this._publicGet("/fapi/v3/trades", {
560
- symbol: this._toApi(symbol),
608
+ symbol: await this._apiSymbol(symbol),
561
609
  limit: String(limit),
562
610
  });
563
- return (trades ?? []).map((t) => ({
564
- time: Number(t.time ?? 0),
565
- symbol: this._fromApi(String(t.symbol ?? symbol)),
566
- side: t.isBuyerMaker ? "sell" : "buy",
567
- price: String(t.price ?? "0"),
568
- size: String(t.qty ?? "0"),
569
- fee: null, // the public tape carries no fee
570
- }));
611
+ if (!Array.isArray(trades))
612
+ throw new PerpError("EXCHANGE_ERROR", `Aster trades is not a list (${JSON.stringify(trades).slice(0, 120)})`, { exchange: "aster" });
613
+ // The tape rows carry no symbol: the normalized venue symbol (was the raw argument, "BTC-PERP").
614
+ const sym = this._fromApi(await this._apiSymbol(symbol));
615
+ return trades.map((t) => {
616
+ if (typeof t.time !== "number" || typeof t.price !== "string" || typeof t.qty !== "string" || typeof t.isBuyerMaker !== "boolean") {
617
+ throw new PerpError("EXCHANGE_ERROR", `Aster trade row without time / price / qty / isBuyerMaker: ${JSON.stringify(t).slice(0, 160)}`, { exchange: "aster" });
618
+ }
619
+ return { time: t.time, symbol: sym, side: t.isBuyerMaker ? "sell" : "buy", price: t.price, size: t.qty, fee: null }; // the public tape carries no fee
620
+ });
571
621
  }
572
622
  async getFundingHistory(symbol, limit = 10) {
573
623
  const data = await this._publicGet("/fapi/v3/fundingRate", {
574
- symbol: this._toApi(symbol),
624
+ symbol: await this._apiSymbol(symbol),
575
625
  limit: String(limit),
576
626
  });
577
627
  if (!Array.isArray(data)) {
578
628
  throw new PerpError("EXCHANGE_ERROR", `Aster fundingRate history is not a list (${JSON.stringify(data).slice(0, 120)})`, { exchange: "aster" });
579
629
  }
630
+ // Per-market interval (/fapi/v3/fundingInfo); null when the venue lists none for the symbol.
631
+ const hours = await this.getFundingHours(symbol);
632
+ const intervalHours = hours === undefined ? null : hours;
580
633
  // `200 []` for an unlisted symbol (live 2026-10-14): checked against the market directory.
581
634
  return nonEmptyOrListed(this, symbol, data.map((d) => {
582
635
  const time = Number(d.fundingTime);
@@ -587,8 +640,10 @@ export class AsterAdapter {
587
640
  time,
588
641
  rate: venueRateField(d.fundingRate, "fundingRate fundingRate", "aster"),
589
642
  price: d.markPrice ? String(d.markPrice) : null,
643
+ intervalHours,
644
+ rateKind: "lastSettled",
590
645
  };
591
- }), this._fromApi(this._toApi(symbol)));
646
+ }), this._fromApi(await this._apiSymbol(symbol)));
592
647
  }
593
648
  /**
594
649
  * Candles over [startTime, endTime]. GET /fapi/v3/klines returns at most 1500 bars per request
@@ -596,6 +651,11 @@ export class AsterAdapter {
596
651
  * range is never cut to the first page. More than ASTER_KLINE_MAX_PAGES pages → INVALID_PARAMS.
597
652
  */
598
653
  async getKlines(symbol, interval, startTime, endTime) {
654
+ if (!ASTER_KLINE_INTERVALS.has(interval)) {
655
+ throw new PerpError("INVALID_PARAMS", `Aster: unsupported candle interval "${interval}"`, {
656
+ remediation: `Use one of: ${[...ASTER_KLINE_INTERVALS].join(", ")}`,
657
+ });
658
+ }
599
659
  if (!(startTime <= endTime)) {
600
660
  throw new PerpError("INVALID_PARAMS", `Aster klines: start ${startTime} is after end ${endTime}`, {
601
661
  remediation: "Pass a --start that is before --end",
@@ -610,7 +670,7 @@ export class AsterAdapter {
610
670
  });
611
671
  }
612
672
  const data = await this._publicGet("/fapi/v3/klines", {
613
- symbol: this._toApi(symbol), interval,
673
+ symbol: await this._apiSymbol(symbol), interval,
614
674
  startTime: String(from), endTime: String(endTime), limit: String(ASTER_KLINE_PAGE),
615
675
  });
616
676
  if (!Array.isArray(data)) {
@@ -993,7 +1053,7 @@ export class AsterAdapter {
993
1053
  // PRICE_FILTER / PERCENT_PRICE). newOrderRespType=RESULT makes the response carry the
994
1054
  // final IOC status (FILLED / EXPIRED) and executedQty.
995
1055
  const r = this._resolveSigner();
996
- const apiSymbol = this._toApi(symbol);
1056
+ const apiSymbol = await this._apiSymbol(symbol);
997
1057
  const clientOrderId = opts.clientOrderId === undefined ? null : requireAsterClientOrderId(opts.clientOrderId);
998
1058
  let protectionPrice;
999
1059
  let protection;
@@ -1081,7 +1141,7 @@ export class AsterAdapter {
1081
1141
  }
1082
1142
  async limitOrder(symbol, side, price, size, opts) {
1083
1143
  const r = this._resolveSigner();
1084
- const apiSymbol = this._toApi(symbol);
1144
+ const apiSymbol = await this._apiSymbol(symbol);
1085
1145
  const clientOrderId = opts?.clientOrderId === undefined ? null : requireAsterClientOrderId(opts.clientOrderId);
1086
1146
  let params;
1087
1147
  try {
@@ -1124,7 +1184,7 @@ export class AsterAdapter {
1124
1184
  */
1125
1185
  async editOrder(symbol, orderId, price, size) {
1126
1186
  const r = this._resolveSigner();
1127
- const apiSymbol = this._toApi(symbol);
1187
+ const apiSymbol = await this._apiSymbol(symbol);
1128
1188
  const rawOrders = await this._signedGetEip712("/fapi/v3/openOrders", {}, r);
1129
1189
  const existing = (rawOrders ?? []).find(o => String(o.orderId ?? "") === orderId);
1130
1190
  if (!existing) {
@@ -1171,7 +1231,7 @@ export class AsterAdapter {
1171
1231
  const label = ref.kind === "orderId" ? ref.value : `c:${ref.value}`;
1172
1232
  let row;
1173
1233
  try {
1174
- row = await this._signedGetEip712("/fapi/v3/order", { symbol: this._toApi(symbol), ...key }, this._resolveSigner());
1234
+ row = await this._signedGetEip712("/fapi/v3/order", { symbol: await this._apiSymbol(symbol), ...key }, this._resolveSigner());
1175
1235
  }
1176
1236
  catch (e) {
1177
1237
  if (e instanceof PerpError && e.structured.details?.venueCode === -2013) {
@@ -1190,7 +1250,7 @@ export class AsterAdapter {
1190
1250
  const r = this._resolveSigner();
1191
1251
  try {
1192
1252
  return await this._signedDeleteEip712("/fapi/v3/order", {
1193
- symbol: this._toApi(symbol),
1253
+ symbol: await this._apiSymbol(symbol),
1194
1254
  ...key,
1195
1255
  }, r);
1196
1256
  }
@@ -1211,6 +1271,7 @@ export class AsterAdapter {
1211
1271
  const r = this._resolveSigner();
1212
1272
  if (!symbol) {
1213
1273
  const orders = await this.getOpenOrders();
1274
+ await this._ensureDirectory();
1214
1275
  const apiSymbols = new Set(orders.map(o => this._toApi(o.symbol)));
1215
1276
  const results = [];
1216
1277
  for (const sym of apiSymbols) {
@@ -1218,7 +1279,7 @@ export class AsterAdapter {
1218
1279
  }
1219
1280
  return results;
1220
1281
  }
1221
- return this._signedDeleteEip712("/fapi/v3/allOpenOrders", { symbol: this._toApi(symbol) }, r);
1282
+ return this._signedDeleteEip712("/fapi/v3/allOpenOrders", { symbol: await this._apiSymbol(symbol) }, r);
1222
1283
  }
1223
1284
  // ── Risk ──
1224
1285
  async setLeverage(symbol, leverage, marginMode) {
@@ -1226,7 +1287,7 @@ export class AsterAdapter {
1226
1287
  if (marginMode) {
1227
1288
  try {
1228
1289
  await this._signedPostEip712("/fapi/v3/marginType", {
1229
- symbol: this._toApi(symbol),
1290
+ symbol: await this._apiSymbol(symbol),
1230
1291
  marginType: marginMode === "cross" ? "CROSSED" : "ISOLATED",
1231
1292
  }, r);
1232
1293
  }
@@ -1265,7 +1326,7 @@ export class AsterAdapter {
1265
1326
  }
1266
1327
  }
1267
1328
  return this._signedPostEip712("/fapi/v3/leverage", {
1268
- symbol: this._toApi(symbol),
1329
+ symbol: await this._apiSymbol(symbol),
1269
1330
  leverage: String(leverage),
1270
1331
  }, r);
1271
1332
  }
@@ -1284,7 +1345,7 @@ export class AsterAdapter {
1284
1345
  */
1285
1346
  async stopOrder(symbol, side, size, triggerPrice, opts) {
1286
1347
  const r = this._resolveSigner();
1287
- const apiSymbol = this._toApi(symbol);
1348
+ const apiSymbol = await this._apiSymbol(symbol);
1288
1349
  const what = `stop ${side} ${size} ${symbol} @ ${triggerPrice}`;
1289
1350
  let params;
1290
1351
  try {
@@ -1786,10 +1847,31 @@ export class AsterAdapter {
1786
1847
  const url = `${this._baseUrl}${path}${qs ? `?${qs}` : ""}`;
1787
1848
  for (let attempt = 0; attempt < 3; attempt++) {
1788
1849
  const res = await fetch(url);
1789
- const result = await this._handleResponse(res, "GET", path, attempt);
1850
+ let result;
1851
+ try {
1852
+ result = await this._handleResponse(res, "GET", path, attempt);
1853
+ }
1854
+ catch (e) {
1855
+ if (e instanceof PerpError)
1856
+ throw e;
1857
+ // A public read never surfaces as UNKNOWN: rate limit / server-side / other HTTP failure.
1858
+ const msg = e instanceof Error ? e.message : String(e);
1859
+ throw new PerpError(res.status === 429 ? "RATE_LIMITED" : res.status >= 500 ? "EXCHANGE_UNREACHABLE" : "EXCHANGE_ERROR", `Aster ${msg}`, {
1860
+ exchange: "aster", status: res.status, remediation: res.status === 429 || res.status >= 500 ? "Retry later" : "Check the request (symbol / parameters)",
1861
+ });
1862
+ }
1863
+ // A parameter rejection arrives as HTTP 200 with an error body (live 2026-10-19,
1864
+ // fundingRate limit=1001: {"status":"ERROR","type":"VALID","code":"000002","errorData":{"limit":"…"}}).
1865
+ if (result !== null && typeof result === "object" && !Array.isArray(result) && result.status === "ERROR") {
1866
+ const r = result;
1867
+ const detail = JSON.stringify(r.errorData ?? r).slice(0, 200);
1868
+ throw new PerpError(r.type === "VALID" ? "INVALID_PARAMS" : "EXCHANGE_ERROR", `Aster ${path} rejected the request: ${detail}`, {
1869
+ exchange: "aster", venueCode: r.code, remediation: "Check the request parameters (e.g. --limit 1..1000)",
1870
+ });
1871
+ }
1790
1872
  if (result !== null)
1791
1873
  return result;
1792
1874
  }
1793
- throw new Error(`GET ${path} failed: max retries exceeded`);
1875
+ throw new PerpError("RATE_LIMITED", `Aster GET ${path}: rate limited after 3 attempts`, { exchange: "aster", remediation: "Retry later" });
1794
1876
  }
1795
1877
  }
@@ -29,3 +29,10 @@ export declare function isNeverMatch(a: string, b: string): boolean;
29
29
  * @param symbol plain symbol as reported by the adapter (no quote suffix, no dex prefix)
30
30
  */
31
31
  export declare function canonicalFor(venueKey: VenueId | `hl:${string}`, symbol: string, assetClass: AssetClass): string | null;
32
+ /**
33
+ * Canonical key for a symbol a USER typed (no venue / asset class known — `arb scan --compare`):
34
+ * a "-PERP" suffix is dropped, a denomination alias maps to its canonical ticker (kPEPE →
35
+ * 1000PEPE, UBTC → BTC), and a venue ticker of the explicit table maps to its canonical key when
36
+ * every venue that lists it agrees (XAU → GOLD, CL → CRUDE_OIL_WTI). Anything else: upper-cased.
37
+ */
38
+ export declare function canonicalQuerySymbol(symbol: string): string;
@@ -116,5 +116,25 @@ export function canonicalFor(venueKey, symbol, assetClass) {
116
116
  case "fx":
117
117
  case "preIpo":
118
118
  return EXPLICIT_CANONICAL[`${venueKey}|${up}`] ?? null;
119
+ case "unclassified":
120
+ return null; // ambiguous ticker: listed, never matched across venues
119
121
  }
120
122
  }
123
+ /**
124
+ * Canonical key for a symbol a USER typed (no venue / asset class known — `arb scan --compare`):
125
+ * a "-PERP" suffix is dropped, a denomination alias maps to its canonical ticker (kPEPE →
126
+ * 1000PEPE, UBTC → BTC), and a venue ticker of the explicit table maps to its canonical key when
127
+ * every venue that lists it agrees (XAU → GOLD, CL → CRUDE_OIL_WTI). Anything else: upper-cased.
128
+ */
129
+ export function canonicalQuerySymbol(symbol) {
130
+ const up = symbol.trim().toUpperCase().replace(/-PERP$/, "");
131
+ const alias = DENOMINATION_ALIASES[up];
132
+ if (alias !== undefined)
133
+ return alias;
134
+ const targets = new Set();
135
+ for (const [key, canonical] of Object.entries(EXPLICIT_CANONICAL)) {
136
+ if (key.slice(key.indexOf("|") + 1) === up)
137
+ targets.add(canonical);
138
+ }
139
+ return targets.size === 1 ? [...targets][0] : up;
140
+ }
@@ -29,6 +29,8 @@ export interface TriggerOrderCapable {
29
29
  export interface DexCapable {
30
30
  readonly dex: string;
31
31
  setDex(dex: string): void;
32
+ /** An independent adapter for `dex` with this account configuration and its own market state (never mutates this one). */
33
+ forDex(dex: string): ExchangeAdapter & DexCapable;
32
34
  listDeployedDexes(): Promise<{
33
35
  name: string;
34
36
  deployer: string;
@@ -18,7 +18,8 @@ export function isTriggerOrderCapable(adapter) {
18
18
  }
19
19
  export function isDexCapable(adapter) {
20
20
  return typeof adapter.setDex === "function"
21
- && typeof adapter.listDeployedDexes === "function";
21
+ && typeof adapter.listDeployedDexes === "function"
22
+ && typeof adapter.forDex === "function";
22
23
  }
23
24
  export function isSubAccountCapable(adapter) {
24
25
  return typeof adapter.createSubAccount === "function";
@@ -39,6 +39,11 @@ export declare function utcDate(ms: number): string;
39
39
  export declare function normalizeExpiry(input: string): string;
40
40
  /** User underlying → venue currency (upper case); INVALID_PARAMS for anything that is not a ticker. */
41
41
  export declare function normalizeUnderlying(input: string): string;
42
+ /**
43
+ * CLI option name → Derive wire name: upper-cased, and a decimal point in the strike written as
44
+ * the venue's underscore (`PUMP-20261023-0.0045-C` → `PUMP-20261023-0_0045-C`).
45
+ */
46
+ export declare function deriveInstrumentName(input: string): string;
42
47
  export declare function parseDeriveInstrument(raw: unknown): DeriveOptionInstrument;
43
48
  /** A ticker_slim snapshot (get_ticker / get_tickers value) of `inst` → OptionQuote. */
44
49
  export declare function parseDeriveOptionTicker(raw: unknown, inst: DeriveOptionInstrument): OptionQuote;
@@ -75,6 +75,16 @@ export function normalizeUnderlying(input) {
75
75
  }
76
76
  return u;
77
77
  }
78
+ /**
79
+ * CLI option name → Derive wire name: upper-cased, and a decimal point in the strike written as
80
+ * the venue's underscore (`PUMP-20261023-0.0045-C` → `PUMP-20261023-0_0045-C`).
81
+ */
82
+ export function deriveInstrumentName(input) {
83
+ const parts = input.trim().toUpperCase().split("-");
84
+ if (parts.length === 4)
85
+ parts[2] = parts[2].replace(/\./g, "_");
86
+ return parts.join("-");
87
+ }
78
88
  // ── parsing ──
79
89
  export function parseDeriveInstrument(raw) {
80
90
  const o = obj(raw, "instrument");
@@ -318,7 +328,7 @@ export class DeriveOptionsVenue {
318
328
  return buildChain(underlying, expiry, wanted === null ? "nearest" : "requested", listed, res.tickers, q.strikesAroundAtm);
319
329
  }
320
330
  async getQuote(instrumentInput) {
321
- const name = instrumentInput.trim().toUpperCase();
331
+ const name = deriveInstrumentName(instrumentInput);
322
332
  // Both reads are needed (instrument facts + live ticker); issued together, judged instrument first.
323
333
  const [instR, tickR] = await Promise.allSettled([
324
334
  this.client.call("get_instrument", { instrument_name: name }),
@@ -339,6 +349,15 @@ export class DeriveOptionsVenue {
339
349
  });
340
350
  }
341
351
  const inst = parseDeriveInstrument(o);
352
+ if (!inst.isActive && inst.expiryMs <= Date.now()) {
353
+ // Expired (live: ETH-20261019-2400-C, is_active false after expiry, option_details.settlement_price set).
354
+ const sp = o.option_details?.settlement_price;
355
+ const settled = typeof sp === "string" && sp !== "" ? `, settled at ${sp}` : " (settlement price not published yet)";
356
+ throw new PerpError("SYMBOL_NOT_FOUND", `Derive option ${name} expired at ${new Date(inst.expiryMs).toISOString()}${settled}`, {
357
+ exchange: VENUE, expired: true, settlementPrice: typeof sp === "string" && sp !== "" ? sp : null,
358
+ remediation: `Active expiries: perp -e ${VENUE} options expiries ${inst.underlying}`,
359
+ });
360
+ }
342
361
  if (!inst.isActive) {
343
362
  throw new PerpError("SYMBOL_NOT_FOUND", `Derive option ${name} is listed but not active (activation ${new Date(inst.activationMs).toISOString()})`, {
344
363
  exchange: VENUE, remediation: `Active expiries: perp -e ${VENUE} options expiries ${inst.underlying}`,
@@ -19,8 +19,18 @@ export declare const EXTENDED_KLINE_INTERVALS: Readonly<Record<string, {
19
19
  wire: string;
20
20
  ms: number;
21
21
  }>>;
22
- /** Documented per-request maximum of the candles and funding-history endpoints. */
22
+ /** Documented per-request maximum of the candles and funding-history endpoints (accepted `limit`). */
23
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;
24
34
  /** Legacy market row plus the Extended fields shown in JSON (asset class, precision, schedule). */
25
35
  export type ExtendedMarketInfo = ExchangeMarketInfo & {
26
36
  venueSymbol: string;
@@ -62,8 +72,12 @@ export declare class ExtendedPublicAdapter implements ExchangeAdapter {
62
72
  asks: [string, string][];
63
73
  }>;
64
74
  getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
65
- /** Applied (settled) hourly rates, newest first — fraction per hour; no oracle price published. */
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
+ */
66
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). */
67
81
  getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
68
82
  private refuse;
69
83
  getBalance(): Promise<ExchangeBalance>;