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
@@ -6,13 +6,28 @@
6
6
  * CAPABILITY_UNSUPPORTED before any request — there is no TxFlow account or order code.
7
7
  */
8
8
  import { PerpError } from "../../errors.js";
9
+ import { assertCandleInterval } from "../interface.js";
9
10
  import { unsupported } from "../trading-v2.js";
10
11
  import { roleUnsupported, networkConfig } from "../venue.js";
11
12
  import { toLegacyMarketInfo } from "../venue-types.js";
12
13
  import { txflowShapeError, TxflowClient } from "./client.js";
13
14
  import { fetchTxflowDirectory, fetchTxflowMarkets, resolveTxflowMarket } from "./public.js";
14
- /** Candle intervals the TxFlow app requests (HL-style). */
15
- export const TXFLOW_KLINE_INTERVALS = new Set(["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "1d", "3d", "1w", "1M"]);
15
+ /**
16
+ * Candle intervals that return bars (verified live 2026-10-19, coin 1). 3d / 1w / 1M are
17
+ * accepted by the API but answer [] for every window, so they are not offered.
18
+ */
19
+ export const TXFLOW_KLINE_MS = {
20
+ "1m": 60_000, "3m": 180_000, "5m": 300_000, "15m": 900_000, "30m": 1_800_000, "1h": 3_600_000,
21
+ "2h": 7_200_000, "4h": 14_400_000, "8h": 28_800_000, "12h": 43_200_000, "1d": 86_400_000,
22
+ };
23
+ export const TXFLOW_KLINE_INTERVALS = new Set(Object.keys(TXFLOW_KLINE_MS));
24
+ /**
25
+ * candleSnapshot answers `200 []` (no error) when the window spans more than ~3000 bars
26
+ * (measured 2026-10-19: 3000 bars OK, 3050 → [] at 1h; same at 1m / 5m / 1d), so ranges are
27
+ * read in windows of at most 2900 bars and merged.
28
+ */
29
+ export const TXFLOW_KLINE_WINDOW_BARS = 2900;
30
+ export const TXFLOW_KLINE_MAX_WINDOWS = 50;
16
31
  function dec(v, what) {
17
32
  if (typeof v !== "string" || !/^-?\d+(\.\d+)?$/.test(v))
18
33
  throw txflowShapeError(`${what} is not a decimal string`);
@@ -68,7 +83,7 @@ export class TxflowPublicAdapter {
68
83
  throw txflowShapeError(`trade side ${String(x.side)}`);
69
84
  if (typeof x.time !== "number")
70
85
  throw txflowShapeError("trade without time");
71
- // Public tape carries no fee; "0" follows the legacy ExchangeTrade convention for public trades.
86
+ // Public tape: no fee applies to the viewer → null (ExchangePublicTrade), never "0".
72
87
  return { time: x.time, symbol: m.symbol, side: x.side === "B" ? "buy" : "sell", price: dec(x.px, "trade px"), size: dec(x.sz, "trade sz"), fee: null };
73
88
  });
74
89
  }
@@ -76,27 +91,44 @@ export class TxflowPublicAdapter {
76
91
  throw unsupported("txflow", "fundingHistory", "TxFlow's unofficial API refuses funding-history reads (HTTP 403)", "Current TxFlow funding (unverified, per hour): perp -e txflow market info <symbol>, or perp arb scan --rates --include-unverified");
77
92
  }
78
93
  async getKlines(symbol, interval, startTime, endTime) {
79
- if (!TXFLOW_KLINE_INTERVALS.has(interval)) {
94
+ const ms = TXFLOW_KLINE_MS[interval];
95
+ if (ms === undefined) {
80
96
  throw new PerpError("INVALID_PARAMS", `TxFlow: unsupported candle interval "${interval}"`, {
81
97
  remediation: `Use one of: ${[...TXFLOW_KLINE_INTERVALS].join(", ")}`,
82
98
  });
83
99
  }
100
+ const span = TXFLOW_KLINE_WINDOW_BARS * ms;
101
+ const windows = Math.max(1, Math.ceil((endTime - startTime) / span));
102
+ if (windows > TXFLOW_KLINE_MAX_WINDOWS) {
103
+ throw new PerpError("INVALID_PARAMS", `TxFlow: ${interval} candles over this range need ${windows} requests (max ${TXFLOW_KLINE_MAX_WINDOWS})`, {
104
+ remediation: "Narrow --start / --end or use a longer interval",
105
+ });
106
+ }
84
107
  const m = await this.market(symbol);
85
- const res = await this.client.info({ type: "candleSnapshot", req: { coin: String(m.index), interval, startTime, endTime } });
86
- if (!Array.isArray(res))
87
- throw txflowShapeError("candleSnapshot is not an array");
88
- return res.map(c => {
89
- const x = c;
90
- if (typeof x.t !== "number")
91
- throw txflowShapeError("candle without t");
92
- return {
93
- time: x.t,
94
- open: dec(x.o, "candle o"), high: dec(x.h, "candle h"), low: dec(x.l, "candle l"), close: dec(x.c, "candle c"),
95
- volume: dec(x.v, "candle v"),
96
- quoteVolume: null,
97
- trades: typeof x.n === "number" ? x.n : null,
98
- };
99
- });
108
+ const byTime = new Map();
109
+ // Disjoint windows [from, to), each within the venue's silent ~3000-bar cap; merged by open time.
110
+ for (let from = startTime;; from += span) {
111
+ const to = Math.min(from + span, endTime);
112
+ const res = await this.client.info({ type: "candleSnapshot", req: { coin: String(m.index), interval, startTime: from, endTime: to } });
113
+ if (!Array.isArray(res))
114
+ throw txflowShapeError("candleSnapshot is not an array");
115
+ for (const c of res) {
116
+ const x = c;
117
+ if (typeof x.t !== "number")
118
+ throw txflowShapeError("candle without t");
119
+ assertCandleInterval("txflow", interval, x.i);
120
+ byTime.set(x.t, {
121
+ time: x.t,
122
+ open: dec(x.o, "candle o"), high: dec(x.h, "candle h"), low: dec(x.l, "candle l"), close: dec(x.c, "candle c"),
123
+ volume: dec(x.v, "candle v"),
124
+ quoteVolume: null,
125
+ trades: typeof x.n === "number" ? x.n : null,
126
+ });
127
+ }
128
+ if (to >= endTime)
129
+ break;
130
+ }
131
+ return [...byTime.values()].sort((a, b) => a.time - b.time);
100
132
  }
101
133
  // ── Account / trading: refused (market-data-only venue, unofficial API) ──
102
134
  refuse(feature) {
@@ -67,7 +67,9 @@ export class TxflowClient {
67
67
  });
68
68
  }
69
69
  if (!res.ok) {
70
- throw new PerpError("EXCHANGE_ERROR", `TxFlow ${body.type}: HTTP ${res.status} ${text.slice(0, 200)}`, {
70
+ // A 5xx can be a raw Cloudflare HTML page (live: candleSnapshot 1w / 3d → HTTP 500): tags stripped.
71
+ const clean = text.replace(/<(script|style)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200);
72
+ throw new PerpError("EXCHANGE_ERROR", `TxFlow ${body.type}: HTTP ${res.status} ${clean}`, {
71
73
  exchange: "txflow", status: res.status, remediation: UNOFFICIAL_REMEDIATION(body.type),
72
74
  });
73
75
  }
@@ -6,6 +6,9 @@
6
6
  * name "BTC-USDC", baseCurrency, szDecimals (can be
7
7
  * NEGATIVE: lot 10, 100 …), priceTick, maxLeverage,
8
8
  * haltTrading, delisted, tagIds
9
+ * POST /info {type:"allMarketTickers"} per instrument: dayNtlVlm (24 h USD notional), prevDayPx …;
10
+ * `instrumentId` == perpMeta `index` (spot rows ≥ 10000),
11
+ * empty fields are "" (→ null). `market list|prices` only.
9
12
  * WS allAssetCtxs (one-shot snapshot) per coin: markPx, oraclePx, funding, openInterest,
10
13
  * nextFundingTime, prevFundingTime. `coin` == perpMeta
11
14
  * `index` (as a string) — joined by id, never by position.
@@ -41,6 +44,7 @@ export interface TxflowMarketMeta {
41
44
  export declare function stepFromSzDecimals(sz: number): string;
42
45
  /** Exact percent → fraction (two-place decimal shift on the string; "0.00125" → "0.0000125"). */
43
46
  export declare function percentToFraction(pct: string): string;
47
+ export declare function looksTxflowTradFi(symbol: string, tagIds: readonly number[], fullName: string | null, description: string | null): boolean;
44
48
  /** Parse one perpMeta row; shape drift is EXCHANGE_ERROR. Returns null for a delisted market. */
45
49
  export declare function toTxflowMarketMeta(raw: unknown): TxflowMarketMeta | null;
46
50
  export declare function fetchTxflowDirectory(client: TxflowClient): Promise<TxflowMarketMeta[]>;
@@ -64,7 +68,14 @@ export declare function joinSnapshot(dir: readonly TxflowMarketMeta[], ctxs: rea
64
68
  * interval cannot be PROVEN from next − prev funding time (a missing time is unknown, never assumed 1h).
65
69
  */
66
70
  export declare function hourlyFundingOf(ctx: TxflowCtx): string | null;
67
- export declare function toTxflowVenueMarket(m: TxflowMarketMeta, ctx: TxflowCtx): VenueMarket;
71
+ /** First spot instrumentId in allMarketTickers (perp ids are the perpMeta index, below it). */
72
+ export declare const TXFLOW_SPOT_ID_BASE = 10000;
73
+ /**
74
+ * 24 h USD notional volume per perp coin id from allMarketTickers. "" (no trades) → null, never
75
+ * "0"; a non-list answer or a row without a numeric instrumentId is EXCHANGE_ERROR.
76
+ */
77
+ export declare function fetchTxflowDayVolumes(client: TxflowClient): Promise<Map<number, string | null>>;
78
+ export declare function toTxflowVenueMarket(m: TxflowMarketMeta, ctx: TxflowCtx, volume24h: string | null): VenueMarket;
68
79
  export interface TxflowPublicDeps {
69
80
  wsFactory: WsFactory;
70
81
  warn: (msg: string) => void;
@@ -6,6 +6,9 @@
6
6
  * name "BTC-USDC", baseCurrency, szDecimals (can be
7
7
  * NEGATIVE: lot 10, 100 …), priceTick, maxLeverage,
8
8
  * haltTrading, delisted, tagIds
9
+ * POST /info {type:"allMarketTickers"} per instrument: dayNtlVlm (24 h USD notional), prevDayPx …;
10
+ * `instrumentId` == perpMeta `index` (spot rows ≥ 10000),
11
+ * empty fields are "" (→ null). `market list|prices` only.
9
12
  * WS allAssetCtxs (one-shot snapshot) per coin: markPx, oraclePx, funding, openInterest,
10
13
  * nextFundingTime, prevFundingTime. `coin` == perpMeta
11
14
  * `index` (as a string) — joined by id, never by position.
@@ -65,14 +68,55 @@ export function percentToFraction(pct) {
65
68
  return "0";
66
69
  return neg ? `-${out}` : out;
67
70
  }
68
- function assetClassOf(symbol, tagIds) {
69
- if (!tagIds.includes(TXFLOW_TRADFI_TAG))
71
+ /**
72
+ * TradFi markets listed WITHOUT the TradFi tag (live 2026-10-19: ~38 rows with `tagIds: []`, e.g.
73
+ * COPPER, NATGAS, MRNA, GS, PATH, TEAM, TLT, SOXS) were classed as crypto and got a crypto
74
+ * canonical, so arb could pair GS or PATH with same-named crypto tokens. A row is TradFi when it is
75
+ * tagged, its description reads like a share / ETF / commodity / FX / pre-IPO contract, its
76
+ * fullName reads like a company or fund (as Variational's looksTradFi), or it is one of the
77
+ * untagged equities whose perpMeta row has no description (UNTAGGED_TRADFI, from the live listing).
78
+ */
79
+ const TRADFI_DESCRIPTION = /\b(common stock|ordinary shares|shares\b|ETF\b|Pre-IPO|exchange rate|price reference for|priced in US dollars, representing)/i;
80
+ const TRADFI_FULLNAME = /\b(Inc|Corp|Corporation|Company|Holdings?|Ltd|Limited|plc|PBC|N\.V|Group|ETF|Shares|Currency FX|Co\.)\b\.?/i;
81
+ const UNTAGGED_TRADFI = new Set([
82
+ "CRDO", "APP", "ADBE", "COST", "BX", "DKNG", "BRKB", "KO", "IBM", "QNTX", "UNH", "TWST", "CVNA", "MCD", "NKE",
83
+ "AAOI", "ASTS", "VST", "SNPS", "BSP",
84
+ ]);
85
+ /** Crypto tokens whose description reads like a commodity contract (Tether Gold is a token). */
86
+ const CRYPTO_DESPITE_DESCRIPTION = new Set(["XAUT"]);
87
+ export function looksTxflowTradFi(symbol, tagIds, fullName, description) {
88
+ if (tagIds.includes(TXFLOW_TRADFI_TAG))
89
+ return true;
90
+ if (CRYPTO_DESPITE_DESCRIPTION.has(symbol))
91
+ return false;
92
+ if (description !== null && TRADFI_DESCRIPTION.test(description))
93
+ return true;
94
+ if (fullName !== null && TRADFI_FULLNAME.test(fullName))
95
+ return true;
96
+ return UNTAGGED_TRADFI.has(symbol);
97
+ }
98
+ /** Class of an unclassified TradFi row from its listing text (its canonical stays null either way). */
99
+ function textClassOf(fullName, description) {
100
+ const text = `${fullName ?? ""} ${description ?? ""}`;
101
+ if (/exchange rate|Currency FX/i.test(text))
102
+ return "fx";
103
+ if (/Pre-IPO|\bPBC\b|private artificial intelligence company/i.test(text))
104
+ return "preIpo";
105
+ if (/\bETF\b|\bShares\b/.test(text))
106
+ return "etf";
107
+ if (/priced in US dollars, representing/i.test(text))
108
+ return "commodity";
109
+ return "equity";
110
+ }
111
+ function assetClassOf(symbol, tradFi, fullName, description) {
112
+ if (!tradFi)
70
113
  return "crypto";
71
- return TRADFI_CLASS[symbol] ?? "equity";
114
+ const known = TRADFI_CLASS[symbol];
115
+ return known !== undefined ? known : textClassOf(fullName, description);
72
116
  }
73
- function canonicalOf(symbol, tagIds, assetClass) {
74
- // An unclassified TradFi market is never matched across venues.
75
- if (tagIds.includes(TXFLOW_TRADFI_TAG) && TRADFI_CLASS[symbol] === undefined)
117
+ function canonicalOf(symbol, tradFi, assetClass) {
118
+ // An unclassified TradFi market (tagged or not) is never matched across venues.
119
+ if (tradFi && TRADFI_CLASS[symbol] === undefined)
76
120
  return null;
77
121
  return canonicalFor("txflow", symbol, assetClass);
78
122
  }
@@ -101,7 +145,10 @@ export function toTxflowMarketMeta(raw) {
101
145
  return null;
102
146
  const symbol = r.baseCurrency.toUpperCase();
103
147
  const tagIds = r.tagIds;
104
- const assetClass = assetClassOf(symbol, tagIds);
148
+ const fullName = typeof r.fullName === "string" ? r.fullName : null;
149
+ const description = typeof r.description === "string" ? r.description : null;
150
+ const tradFi = looksTxflowTradFi(symbol, tagIds, fullName, description);
151
+ const assetClass = assetClassOf(symbol, tradFi, fullName, description);
105
152
  return {
106
153
  index: r.index,
107
154
  name: r.name,
@@ -113,7 +160,7 @@ export function toTxflowMarketMeta(raw) {
113
160
  halted: r.haltTrading,
114
161
  tagIds,
115
162
  assetClass,
116
- canonical: canonicalOf(symbol, tagIds, assetClass),
163
+ canonical: canonicalOf(symbol, tradFi, assetClass),
117
164
  };
118
165
  }
119
166
  export async function fetchTxflowDirectory(client) {
@@ -189,7 +236,28 @@ export function hourlyFundingOf(ctx) {
189
236
  return percentToFraction(ctx.fundingPct);
190
237
  }
191
238
  // ── Mapping ──
192
- export function toTxflowVenueMarket(m, ctx) {
239
+ /** First spot instrumentId in allMarketTickers (perp ids are the perpMeta index, below it). */
240
+ export const TXFLOW_SPOT_ID_BASE = 10_000;
241
+ /**
242
+ * 24 h USD notional volume per perp coin id from allMarketTickers. "" (no trades) → null, never
243
+ * "0"; a non-list answer or a row without a numeric instrumentId is EXCHANGE_ERROR.
244
+ */
245
+ export async function fetchTxflowDayVolumes(client) {
246
+ const res = await client.info({ type: "allMarketTickers" });
247
+ if (!Array.isArray(res))
248
+ throw txflowShapeError("allMarketTickers is not an array");
249
+ const out = new Map();
250
+ for (const r of res) {
251
+ const x = r;
252
+ if (typeof x.instrumentId !== "number" || !Number.isInteger(x.instrumentId))
253
+ throw txflowShapeError("allMarketTickers row without a numeric instrumentId");
254
+ if (x.instrumentId >= TXFLOW_SPOT_ID_BASE)
255
+ continue;
256
+ out.set(x.instrumentId, decOrNull(x.dayNtlVlm));
257
+ }
258
+ return out;
259
+ }
260
+ export function toTxflowVenueMarket(m, ctx, volume24h) {
193
261
  return {
194
262
  venue: "txflow",
195
263
  venueSymbol: String(m.index),
@@ -205,7 +273,7 @@ export function toTxflowVenueMarket(m, ctx) {
205
273
  settlementIntervalHours: 1,
206
274
  // Base units (as Hyperliquid publishes it) [inferred].
207
275
  openInterest: ctx.openInterest,
208
- volume24h: null,
276
+ volume24h,
209
277
  tickSize: m.tickSize,
210
278
  stepSize: m.stepSize,
211
279
  minSize: null,
@@ -246,7 +314,10 @@ async function fetchJoined(net, deps) {
246
314
  return out;
247
315
  }
248
316
  export async function fetchTxflowMarkets(net, deps = DEFAULT_DEPS) {
249
- return (await fetchJoined(net, deps)).map(({ meta, ctx }) => toTxflowVenueMarket(meta, ctx));
317
+ const { client } = clientFor(net); // testnet → NOT_IMPLEMENTED before any request
318
+ const [joined, volumes] = await Promise.all([fetchJoined(net, deps), fetchTxflowDayVolumes(client)]);
319
+ // A market absent from allMarketTickers has no published volume → null.
320
+ return joined.map(({ meta, ctx }) => toTxflowVenueMarket(meta, ctx, volumes.has(meta.index) ? volumes.get(meta.index) : null));
250
321
  }
251
322
  export async function fetchTxflowFundingRates(net, deps = DEFAULT_DEPS) {
252
323
  const out = [];
@@ -12,7 +12,12 @@ import { PerpError } from "../errors.js";
12
12
  import type { ExchangeMarketInfo } from "./interface.js";
13
13
  /** SYMBOL_NOT_FOUND for `symbol` on `venue`, with the listing command as remediation. */
14
14
  export declare function symbolNotFound(venue: string, symbol: string, message?: string): PerpError;
15
- /** Directory match: case-insensitive, a "-PERP" display suffix ignored on either side. */
15
+ /**
16
+ * Directory match on the exact wire symbol (case-sensitive), a "-PERP" display suffix ignored on
17
+ * either side. Case-sensitive on purpose: the venues are (Pacifica `KPEPE` answers `200 []` where
18
+ * `kPEPE` has rows), so a casing miss must be SYMBOL_NOT_FOUND, never "no data". The adapter
19
+ * resolves the typed symbol to the venue's spelling before the request.
20
+ */
16
21
  export declare function listsSymbol(markets: readonly Pick<ExchangeMarketInfo, "symbol">[], symbol: string): boolean;
17
22
  /**
18
23
  * `rows` as returned by the venue, unless they are empty and the market is not in the venue's
@@ -16,9 +16,14 @@ export function symbolNotFound(venue, symbol, message) {
16
16
  remediation: `List markets: perp -e ${venue} market list`,
17
17
  });
18
18
  }
19
- /** Directory match: case-insensitive, a "-PERP" display suffix ignored on either side. */
19
+ /**
20
+ * Directory match on the exact wire symbol (case-sensitive), a "-PERP" display suffix ignored on
21
+ * either side. Case-sensitive on purpose: the venues are (Pacifica `KPEPE` answers `200 []` where
22
+ * `kPEPE` has rows), so a casing miss must be SYMBOL_NOT_FOUND, never "no data". The adapter
23
+ * resolves the typed symbol to the venue's spelling before the request.
24
+ */
20
25
  export function listsSymbol(markets, symbol) {
21
- const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
26
+ const norm = (s) => s.replace(/-PERP$/i, "");
22
27
  const want = norm(symbol);
23
28
  return markets.some(m => norm(m.symbol) === want);
24
29
  }
@@ -7,7 +7,7 @@
7
7
  * getFundingHistory refuse with CAPABILITY_UNSUPPORTED (with the command that does work), and every
8
8
  * account / trading method refuses before any request.
9
9
  */
10
- import type { CurrentFundingTerms, RfqQuotes, ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade } from "../interface.js";
10
+ import type { CurrentFundingTerms, RfqQuotes, ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade, FundingHistoryRow } from "../interface.js";
11
11
  import { type VenueDefinition } from "../venue.js";
12
12
  import { type Network } from "../venue-types.js";
13
13
  import { type VariationalSwapRate } from "./public.js";
@@ -48,11 +48,7 @@ export declare class VariationalPublicAdapter implements ExchangeAdapter {
48
48
  asks: [string, string][];
49
49
  }>;
50
50
  getRecentTrades(): Promise<ExchangePublicTrade[]>;
51
- getFundingHistory(): Promise<{
52
- time: number;
53
- rate: string;
54
- price: string | null;
55
- }[]>;
51
+ getFundingHistory(): Promise<FundingHistoryRow[]>;
56
52
  getKlines(): Promise<ExchangeKline[]>;
57
53
  private refuse;
58
54
  getBalance(): Promise<ExchangeBalance>;
@@ -4,7 +4,7 @@
4
4
  * SSOT Rule #2: every "unknown" value is `T | null`, never optional, never a
5
5
  * silently substituted default.
6
6
  */
7
- import type { ExchangeMarketInfo } from "./interface.js";
7
+ import type { ExchangeMarketInfo, FundingRateKind } from "./interface.js";
8
8
  import type { ResolvedSlippage, SlippageSource } from "./slippage.js";
9
9
  import type { AppliedBuilderFee } from "./builder-fee.js";
10
10
  export type VenueId = "pacifica" | "hyperliquid" | "lighter" | "aster" | "lighter-rh" | "ondo" | "arcus" | "qfex" | "risex" | "txflow" | "variational" | "grvt" | "extended";
@@ -23,7 +23,12 @@ export type FundingRateBasis = {
23
23
  };
24
24
  /** Convert a raw quoted rate to a per-hour rate. */
25
25
  export declare function rateToHourly(rate: number, basis: FundingRateBasis): number;
26
- export type AssetClass = "crypto" | "equity" | "index" | "etf" | "commodity" | "fx" | "preIpo";
26
+ /**
27
+ * "unclassified": listed, but its class is ambiguous (a ticker that names different instruments
28
+ * on different venues: SPX6900 vs the S&P 500, Pacifica BP / CHIP, Lighter BB / BOT / WEN / QNT,
29
+ * Lighter RH USELESS). Its canonical is always null, so it is never matched across venues.
30
+ */
31
+ export type AssetClass = "crypto" | "equity" | "index" | "etf" | "commodity" | "fx" | "preIpo" | "unclassified";
27
32
  export interface MarketSession {
28
33
  /** Whether the underlying (e.g. US equity) market is open right now. */
29
34
  underlyingOpen: boolean;
@@ -73,7 +78,18 @@ export interface VenueMarket {
73
78
  session: MarketSession | null;
74
79
  /** false = listed but not tradable right now (offline / paused / corporate action). */
75
80
  tradable: boolean;
81
+ /**
82
+ * The venue's listing status as the CLI market row shows it (Extended "ACTIVE" / "REDUCE_ONLY",
83
+ * Ondo "active" / …, QFEX "active" / "nonUsdQuote"); absent = the venue states none.
84
+ */
85
+ status?: string | null;
76
86
  }
87
+ /**
88
+ * Quantity decimals of a lot step, or null when the step is not a power of ten ≤ 1 ("0.001" → 3,
89
+ * "1" → 0; TxFlow "10" / "100" and a "0.5" step → null): a decimals count cannot express them, and
90
+ * 0 next to stepSize "10" read as "any integer". `stepSize` is the authoritative grid.
91
+ */
92
+ export declare function decimalsOf(step: string): number | null;
77
93
  /** Pure mapping used by new-venue adapters to serve the legacy getMarkets(). */
78
94
  /** Tick/step/leverage required for order entry. Throws when the venue did not publish them. */
79
95
  export declare function requireTradingPrecision(m: VenueMarket): {
@@ -81,16 +97,35 @@ export declare function requireTradingPrecision(m: VenueMarket): {
81
97
  stepSize: string;
82
98
  maxLeverage: number;
83
99
  };
100
+ export declare function fundingRateKindOf(venue: VenueMarket["venue"], fundingRate: string | null): FundingRateKind | null;
101
+ /**
102
+ * THE market-row shape (`perp market list` / `info` / `prices`, ExchangeMarketInfo) of a
103
+ * VenueMarket — the one mapper for the CLI adapters of VenueMarket venues and the keyless MCP
104
+ * `get_markets` public directory rows. It does not require trading precision: a public row
105
+ * without a published tick / lot / leverage maps those to null.
106
+ */
107
+ export declare function venueMarketRow(m: VenueMarket): ExchangeMarketInfo & {
108
+ tradable: boolean;
109
+ quoteAsset: string | null;
110
+ status: string | null;
111
+ };
112
+ /** venueMarketRow for an adapter that trades the market: tick / lot / leverage must be published. */
84
113
  export declare function toLegacyMarketInfo(m: VenueMarket): ExchangeMarketInfo;
85
114
  export interface VenueFundingRate {
86
115
  venue: VenueId;
87
116
  venueSymbol: string;
88
117
  symbol: string;
89
118
  canonical: string | null;
90
- rate: string;
119
+ /** null = no funding right now (`marketClosed` true: outside the market's funding hours). */
120
+ rate: string | null;
91
121
  basis: FundingRateBasis;
92
122
  markPrice: string | null;
93
123
  nextFundingTime: number | null;
124
+ /**
125
+ * true = the current funding window is outside the market's funding hours (QFEX market_hours):
126
+ * `rate` is null — the venue's 0 is "no funding", not a 0 rate. Absent = funds around the clock.
127
+ */
128
+ marketClosed?: boolean;
94
129
  }
95
130
  /** CLI-level TIF inputs. "TOB" = Pacifica top-of-book, "GTX" = Aster post-only. */
96
131
  export type TimeInForce = "GTC" | "GTT" | "IOC" | "FOK" | "ALO" | "TOB" | "GTX";
@@ -228,3 +263,11 @@ export interface LeverageResult {
228
263
  export declare function orderRefOf(r: Pick<OrderResult, "orderId" | "clientOrderId">): OrderRef;
229
264
  /** Parse a CLI order reference: "c:<id>" → clientOrderId, otherwise orderId. */
230
265
  export declare function parseOrderRef(s: string): OrderRef;
266
+ /**
267
+ * Next funding settlement of a venue that documents hourly settlement on the hour (UTC): the next
268
+ * top of the hour after `now`. Used only where the venue's docs say so — Hyperliquid ("paid every
269
+ * hour"), Lighter ("at each hour mark"), Pacifica ("at the end of each 1-hour interval") — and
270
+ * their settled history is stamped on the hour (live 2026-10-19: HL 18:00:00.139 … 21:00:00.032,
271
+ * Pacifica 18:00:02.674 … 21:00:02.574). Other venues keep null unless they publish the time.
272
+ */
273
+ export declare function nextTopOfHourMs(now?: number): number;
@@ -21,10 +21,17 @@ export function rateToHourly(rate, basis) {
21
21
  return rate / basis.hours;
22
22
  return rate / (24 * 365);
23
23
  }
24
- function decimalsOf(step) {
25
- const s = step.includes("e") || step.includes("E") ? Number(step).toFixed(20).replace(/0+$/, "") : step;
26
- const dot = s.indexOf(".");
27
- return dot === -1 ? 0 : s.length - dot - 1;
24
+ /**
25
+ * Quantity decimals of a lot step, or null when the step is not a power of ten ≤ 1 ("0.001" → 3,
26
+ * "1" → 0; TxFlow "10" / "100" and a "0.5" step → null): a decimals count cannot express them, and
27
+ * 0 next to stepSize "10" read as "any integer". `stepSize` is the authoritative grid.
28
+ */
29
+ export function decimalsOf(step) {
30
+ const s = step.includes("e") || step.includes("E") ? Number(step).toFixed(20) : step;
31
+ const t = s.includes(".") ? s.replace(/0+$/, "").replace(/\.$/, "") : s;
32
+ if (!/^(1|0\.0*1)$/.test(t))
33
+ return null;
34
+ return t === "1" ? 0 : t.length - 2;
28
35
  }
29
36
  /** Pure mapping used by new-venue adapters to serve the legacy getMarkets(). */
30
37
  /** Tick/step/leverage required for order entry. Throws when the venue did not publish them. */
@@ -34,9 +41,54 @@ export function requireTradingPrecision(m) {
34
41
  }
35
42
  return { tickSize: m.tickSize, stepSize: m.stepSize, maxLeverage: m.maxLeverage };
36
43
  }
37
- export function toLegacyMarketInfo(m) {
38
- const p = requireTradingPrecision(m);
44
+ /**
45
+ * What a VenueMarket.fundingRate is per venue (REST source), consistent with the WS `rateKind` where
46
+ * both read the same field. null = not verified. Venues with per-row kinds (GRVT, Extended,
47
+ * Variational) set their own on top.
48
+ * - arcus: /v1/markets fundingRate = the last hourly settlement (WS markets channel: lastSettled).
49
+ * - risex: current_funding_rate equals the last settled print (venues/risex.ts, verified 2026-10-14).
50
+ * - txflow: allAssetCtxs `funding` moves every tick — the running period's accruing estimate.
51
+ * - hyperliquid: metaAndAssetCtxs `funding` (WS allDexsAssetCtxs) — accruing.
52
+ * - aster: premiumIndex lastFundingRate moves during the period (WS markPrice@arr `r`) — accruing.
53
+ * - pacifica: /info/prices `funding` = the last settled hourly rate (WS prices `funding`).
54
+ * - lighter, lighter-rh: WS market_stats funding_rate = the newest /fundings print — lastSettled.
55
+ * - grvt: the ticker's funding rate is the next settlement's (predicted) — as the adapter labels it.
56
+ * - extended: /info/markets fundingRate is the current hour's accruing rate.
57
+ * - variational: the quoted rate is the predicted next payment.
58
+ * - ondo, qfex: not verified against settled prints → null.
59
+ */
60
+ const VENUE_MARKET_RATE_KIND = {
61
+ arcus: "lastSettled",
62
+ risex: "lastSettled",
63
+ txflow: "accruing",
64
+ hyperliquid: "accruing",
65
+ aster: "accruing",
66
+ pacifica: "lastSettled",
67
+ lighter: "lastSettled",
68
+ "lighter-rh": "lastSettled",
69
+ grvt: "predicted",
70
+ extended: "accruing",
71
+ variational: "predicted",
72
+ // Ondo /v1/perps/contracts `fundingRate` is the last settled print: live 2026-10-19 21:17 UTC it
73
+ // equalled the 21:00 funding_rate_history row on every market whose rate moves (CXMT 0.0001139,
74
+ // BB 0.00003, PURR 0.0000182, WTI 0.0000038, USDJPY 0.000001 — each different from the 20:00 print).
75
+ ondo: "lastSettled",
76
+ };
77
+ export function fundingRateKindOf(venue, fundingRate) {
78
+ if (fundingRate === null)
79
+ return null;
80
+ const k = VENUE_MARKET_RATE_KIND[venue];
81
+ return k === undefined ? null : k;
82
+ }
83
+ /**
84
+ * THE market-row shape (`perp market list` / `info` / `prices`, ExchangeMarketInfo) of a
85
+ * VenueMarket — the one mapper for the CLI adapters of VenueMarket venues and the keyless MCP
86
+ * `get_markets` public directory rows. It does not require trading precision: a public row
87
+ * without a published tick / lot / leverage maps those to null.
88
+ */
89
+ export function venueMarketRow(m) {
39
90
  const hours = m.fundingRateBasis.kind === "perHours" ? m.fundingRateBasis.hours : null;
91
+ const status = m.status;
40
92
  return {
41
93
  symbol: m.symbol,
42
94
  markPrice: m.markPrice,
@@ -44,13 +96,22 @@ export function toLegacyMarketInfo(m) {
44
96
  fundingRate: m.fundingRate,
45
97
  volume24h: m.volume24h,
46
98
  openInterest: m.openInterest,
47
- maxLeverage: p.maxLeverage,
48
- sizeDecimals: decimalsOf(p.stepSize),
49
- stepSize: p.stepSize,
99
+ maxLeverage: m.maxLeverage,
100
+ sizeDecimals: m.stepSize === null ? null : decimalsOf(m.stepSize),
101
+ stepSize: m.stepSize,
50
102
  fundingHours: hours,
51
103
  marginTiers: m.marginTiers,
104
+ tradable: m.tradable,
105
+ fundingRateKind: fundingRateKindOf(m.venue, m.fundingRate),
106
+ quoteAsset: m.quoteAsset,
107
+ status: typeof status === "string" ? status : null,
52
108
  };
53
109
  }
110
+ /** venueMarketRow for an adapter that trades the market: tick / lot / leverage must be published. */
111
+ export function toLegacyMarketInfo(m) {
112
+ requireTradingPrecision(m);
113
+ return venueMarketRow(m);
114
+ }
54
115
  /**
55
116
  * Attach an execution record to a result built without one. Fields of `partial`
56
117
  * that are absent are recorded as null (unknown), never defaulted to a value.
@@ -85,3 +146,13 @@ export function parseOrderRef(s) {
85
146
  return { kind: "clientOrderId", value: s.slice(2) };
86
147
  return { kind: "orderId", value: s };
87
148
  }
149
+ /**
150
+ * Next funding settlement of a venue that documents hourly settlement on the hour (UTC): the next
151
+ * top of the hour after `now`. Used only where the venue's docs say so — Hyperliquid ("paid every
152
+ * hour"), Lighter ("at each hour mark"), Pacifica ("at the end of each 1-hour interval") — and
153
+ * their settled history is stamped on the hour (live 2026-10-19: HL 18:00:00.139 … 21:00:00.032,
154
+ * Pacifica 18:00:02.674 … 21:00:02.574). Other venues keep null unless they publish the time.
155
+ */
156
+ export function nextTopOfHourMs(now = Date.now()) {
157
+ return (Math.floor(now / 3_600_000) + 1) * 3_600_000;
158
+ }