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.
- package/README.ja.md +243 -0
- package/README.ko.md +243 -0
- package/README.md +129 -535
- package/README.zh.md +243 -0
- package/dist/api/public/lighter.d.ts +5 -0
- package/dist/api/public/lighter.js +5 -0
- package/dist/api/public/pacifica.js +2 -2
- package/dist/arb/sizing.js +4 -2
- package/dist/arb/state.d.ts +6 -0
- package/dist/balance-by-address.d.ts +39 -0
- package/dist/balance-by-address.js +44 -0
- package/dist/bot/quote-rules.js +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +10 -3
- package/dist/bot/strategies/funding-arb-utils.d.ts +14 -2
- package/dist/bot/strategies/funding-arb-utils.js +48 -15
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +4 -2
- package/dist/bot/strategies/spot-perp-executor.js +2 -2
- package/dist/cli-program.js +1 -1
- package/dist/commands/account.js +168 -24
- package/dist/commands/arb/index.js +19 -77
- package/dist/commands/arb-auto.d.ts +24 -0
- package/dist/commands/arb-auto.js +155 -81
- package/dist/commands/arb.d.ts +8 -0
- package/dist/commands/arb.js +49 -23
- package/dist/commands/backtest.js +23 -21
- package/dist/commands/market.d.ts +48 -0
- package/dist/commands/market.js +241 -78
- package/dist/commands/options.d.ts +5 -0
- package/dist/commands/options.js +20 -3
- package/dist/commands/pacifica-earn.js +8 -21
- package/dist/commands/portfolio.d.ts +16 -0
- package/dist/commands/portfolio.js +51 -8
- package/dist/commands/trade.js +4 -4
- package/dist/cross-chain-margin.d.ts +3 -1
- package/dist/cross-chain-margin.js +4 -2
- package/dist/dashboard/server.js +2 -2
- package/dist/dashboard/ui.js +9 -6
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +10 -0
- package/dist/exchanges/address.d.ts +8 -0
- package/dist/exchanges/address.js +47 -0
- package/dist/exchanges/arcus/adapter.d.ts +10 -6
- package/dist/exchanges/arcus/adapter.js +33 -6
- package/dist/exchanges/arcus/public.js +3 -2
- package/dist/exchanges/aster.d.ts +27 -7
- package/dist/exchanges/aster.js +124 -42
- package/dist/exchanges/canonical-symbols.d.ts +7 -0
- package/dist/exchanges/canonical-symbols.js +20 -0
- package/dist/exchanges/capabilities.d.ts +2 -0
- package/dist/exchanges/capabilities.js +2 -1
- package/dist/exchanges/derive/options.d.ts +5 -0
- package/dist/exchanges/derive/options.js +20 -1
- package/dist/exchanges/extended/adapter.d.ts +16 -2
- package/dist/exchanges/extended/adapter.js +96 -34
- package/dist/exchanges/extended/public.d.ts +3 -0
- package/dist/exchanges/extended/public.js +9 -2
- package/dist/exchanges/grvt/adapter.d.ts +16 -2
- package/dist/exchanges/grvt/adapter.js +98 -21
- package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
- package/dist/exchanges/hyperliquid-outcome.js +29 -1
- package/dist/exchanges/hyperliquid-spot.d.ts +8 -0
- package/dist/exchanges/hyperliquid-spot.js +20 -12
- package/dist/exchanges/hyperliquid.d.ts +180 -13
- package/dist/exchanges/hyperliquid.js +487 -185
- package/dist/exchanges/interface.d.ts +64 -8
- package/dist/exchanges/interface.js +31 -0
- package/dist/exchanges/lighter-public.d.ts +66 -11
- package/dist/exchanges/lighter-public.js +249 -81
- package/dist/exchanges/lighter-spot.d.ts +7 -1
- package/dist/exchanges/lighter-spot.js +99 -90
- package/dist/exchanges/lighter.d.ts +70 -11
- package/dist/exchanges/lighter.js +285 -92
- package/dist/exchanges/ondo/adapter.d.ts +34 -8
- package/dist/exchanges/ondo/adapter.js +73 -16
- package/dist/exchanges/ondo/public.d.ts +27 -2
- package/dist/exchanges/ondo/public.js +63 -31
- package/dist/exchanges/outcome-interface.d.ts +4 -0
- package/dist/exchanges/pacifica-spot.d.ts +6 -0
- package/dist/exchanges/pacifica-spot.js +23 -15
- package/dist/exchanges/pacifica-vaults.d.ts +9 -0
- package/dist/exchanges/pacifica-vaults.js +14 -1
- package/dist/exchanges/pacifica.d.ts +77 -6
- package/dist/exchanges/pacifica.js +321 -130
- 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 +52 -9
- package/dist/exchanges/qfex/adapter.js +74 -9
- package/dist/exchanges/qfex/public.d.ts +62 -9
- package/dist/exchanges/qfex/public.js +128 -22
- package/dist/exchanges/qfex/ws-transport.d.ts +1 -1
- package/dist/exchanges/risex/adapter.d.ts +19 -7
- package/dist/exchanges/risex/adapter.js +58 -12
- package/dist/exchanges/spot-factory.js +1 -1
- package/dist/exchanges/txflow/adapter.d.ts +14 -7
- package/dist/exchanges/txflow/adapter.js +51 -19
- package/dist/exchanges/txflow/client.js +3 -1
- package/dist/exchanges/txflow/public.d.ts +12 -1
- package/dist/exchanges/txflow/public.js +82 -11
- package/dist/exchanges/unknown-symbol.d.ts +6 -1
- package/dist/exchanges/unknown-symbol.js +7 -2
- package/dist/exchanges/variational/adapter.d.ts +2 -6
- package/dist/exchanges/venue-types.d.ts +46 -3
- package/dist/exchanges/venue-types.js +80 -9
- package/dist/exchanges/venue.d.ts +99 -3
- package/dist/exchanges/venue.js +72 -0
- package/dist/exchanges/venues/hyperliquid.js +9 -0
- package/dist/exchanges/venues/lighter-rh.js +13 -3
- package/dist/exchanges/venues/lighter.js +13 -3
- package/dist/exchanges/venues/pacifica.js +9 -0
- package/dist/exchanges/venues/risex.js +15 -0
- package/dist/funding/legacy-public.d.ts +5 -3
- package/dist/funding/legacy-public.js +33 -37
- package/dist/funding/rates.d.ts +54 -5
- package/dist/funding/rates.js +49 -13
- package/dist/liquidity.d.ts +3 -1
- package/dist/liquidity.js +8 -4
- package/dist/mcp-pacifica-earn.js +11 -5
- package/dist/mcp-server.js +104 -15
- package/dist/pacifica/client.d.ts +2 -1
- package/dist/pacifica/client.js +11 -0
- package/dist/positions-by-address.d.ts +34 -0
- package/dist/positions-by-address.js +46 -0
- package/dist/strategies/funding-arb.js +35 -62
- package/dist/utils.d.ts +11 -1
- package/dist/venue-health.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 +30 -9
- package/dist/ws/index.js +180 -15
- package/dist/ws/lighter-market.d.ts +14 -0
- package/dist/ws/lighter-market.js +13 -0
- package/dist/ws/node.d.ts +30 -9
- package/dist/ws/node.js +204 -16
- package/dist/ws/unclassified.d.ts +16 -0
- package/dist/ws/unclassified.js +43 -0
- package/package.json +15 -2
- package/skills/perp-cli/SKILL.md +97 -33
- package/skills/perp-cli/package.json +15 -2
- package/skills/perp-cli/references/agent-operations.md +15 -12
- package/skills/perp-cli/references/commands.md +67 -12
- package/skills/perp-cli/references/strategies.md +38 -3
- package/skills/perp-cli/references/ws-library.md +55 -24
- 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
|
@@ -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
|
-
/**
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
throw txflowShapeError("
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
114
|
+
const known = TRADFI_CLASS[symbol];
|
|
115
|
+
return known !== undefined ? known : textClassOf(fullName, description);
|
|
72
116
|
}
|
|
73
|
-
function canonicalOf(symbol,
|
|
74
|
-
// An unclassified TradFi market is never matched across venues.
|
|
75
|
-
if (
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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:
|
|
48
|
-
sizeDecimals: decimalsOf(
|
|
49
|
-
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
|
+
}
|