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,16 +6,25 @@
|
|
|
6
6
|
* quote / margin asset, allowed order types + TIFs, market hours
|
|
7
7
|
* MDS market_stats snapshot mark price (each entry with its own `time`), open interest,
|
|
8
8
|
* 24h notional volume
|
|
9
|
-
* GET /md/contracts funding_rate / next_funding_rate_timestamp (funding reads
|
|
9
|
+
* GET /md/contracts funding_rate / next_funding_rate_timestamp (funding reads);
|
|
10
|
+
* index_price (market rows, best effort)
|
|
10
11
|
*
|
|
11
12
|
* Coupling: order-entry precision comes from /refdata ALONE (fetchQfexInstruments).
|
|
12
|
-
* Markets need /refdata + the MDS snapshot
|
|
13
|
-
*
|
|
13
|
+
* Markets need /refdata + the MDS snapshot; /md/contracts only adds indexPrice — its failure
|
|
14
|
+
* leaves indexPrice null (unknown) with a warning, never fails market listing or order entry.
|
|
15
|
+
* Funding reads need /md/contracts.
|
|
16
|
+
*
|
|
17
|
+
* indexPrice = /md/contracts `index_price` (docs contract-specifications: the Index Price is "the
|
|
18
|
+
* reference spot price for the asset"; the OpenAPI field has no description). Live 2026-10-19 it
|
|
19
|
+
* tracked the MDS `underlier` channel (AAPL 342.47 = 342.47, GOLD 4170.22 vs 4170.05, SAMSUNG-KRW
|
|
20
|
+
* 220092 vs 220082 KRW), moved on 122 of 222 markets within 45 s, and sat a median 0.015 % from
|
|
21
|
+
* the mark (max 5 %, CVNA pre-market). It is in the quote currency (`index_currency`), like the mark.
|
|
22
|
+
* /md/contracts lists ACTIVE markets only; a non-positive / missing value is null.
|
|
14
23
|
*
|
|
15
24
|
* Funding is UNVERIFIED (venue flag fundingVerified: false, Q4): the rate's unit is not
|
|
16
25
|
* confirmed against settled payments and `funding_rate` vs `next_funding_rate` is undecided.
|
|
17
26
|
* Markets therefore carry fundingRate null; fetchFundingRates returns the raw `funding_rate`
|
|
18
|
-
* for the explicit, flagged reads only (`
|
|
27
|
+
* for the explicit, flagged reads only (`arb scan --rates|--compare --include-unverified`).
|
|
19
28
|
*
|
|
20
29
|
* Symbols: "AAPL-USD" → CLI symbol "AAPL". Non-USD quotes keep the suffix ("SKHYNIX-KRW"),
|
|
21
30
|
* have no canonical symbol and are NOT tradable through perp-cli (USD-assuming sizing / risk).
|
|
@@ -60,6 +69,14 @@ export interface QfexContractRow {
|
|
|
60
69
|
funding_rate: string;
|
|
61
70
|
next_funding_rate: string;
|
|
62
71
|
next_funding_rate_timestamp: number;
|
|
72
|
+
/** Index (reference spot) price in the quote currency — see the module header. */
|
|
73
|
+
index_price?: string;
|
|
74
|
+
/**
|
|
75
|
+
* NOT USD on a non-USD market despite the name: live 2026-10-19 SAMSUNG-KRW 1,234,418 =
|
|
76
|
+
* open_interest 5.58 × index 221,118 → KRW (as is target_volume). Nothing reads it; never wire it
|
|
77
|
+
* up as a USD figure. OI is reported in base units (VenueMarket.openInterest, MDS open_interests).
|
|
78
|
+
*/
|
|
79
|
+
open_interest_usd?: string;
|
|
63
80
|
}
|
|
64
81
|
/** One instrument, as order entry needs it — from /refdata only. */
|
|
65
82
|
export interface QfexInstrument {
|
|
@@ -92,15 +109,40 @@ export declare function toQfexInstrument(raw: unknown): QfexInstrument;
|
|
|
92
109
|
/** Is the market one perp-cli may send orders to? USD-quoted and ACTIVE. */
|
|
93
110
|
export declare function qfexTradable(i: Pick<QfexInstrument, "status" | "quoteAsset">): boolean;
|
|
94
111
|
/**
|
|
95
|
-
* Whether `nowMs` falls inside a `session: "open"` window of the instrument's market_hours
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* accrues (the schedule and the documented funding hours do not map 1:1, Q9).
|
|
112
|
+
* Whether `nowMs` falls inside a `session: "open"` window of the instrument's market_hours.
|
|
113
|
+
* null = the venue published no usable schedule. This is the underlying's regular session only;
|
|
114
|
+
* funding hours are qfexFundingWindowClosed's question.
|
|
99
115
|
*/
|
|
100
116
|
export declare function qfexUnderlyingOpen(marketHours: Record<string, unknown> | null, nowMs: number): boolean | null;
|
|
117
|
+
/**
|
|
118
|
+
* Was the funding window labelled `windowStartMs` outside the market's funding hours? Docs
|
|
119
|
+
* (docs.qfex.com/qfex/funding): "Funding is only active during a symbol's configured
|
|
120
|
+
* market_hours"; outside them "funding rate is 0 and no funding payment is exchanged".
|
|
121
|
+
*
|
|
122
|
+
* The window labelled T covers the hour BEFORE T (live 2026-10-19, GET /funding, 9 symbols over
|
|
123
|
+
* 10 days: AAPL-USD prints its last non-zero rate at Sat 00:00Z = Fri 20:00 ET and its first at
|
|
124
|
+
* Mon 01:00Z = Sun 21:00 ET, i.e. funding Sun 20:00–Fri 20:00 ET as documented, only when each
|
|
125
|
+
* label is read as the end of its hour).
|
|
126
|
+
*
|
|
127
|
+
* market_hours alone does not say whether an unlisted gap funds (US equities fund overnight
|
|
128
|
+
* between post_market and pre_market; Korean equities do not fund outside "open"), so a window is
|
|
129
|
+
* closed when the venue printed exactly 0 AND no part of the hour was in a funding session
|
|
130
|
+
* (anything but "weekend" / "close"). A non-zero rate is never marked closed, and a 0 inside a
|
|
131
|
+
* session stays a real 0. No usable schedule → never closed.
|
|
132
|
+
*
|
|
133
|
+
* Known limits (review B2-4 / B2-5):
|
|
134
|
+
* - market holidays are not in market_hours: a holiday inside a configured weekday session
|
|
135
|
+
* (SAMSUNG-KRW on Hangul Day) prints the venue's 0 as a real-looking "0", marketClosed false;
|
|
136
|
+
* - US equities fund in the weekday overnight gap (post_market → pre_market, docs "Sunday 20:00 –
|
|
137
|
+
* Friday 20:00 ET"), so a gap is never closed by schedule alone — only an exact 0 there is, and
|
|
138
|
+
* a genuine 0 rate in such a gap is (rarely) reported as closed.
|
|
139
|
+
*/
|
|
140
|
+
export declare function qfexFundingWindowClosed(marketHours: Record<string, unknown> | null, windowStartMs: number, rate: number): boolean;
|
|
101
141
|
export declare function qfexPublicClient(net: Network): QfexClient;
|
|
102
142
|
/** Every instrument of /refdata (all statuses). The only input of order-entry validation. */
|
|
103
143
|
export declare function fetchQfexInstruments(client: QfexClient): Promise<QfexInstrument[]>;
|
|
144
|
+
/** venue symbol → /md/contracts `index_price` (positive finite only). */
|
|
145
|
+
export declare function qfexIndexPrices(contracts: readonly Partial<QfexContractRow>[]): Map<string, string>;
|
|
104
146
|
export declare function buildQfexMarkets(instruments: readonly QfexInstrument[], snap: {
|
|
105
147
|
marks: ReadonlyMap<string, {
|
|
106
148
|
price: string;
|
|
@@ -108,10 +150,21 @@ export declare function buildQfexMarkets(instruments: readonly QfexInstrument[],
|
|
|
108
150
|
}>;
|
|
109
151
|
openInterest: ReadonlyMap<string, string>;
|
|
110
152
|
volumeNotional: ReadonlyMap<string, string>;
|
|
111
|
-
}, nowMs: number): {
|
|
153
|
+
}, nowMs: number, indexPrices?: ReadonlyMap<string, string>): {
|
|
112
154
|
markets: VenueMarket[];
|
|
113
155
|
activeWithoutMark: string[];
|
|
114
156
|
};
|
|
157
|
+
/**
|
|
158
|
+
* Priced markets, the /refdata instruments they were built from (status / quote asset per row) and
|
|
159
|
+
* the 24h notional per venue symbol in its QUOTE currency (KRW for SAMSUNG-KRW).
|
|
160
|
+
*/
|
|
161
|
+
export declare function fetchQfexMarketsWithInstruments(net: Network): Promise<{
|
|
162
|
+
markets: VenueMarket[];
|
|
163
|
+
instruments: QfexInstrument[];
|
|
164
|
+
volumeNotional: ReadonlyMap<string, string>;
|
|
165
|
+
}>;
|
|
166
|
+
/** "active" = ACTIVE and USD-quoted; "nonUsdQuote" = ACTIVE, quoted in KRW / JPY / … (readable, never ordered). */
|
|
167
|
+
export declare function qfexListedStatus(i: Pick<QfexInstrument, "status" | "quoteAsset" | "venueSymbol">): "active" | "nonUsdQuote";
|
|
115
168
|
export declare function buildQfexFundingRates(instruments: readonly QfexInstrument[], contracts: readonly QfexContractRow[], marks: ReadonlyMap<string, {
|
|
116
169
|
price: string;
|
|
117
170
|
time: number;
|
|
@@ -6,16 +6,25 @@
|
|
|
6
6
|
* quote / margin asset, allowed order types + TIFs, market hours
|
|
7
7
|
* MDS market_stats snapshot mark price (each entry with its own `time`), open interest,
|
|
8
8
|
* 24h notional volume
|
|
9
|
-
* GET /md/contracts funding_rate / next_funding_rate_timestamp (funding reads
|
|
9
|
+
* GET /md/contracts funding_rate / next_funding_rate_timestamp (funding reads);
|
|
10
|
+
* index_price (market rows, best effort)
|
|
10
11
|
*
|
|
11
12
|
* Coupling: order-entry precision comes from /refdata ALONE (fetchQfexInstruments).
|
|
12
|
-
* Markets need /refdata + the MDS snapshot
|
|
13
|
-
*
|
|
13
|
+
* Markets need /refdata + the MDS snapshot; /md/contracts only adds indexPrice — its failure
|
|
14
|
+
* leaves indexPrice null (unknown) with a warning, never fails market listing or order entry.
|
|
15
|
+
* Funding reads need /md/contracts.
|
|
16
|
+
*
|
|
17
|
+
* indexPrice = /md/contracts `index_price` (docs contract-specifications: the Index Price is "the
|
|
18
|
+
* reference spot price for the asset"; the OpenAPI field has no description). Live 2026-10-19 it
|
|
19
|
+
* tracked the MDS `underlier` channel (AAPL 342.47 = 342.47, GOLD 4170.22 vs 4170.05, SAMSUNG-KRW
|
|
20
|
+
* 220092 vs 220082 KRW), moved on 122 of 222 markets within 45 s, and sat a median 0.015 % from
|
|
21
|
+
* the mark (max 5 %, CVNA pre-market). It is in the quote currency (`index_currency`), like the mark.
|
|
22
|
+
* /md/contracts lists ACTIVE markets only; a non-positive / missing value is null.
|
|
14
23
|
*
|
|
15
24
|
* Funding is UNVERIFIED (venue flag fundingVerified: false, Q4): the rate's unit is not
|
|
16
25
|
* confirmed against settled payments and `funding_rate` vs `next_funding_rate` is undecided.
|
|
17
26
|
* Markets therefore carry fundingRate null; fetchFundingRates returns the raw `funding_rate`
|
|
18
|
-
* for the explicit, flagged reads only (`
|
|
27
|
+
* for the explicit, flagged reads only (`arb scan --rates|--compare --include-unverified`).
|
|
19
28
|
*
|
|
20
29
|
* Symbols: "AAPL-USD" → CLI symbol "AAPL". Non-USD quotes keep the suffix ("SKHYNIX-KRW"),
|
|
21
30
|
* have no canonical symbol and are NOT tradable through perp-cli (USD-assuming sizing / risk).
|
|
@@ -110,19 +119,17 @@ export function qfexTradable(i) {
|
|
|
110
119
|
// ── Market hours → session ──
|
|
111
120
|
const WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
112
121
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* This is the underlying's regular session only — it is NOT used to infer whether funding
|
|
116
|
-
* accrues (the schedule and the documented funding hours do not map 1:1, Q9).
|
|
122
|
+
* Session names of the instrument's market_hours that contain `ms` (weekday + local time in its
|
|
123
|
+
* `tz`); [] = no window listed at that time. null = the venue published no usable schedule.
|
|
117
124
|
*/
|
|
118
|
-
|
|
125
|
+
function qfexSessionsAt(marketHours, ms) {
|
|
119
126
|
if (marketHours === null || typeof marketHours.tz !== "string")
|
|
120
127
|
return null;
|
|
121
128
|
let parts;
|
|
122
129
|
try {
|
|
123
130
|
parts = new Intl.DateTimeFormat("en-US", {
|
|
124
131
|
timeZone: marketHours.tz, weekday: "long", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23",
|
|
125
|
-
}).formatToParts(new Date(
|
|
132
|
+
}).formatToParts(new Date(ms));
|
|
126
133
|
}
|
|
127
134
|
catch {
|
|
128
135
|
return null; // an unknown IANA zone name is "no usable schedule", not a closed market
|
|
@@ -135,19 +142,70 @@ export function qfexUnderlyingOpen(marketHours, nowMs) {
|
|
|
135
142
|
const now = `${h}:${m}:${s}`;
|
|
136
143
|
const day = marketHours[weekday];
|
|
137
144
|
if (day === undefined)
|
|
138
|
-
return
|
|
145
|
+
return []; // no window listed for this weekday
|
|
139
146
|
if (!Array.isArray(day))
|
|
140
147
|
return null;
|
|
148
|
+
const out = [];
|
|
141
149
|
for (const w of day) {
|
|
142
150
|
if (w === null || typeof w !== "object")
|
|
143
151
|
return null;
|
|
144
152
|
const win = w;
|
|
145
153
|
if (typeof win.session !== "string" || typeof win.open !== "string" || typeof win.close !== "string")
|
|
146
154
|
return null;
|
|
147
|
-
if (
|
|
148
|
-
|
|
155
|
+
if (now >= win.open && now <= win.close)
|
|
156
|
+
out.push(win.session);
|
|
149
157
|
}
|
|
150
|
-
return
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Whether `nowMs` falls inside a `session: "open"` window of the instrument's market_hours.
|
|
162
|
+
* null = the venue published no usable schedule. This is the underlying's regular session only;
|
|
163
|
+
* funding hours are qfexFundingWindowClosed's question.
|
|
164
|
+
*/
|
|
165
|
+
export function qfexUnderlyingOpen(marketHours, nowMs) {
|
|
166
|
+
const sessions = qfexSessionsAt(marketHours, nowMs);
|
|
167
|
+
return sessions === null ? null : sessions.includes("open");
|
|
168
|
+
}
|
|
169
|
+
/** market_hours session names during which no funding accrues. */
|
|
170
|
+
const QFEX_NON_FUNDING_SESSIONS = new Set(["weekend", "close"]);
|
|
171
|
+
/**
|
|
172
|
+
* Was the funding window labelled `windowStartMs` outside the market's funding hours? Docs
|
|
173
|
+
* (docs.qfex.com/qfex/funding): "Funding is only active during a symbol's configured
|
|
174
|
+
* market_hours"; outside them "funding rate is 0 and no funding payment is exchanged".
|
|
175
|
+
*
|
|
176
|
+
* The window labelled T covers the hour BEFORE T (live 2026-10-19, GET /funding, 9 symbols over
|
|
177
|
+
* 10 days: AAPL-USD prints its last non-zero rate at Sat 00:00Z = Fri 20:00 ET and its first at
|
|
178
|
+
* Mon 01:00Z = Sun 21:00 ET, i.e. funding Sun 20:00–Fri 20:00 ET as documented, only when each
|
|
179
|
+
* label is read as the end of its hour).
|
|
180
|
+
*
|
|
181
|
+
* market_hours alone does not say whether an unlisted gap funds (US equities fund overnight
|
|
182
|
+
* between post_market and pre_market; Korean equities do not fund outside "open"), so a window is
|
|
183
|
+
* closed when the venue printed exactly 0 AND no part of the hour was in a funding session
|
|
184
|
+
* (anything but "weekend" / "close"). A non-zero rate is never marked closed, and a 0 inside a
|
|
185
|
+
* session stays a real 0. No usable schedule → never closed.
|
|
186
|
+
*
|
|
187
|
+
* Known limits (review B2-4 / B2-5):
|
|
188
|
+
* - market holidays are not in market_hours: a holiday inside a configured weekday session
|
|
189
|
+
* (SAMSUNG-KRW on Hangul Day) prints the venue's 0 as a real-looking "0", marketClosed false;
|
|
190
|
+
* - US equities fund in the weekday overnight gap (post_market → pre_market, docs "Sunday 20:00 –
|
|
191
|
+
* Friday 20:00 ET"), so a gap is never closed by schedule alone — only an exact 0 there is, and
|
|
192
|
+
* a genuine 0 rate in such a gap is (rarely) reported as closed.
|
|
193
|
+
*/
|
|
194
|
+
export function qfexFundingWindowClosed(marketHours, windowStartMs, rate) {
|
|
195
|
+
if (rate !== 0)
|
|
196
|
+
return false;
|
|
197
|
+
const from = windowStartMs - 3_600_000;
|
|
198
|
+
// Sampled inside the hour, never on its edges: market_hours closes are inclusive and often on
|
|
199
|
+
// the hour (GOLD-USD Fri close "17:00:00"), so a sample AT `from` would put the hour after the
|
|
200
|
+
// close inside the session (GOLD printed 0 for Fri 17:00–18:00 ET on 2026-10-09 and 10-16).
|
|
201
|
+
for (const t of [from + 1_000, from + 900_000, from + 1_800_000, from + 2_700_000, windowStartMs - 1_000]) {
|
|
202
|
+
const sessions = qfexSessionsAt(marketHours, t);
|
|
203
|
+
if (sessions === null)
|
|
204
|
+
return false;
|
|
205
|
+
if (sessions.some(x => !QFEX_NON_FUNDING_SESSIONS.has(x)))
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
151
209
|
}
|
|
152
210
|
function sessionOf(i, nowMs) {
|
|
153
211
|
const open = qfexUnderlyingOpen(i.marketHours, nowMs);
|
|
@@ -173,10 +231,24 @@ function canonicalOf(i) {
|
|
|
173
231
|
return null;
|
|
174
232
|
return canonicalFor("qfex", i.symbol, i.assetClass);
|
|
175
233
|
}
|
|
176
|
-
|
|
234
|
+
/** venue symbol → /md/contracts `index_price` (positive finite only). */
|
|
235
|
+
export function qfexIndexPrices(contracts) {
|
|
236
|
+
const out = new Map();
|
|
237
|
+
for (const c of contracts) {
|
|
238
|
+
const v = c.index_price;
|
|
239
|
+
if (typeof c.ticker_id === "string" && typeof v === "string" && Number.isFinite(Number(v)) && Number(v) > 0)
|
|
240
|
+
out.set(c.ticker_id, v);
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
export function buildQfexMarkets(instruments, snap, nowMs, indexPrices = new Map()) {
|
|
177
245
|
const markets = [];
|
|
178
246
|
const activeWithoutMark = [];
|
|
179
247
|
for (const i of instruments) {
|
|
248
|
+
// Listing policy: a DELISTED / INACTIVE market is not listed (per-symbol reads refuse it); only
|
|
249
|
+
// ACTIVE markets are, with tradable false when perp-cli cannot trade them (non-USD quote).
|
|
250
|
+
if (i.status !== "ACTIVE")
|
|
251
|
+
continue;
|
|
180
252
|
const mark = snap.marks.get(i.venueSymbol);
|
|
181
253
|
if (mark === undefined) {
|
|
182
254
|
if (i.status === "ACTIVE")
|
|
@@ -194,12 +266,16 @@ export function buildQfexMarkets(instruments, snap, nowMs) {
|
|
|
194
266
|
settle: i.marginAsset,
|
|
195
267
|
quoteAsset: i.quoteAsset,
|
|
196
268
|
markPrice: mark.price,
|
|
197
|
-
indexPrice: null,
|
|
269
|
+
indexPrice: indexPrices.get(i.venueSymbol) ?? null,
|
|
198
270
|
fundingRate: null, // unverified (Q4) — see module header
|
|
199
271
|
fundingRateBasis: QFEX_FUNDING_BASIS,
|
|
200
272
|
settlementIntervalHours: QFEX_SETTLEMENT_HOURS,
|
|
201
273
|
openInterest: oi === undefined ? null : oi,
|
|
202
|
-
|
|
274
|
+
// daily_volumes_notional is in the QUOTE currency (live 2026-10-19: SAMSUNG-KRW 9,827,530 =
|
|
275
|
+
// /md/contracts target_volume 9,763,491 KRW ≈ base_volume 45.16 × ~216,000 KRW, and the
|
|
276
|
+
// 24 hourly candles' usdVolume sum to 9,821,767). VenueMarket.volume24h is USD, and the venue
|
|
277
|
+
// publishes no USD-KRW / JPY / CNY / SEK / HKD rate → null for a non-USD quote.
|
|
278
|
+
volume24h: vol === undefined || i.quoteAsset !== "USD" ? null : vol,
|
|
203
279
|
tickSize: i.tickSize,
|
|
204
280
|
stepSize: i.lotSize,
|
|
205
281
|
minSize: i.minQuantity,
|
|
@@ -212,14 +288,40 @@ export function buildQfexMarkets(instruments, snap, nowMs) {
|
|
|
212
288
|
}
|
|
213
289
|
return { markets, activeWithoutMark };
|
|
214
290
|
}
|
|
215
|
-
|
|
216
|
-
|
|
291
|
+
/**
|
|
292
|
+
* Priced markets, the /refdata instruments they were built from (status / quote asset per row) and
|
|
293
|
+
* the 24h notional per venue symbol in its QUOTE currency (KRW for SAMSUNG-KRW).
|
|
294
|
+
*/
|
|
295
|
+
export async function fetchQfexMarketsWithInstruments(net) {
|
|
296
|
+
const client = qfexPublicClient(net);
|
|
297
|
+
const instruments = await fetchQfexInstruments(client);
|
|
217
298
|
const snap = await fetchQfexMarketStats(QFEX_ENDPOINTS[net].mdsWs, "*", QFEX_MDS_TIMEOUT_MS);
|
|
218
|
-
|
|
299
|
+
let indexPrices = new Map();
|
|
300
|
+
try {
|
|
301
|
+
const body = await client.publicGet("/md/contracts", null);
|
|
302
|
+
if (!Array.isArray(body.data))
|
|
303
|
+
throw new Error("response has no data list");
|
|
304
|
+
indexPrices = qfexIndexPrices(body.data);
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
console.warn(`[qfex] index prices unavailable (/md/contracts: ${e instanceof Error ? e.message : String(e)}); indexPrice left null`);
|
|
308
|
+
}
|
|
309
|
+
const { markets, activeWithoutMark } = buildQfexMarkets(instruments, snap, Date.now(), indexPrices);
|
|
219
310
|
if (activeWithoutMark.length > 0) {
|
|
220
311
|
console.warn(`[qfex] excluded ${activeWithoutMark.length} active market(s) without a mark price: ${activeWithoutMark.join(", ")}`);
|
|
221
312
|
}
|
|
222
|
-
return markets;
|
|
313
|
+
return { markets, instruments, volumeNotional: snap.volumeNotional };
|
|
314
|
+
}
|
|
315
|
+
/** "active" = ACTIVE and USD-quoted; "nonUsdQuote" = ACTIVE, quoted in KRW / JPY / … (readable, never ordered). */
|
|
316
|
+
export function qfexListedStatus(i) {
|
|
317
|
+
if (i.status !== "ACTIVE")
|
|
318
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX ${i.venueSymbol} is ${i.status}; it is not listed`, { exchange: "qfex" });
|
|
319
|
+
return qfexTradable(i) ? "active" : "nonUsdQuote";
|
|
320
|
+
}
|
|
321
|
+
async function fetchMarkets(net) {
|
|
322
|
+
const { markets, instruments } = await fetchQfexMarketsWithInstruments(net);
|
|
323
|
+
const bySymbol = new Map(instruments.map(i => [i.venueSymbol, i]));
|
|
324
|
+
return markets.map(m => { const i = bySymbol.get(m.venueSymbol); return i === undefined ? m : { ...m, status: qfexListedStatus(i) }; });
|
|
223
325
|
}
|
|
224
326
|
export function buildQfexFundingRates(instruments, contracts, marks, nowMs) {
|
|
225
327
|
const byVenueSymbol = new Map(instruments.map(i => [i.venueSymbol, i]));
|
|
@@ -238,12 +340,16 @@ export function buildQfexFundingRates(instruments, contracts, marks, nowMs) {
|
|
|
238
340
|
}
|
|
239
341
|
const mark = marks.get(c.ticker_id);
|
|
240
342
|
const nextMs = typeof c.next_funding_rate_timestamp === "number" ? c.next_funding_rate_timestamp * 1000 : null;
|
|
343
|
+
// `funding_rate` is the implied rate of the window in progress, labelled by its END (the next
|
|
344
|
+
// top of the hour, = next_funding_rate_timestamp live) — the same window rule as history.
|
|
345
|
+
const marketClosed = qfexFundingWindowClosed(i.marketHours, Math.floor(nowMs / 3_600_000) * 3_600_000 + 3_600_000, Number(c.funding_rate));
|
|
241
346
|
rates.push({
|
|
242
347
|
venue: "qfex",
|
|
243
348
|
venueSymbol: c.ticker_id,
|
|
244
349
|
symbol: i.symbol,
|
|
245
350
|
canonical: canonicalOf(i),
|
|
246
|
-
rate: c.funding_rate,
|
|
351
|
+
rate: marketClosed ? null : c.funding_rate,
|
|
352
|
+
marketClosed,
|
|
247
353
|
basis: QFEX_FUNDING_BASIS,
|
|
248
354
|
markPrice: mark === undefined ? null : mark.price,
|
|
249
355
|
// A timestamp in the past is not a "next" funding time (seen on closed markets).
|
|
@@ -277,7 +277,7 @@ export interface QfexMdsSnapshot {
|
|
|
277
277
|
}>;
|
|
278
278
|
/** Venue symbol → open interest (base units). */
|
|
279
279
|
openInterest: Map<string, string>;
|
|
280
|
-
/** Venue symbol → 24h volume,
|
|
280
|
+
/** Venue symbol → 24h volume, notional in the market's QUOTE currency (KRW for SAMSUNG-KRW — not USD). */
|
|
281
281
|
volumeNotional: Map<string, string>;
|
|
282
282
|
frame: QfexFrame;
|
|
283
283
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { PerpError } from "../../errors.js";
|
|
21
21
|
import type { RisexAgentMeta } from "../../settings.js";
|
|
22
22
|
import type { EvmSigner } from "../../signer/interface.js";
|
|
23
|
-
import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade } from "../interface.js";
|
|
23
|
+
import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade, FundingHistoryRow } from "../interface.js";
|
|
24
24
|
import type { ResolvedSlippage } from "../slippage.js";
|
|
25
25
|
import { type TradingV2 } from "../trading-v2.js";
|
|
26
26
|
import type { VenueCreateContext, VenueDefinition } from "../venue.js";
|
|
@@ -92,16 +92,19 @@ export declare class RisexAdapter implements ExchangeAdapter, TradingV2 {
|
|
|
92
92
|
postOnlyMarket: boolean;
|
|
93
93
|
tradable: boolean;
|
|
94
94
|
}>>;
|
|
95
|
-
|
|
95
|
+
/** `depth` levels per side (API max 250, developer.rise.trade; was always 50). */
|
|
96
|
+
getOrderbook(symbol: string, depth?: number): Promise<{
|
|
96
97
|
bids: [string, string][];
|
|
97
98
|
asks: [string, string][];
|
|
98
99
|
}>;
|
|
99
100
|
getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Settled hourly prints, newest first. The API defaults `start_time` to now − 7 d (168 rows, any
|
|
103
|
+
* `limit`), so the window is sent explicitly (`limit` hours back, one spare) and pages are
|
|
104
|
+
* followed with `page` / `has_next_page` (max 1000 rows per page; developer.rise.trade
|
|
105
|
+
* getfundingratehistory; verified live 2026-10-19).
|
|
106
|
+
*/
|
|
107
|
+
getFundingHistory(symbol: string, limit?: number): Promise<FundingHistoryRow[]>;
|
|
105
108
|
getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
|
|
106
109
|
private portfolio;
|
|
107
110
|
getBalance(): Promise<ExchangeBalance>;
|
|
@@ -145,4 +148,13 @@ export declare class RisexAdapter implements ExchangeAdapter, TradingV2 {
|
|
|
145
148
|
automationGate(): PerpError;
|
|
146
149
|
}
|
|
147
150
|
/** Candle intervals → ms (the venue takes the interval in ns). */
|
|
151
|
+
/**
|
|
152
|
+
* Candle intervals verified live (2026-10-19, market 1): each returns rows labelled with that
|
|
153
|
+
* interval. 3m / 30m / 2h / 8h / 12h / 3d are answered with 1m bars (no error), so they are not
|
|
154
|
+
* listed — getKlines refuses them with INVALID_PARAMS instead of returning 1m candles.
|
|
155
|
+
*/
|
|
156
|
+
export declare const RISEX_BOOK_MAX = 250;
|
|
157
|
+
export declare const RISEX_TRADES_CAP = 1000;
|
|
158
|
+
/** funding-rate-history: max rows per page (docs) and pages followed before giving up. */
|
|
159
|
+
export declare const RISEX_FUNDING_PAGE_MAX = 1000;
|
|
148
160
|
export declare const KLINE_INTERVAL_MS: Readonly<Record<string, number>>;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { PerpError, extractErrorMessage } from "../../errors.js";
|
|
21
21
|
import { quarantineSymbol } from "../../bot/quarantine.js";
|
|
22
|
+
import { assertCandleInterval } from "../interface.js";
|
|
22
23
|
import { alignProtectionPrice, exactUnits, mulDecimal, slippagePrice, subDecimal } from "../precision.js";
|
|
23
24
|
import { newVenueClientOrderId, unsupported, validateLegacyTif, validateOrderRequest, } from "../trading-v2.js";
|
|
24
25
|
import { networkConfig, requireCapabilities, roleUnsupported } from "../venue.js";
|
|
@@ -250,9 +251,13 @@ export class RisexAdapter {
|
|
|
250
251
|
return { ...toLegacyMarketInfo(m), marketId: m.venueSymbol, reduceOnlyMarket: r.reduce_only, postOnlyMarket: r.post_only, tradable: m.tradable };
|
|
251
252
|
});
|
|
252
253
|
}
|
|
253
|
-
|
|
254
|
+
/** `depth` levels per side (API max 250, developer.rise.trade; was always 50). */
|
|
255
|
+
async getOrderbook(symbol, depth = 50) {
|
|
256
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > RISEX_BOOK_MAX) {
|
|
257
|
+
throw new PerpError("INVALID_PARAMS", `RISEx: book depth must be 1..${RISEX_BOOK_MAX} (got ${depth})`);
|
|
258
|
+
}
|
|
254
259
|
const m = await this.market(symbol);
|
|
255
|
-
const d = await this.client.get("/v1/orderbook", { market_id: m.market_id, limit:
|
|
260
|
+
const d = await this.client.get("/v1/orderbook", { market_id: m.market_id, limit: depth });
|
|
256
261
|
const side = (rows, what) => {
|
|
257
262
|
if (!Array.isArray(rows))
|
|
258
263
|
throw new PerpError("EXCHANGE_ERROR", `RISEx /v1/orderbook: no ${what}`, { exchange: "risex" });
|
|
@@ -261,8 +266,12 @@ export class RisexAdapter {
|
|
|
261
266
|
return { bids: side(d.bids, "bids"), asks: side(d.asks, "asks") };
|
|
262
267
|
}
|
|
263
268
|
async getRecentTrades(symbol, limit = 50) {
|
|
269
|
+
// Venue cap (live 2026-10-19: "page_size cannot exceed 1000"); refused, never clamped.
|
|
270
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > RISEX_TRADES_CAP) {
|
|
271
|
+
throw new PerpError("INVALID_PARAMS", `RISEx: trades limit must be 1..${RISEX_TRADES_CAP} (got ${limit})`, { remediation: `RISEx serves at most ${RISEX_TRADES_CAP} trades per request` });
|
|
272
|
+
}
|
|
264
273
|
const m = await this.market(symbol);
|
|
265
|
-
const d = await this.client.get(`/v1/markets/id/${m.market_id}/trade-history`, { limit
|
|
274
|
+
const d = await this.client.get(`/v1/markets/id/${m.market_id}/trade-history`, { limit });
|
|
266
275
|
if (!Array.isArray(d.trades))
|
|
267
276
|
throw new PerpError("EXCHANGE_ERROR", "RISEx trade-history: no trades array", { exchange: "risex" });
|
|
268
277
|
const sym = risexSymbolOf(m);
|
|
@@ -271,19 +280,38 @@ export class RisexAdapter {
|
|
|
271
280
|
if (x.maker_side !== "BUY" && x.maker_side !== "SELL")
|
|
272
281
|
throw new PerpError("EXCHANGE_ERROR", `RISEx trade maker_side ${String(x.maker_side)}`, { exchange: "risex" });
|
|
273
282
|
// The tape side is the taker's: a SELL maker means a buy took it.
|
|
274
|
-
// Public tape
|
|
283
|
+
// Public tape: no fee applies to the viewer → null, never "0".
|
|
275
284
|
return { time: nsToMs(str(x.time, "trade time")), symbol: sym, side: x.maker_side === "SELL" ? "buy" : "sell", price: decReq(x.price, "trade price"), size: decReq(x.size, "trade size"), fee: null };
|
|
276
285
|
});
|
|
277
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Settled hourly prints, newest first. The API defaults `start_time` to now − 7 d (168 rows, any
|
|
289
|
+
* `limit`), so the window is sent explicitly (`limit` hours back, one spare) and pages are
|
|
290
|
+
* followed with `page` / `has_next_page` (max 1000 rows per page; developer.rise.trade
|
|
291
|
+
* getfundingratehistory; verified live 2026-10-19).
|
|
292
|
+
*/
|
|
278
293
|
async getFundingHistory(symbol, limit = 100) {
|
|
294
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
295
|
+
throw new PerpError("INVALID_PARAMS", `RISEx: funding history limit must be a positive integer (got ${limit})`);
|
|
279
296
|
const m = await this.market(symbol);
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
297
|
+
const startNs = (BigInt(Date.now() - (limit + 1) * 3_600_000) * 1000000n).toString();
|
|
298
|
+
const perPage = Math.min(limit, RISEX_FUNDING_PAGE_MAX);
|
|
299
|
+
const out = [];
|
|
300
|
+
for (let page = 1; out.length < limit; page++) {
|
|
301
|
+
if (page > RISEX_FUNDING_MAX_PAGES)
|
|
302
|
+
throw new PerpError("EXCHANGE_ERROR", `RISEx funding-rate-history: still paging after ${RISEX_FUNDING_MAX_PAGES} pages`, { exchange: "risex" });
|
|
303
|
+
const d = await this.client.get(`/v1/markets/id/${m.market_id}/funding-rate-history`, { limit: perPage, page, start_time: startNs });
|
|
304
|
+
if (!Array.isArray(d.records))
|
|
305
|
+
throw new PerpError("EXCHANGE_ERROR", "RISEx funding-rate-history: no records", { exchange: "risex" });
|
|
306
|
+
for (const r of d.records) {
|
|
307
|
+
const x = r;
|
|
308
|
+
out.push({ time: nsToMs(str(x.end_time, "funding end_time")), rate: decReq(x.funding_rate, "funding_rate"), price: positiveOrNull(x.index_price), intervalHours: 1, rateKind: "lastSettled" });
|
|
309
|
+
}
|
|
310
|
+
if (d.has_next_page !== true || d.records.length === 0)
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
const seen = new Set();
|
|
314
|
+
return out.filter(r => (seen.has(r.time) ? false : (seen.add(r.time), true))).sort((a, b) => b.time - a.time).slice(0, limit);
|
|
287
315
|
}
|
|
288
316
|
async getKlines(symbol, interval, startTime, endTime) {
|
|
289
317
|
const ms = KLINE_INTERVAL_MS[interval];
|
|
@@ -300,6 +328,14 @@ export class RisexAdapter {
|
|
|
300
328
|
throw new PerpError("EXCHANGE_ERROR", "RISEx trading-view-data: not an array", { exchange: "risex" });
|
|
301
329
|
return d.map(c => {
|
|
302
330
|
const x = c;
|
|
331
|
+
// The venue answers an interval it does not serve with 1m bars: a row of another interval
|
|
332
|
+
// is never returned as the requested one.
|
|
333
|
+
// OHLC are mapped by field name, unscaled. RISEx opens a bar at the PREVIOUS bar's close
|
|
334
|
+
// while high / low are the bar's own trades, so `open` can lie outside [low, high] (live
|
|
335
|
+
// 2026-10-19: 2555 of 7187 1m bars, 5 of 121 1h bars; daily bars none). That is the venue's
|
|
336
|
+
// data, passed through as published — never clamped or "fixed" (documented in the CHANGELOG).
|
|
337
|
+
// Required on every row here (the field is always sent; null/absent would skip the check).
|
|
338
|
+
assertCandleInterval("risex", interval, x.interval === undefined ? null : x.interval, `Use one of: ${Object.keys(KLINE_INTERVAL_MS).join(", ")}`);
|
|
303
339
|
return {
|
|
304
340
|
time: nsToMs(str(x.time, "candle time")),
|
|
305
341
|
open: decReq(x.open, "open"), high: decReq(x.high, "high"), low: decReq(x.low, "low"), close: decReq(x.close, "close"),
|
|
@@ -791,6 +827,16 @@ function historyStatus(o) {
|
|
|
791
827
|
}
|
|
792
828
|
}
|
|
793
829
|
/** Candle intervals → ms (the venue takes the interval in ns). */
|
|
830
|
+
/**
|
|
831
|
+
* Candle intervals verified live (2026-10-19, market 1): each returns rows labelled with that
|
|
832
|
+
* interval. 3m / 30m / 2h / 8h / 12h / 3d are answered with 1m bars (no error), so they are not
|
|
833
|
+
* listed — getKlines refuses them with INVALID_PARAMS instead of returning 1m candles.
|
|
834
|
+
*/
|
|
835
|
+
export const RISEX_BOOK_MAX = 250;
|
|
836
|
+
export const RISEX_TRADES_CAP = 1000;
|
|
837
|
+
/** funding-rate-history: max rows per page (docs) and pages followed before giving up. */
|
|
838
|
+
export const RISEX_FUNDING_PAGE_MAX = 1000;
|
|
839
|
+
const RISEX_FUNDING_MAX_PAGES = 20;
|
|
794
840
|
export const KLINE_INTERVAL_MS = {
|
|
795
|
-
"1m": 60_000, "5m": 300_000, "15m": 900_000, "
|
|
841
|
+
"1m": 60_000, "5m": 300_000, "15m": 900_000, "1h": 3_600_000, "4h": 14_400_000, "1d": 86_400_000, "1w": 604_800_000,
|
|
796
842
|
};
|
|
@@ -427,5 +427,5 @@ export function matchSpotPerp(spotVenue, spotMarkets, perpVenue, perpMarkets, wa
|
|
|
427
427
|
}
|
|
428
428
|
/** The perp venue's public markets (canonical + asset class + mark) on `net` (default: process network). */
|
|
429
429
|
export function fetchPerpMarketsForMatching(perpVenue, net = sharedApiNetwork()) {
|
|
430
|
-
return getVenue(perpVenue).publicData.fetchMarkets(net);
|
|
430
|
+
return getVenue(perpVenue).publicData.fetchMarkets(net, { funding: false });
|
|
431
431
|
}
|
|
@@ -5,11 +5,22 @@
|
|
|
5
5
|
* readable (the unofficial API refuses it) and every account / trading method refuses with
|
|
6
6
|
* CAPABILITY_UNSUPPORTED before any request — there is no TxFlow account or order code.
|
|
7
7
|
*/
|
|
8
|
-
import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade } from "../interface.js";
|
|
8
|
+
import type { ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade, FundingHistoryRow } from "../interface.js";
|
|
9
9
|
import { type VenueDefinition } from "../venue.js";
|
|
10
10
|
import { type Network, type VenueMarket } from "../venue-types.js";
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Candle intervals that return bars (verified live 2026-10-19, coin 1). 3d / 1w / 1M are
|
|
13
|
+
* accepted by the API but answer [] for every window, so they are not offered.
|
|
14
|
+
*/
|
|
15
|
+
export declare const TXFLOW_KLINE_MS: Readonly<Record<string, number>>;
|
|
12
16
|
export declare const TXFLOW_KLINE_INTERVALS: ReadonlySet<string>;
|
|
17
|
+
/**
|
|
18
|
+
* candleSnapshot answers `200 []` (no error) when the window spans more than ~3000 bars
|
|
19
|
+
* (measured 2026-10-19: 3000 bars OK, 3050 → [] at 1h; same at 1m / 5m / 1d), so ranges are
|
|
20
|
+
* read in windows of at most 2900 bars and merged.
|
|
21
|
+
*/
|
|
22
|
+
export declare const TXFLOW_KLINE_WINDOW_BARS = 2900;
|
|
23
|
+
export declare const TXFLOW_KLINE_MAX_WINDOWS = 50;
|
|
13
24
|
/** Legacy market row plus the labels every TxFlow row carries in JSON output. */
|
|
14
25
|
export type TxflowMarketInfo = ExchangeMarketInfo & {
|
|
15
26
|
venueApi: "unofficial";
|
|
@@ -31,11 +42,7 @@ export declare class TxflowPublicAdapter implements ExchangeAdapter {
|
|
|
31
42
|
asks: [string, string][];
|
|
32
43
|
}>;
|
|
33
44
|
getRecentTrades(symbol: string, limit?: number): Promise<ExchangePublicTrade[]>;
|
|
34
|
-
getFundingHistory(): Promise<
|
|
35
|
-
time: number;
|
|
36
|
-
rate: string;
|
|
37
|
-
price: string | null;
|
|
38
|
-
}[]>;
|
|
45
|
+
getFundingHistory(): Promise<FundingHistoryRow[]>;
|
|
39
46
|
getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
|
|
40
47
|
private refuse;
|
|
41
48
|
getBalance(): Promise<ExchangeBalance>;
|