@pear-protocol/market-sdk 0.0.4 → 0.0.6

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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @pear-protocol/market-sdk
2
2
 
3
- SDK for real-time market data across cryptocurrency derivative exchanges. Provides charting with multiple visualization modes (weighted ratio, price ratio, performance) and aggregated order book depth through a unified, exchange-agnostic interface with automatic WebSocket streaming.
3
+ SDK for real-time market data across cryptocurrency derivative exchanges. Provides charting with multiple visualization modes (weighted ratio, price ratio, performance), aggregated order book depth, and historical funding rate analysis through a unified, exchange-agnostic interface.
4
4
 
5
5
  ## Supported Exchanges
6
6
 
@@ -10,6 +10,7 @@ SDK for real-time market data across cryptocurrency derivative exchanges. Provid
10
10
  | Bybit | `bybit` |
11
11
  | Hyperliquid | `hyperliquid` |
12
12
  | OKX | `okx` |
13
+ | Lighter | `lighter` |
13
14
 
14
15
  ## Installation
15
16
 
@@ -20,7 +21,7 @@ npm install @pear-protocol/market-sdk
20
21
  ## Quick Start
21
22
 
22
23
  ```typescript
23
- import { Chart, Orderbook, CreateTransport } from '@pear-protocol/market-sdk';
24
+ import { Chart, FundingRate, Orderbook, CreateTransport } from '@pear-protocol/market-sdk';
24
25
 
25
26
  const transport = CreateTransport('hyperliquid');
26
27
 
@@ -38,6 +39,17 @@ const subId = chart.subscribeRealtimeBars('weighted-ratio', (bar) => {
38
39
  console.log(bar.time, bar.open, bar.high, bar.low, bar.close);
39
40
  });
40
41
 
42
+ // Funding rates
43
+ const fundingRate = new FundingRate({
44
+ transport,
45
+ longTokens: [{ symbol: 'ETH', weight: 0.6 }],
46
+ shortTokens: [{ symbol: 'BTC', weight: 0.4 }],
47
+ interval: '1d',
48
+ });
49
+
50
+ const rates = await fundingRate.getAssetRates('ETH');
51
+ const basketRates = await fundingRate.getBasketRates();
52
+
41
53
  // Order book
42
54
  const orderbook = new Orderbook({
43
55
  transport,
@@ -53,6 +65,7 @@ orderbook.subscribe((snapshot) => {
53
65
  // Cleanup
54
66
  chart.unsubscribeRealtimeBars(subId);
55
67
  chart.destroy();
68
+ fundingRate.destroy();
56
69
  orderbook.destroy();
57
70
  transport.destroy();
58
71
  ```
@@ -299,21 +312,133 @@ orderbook.destroy();
299
312
 
300
313
  The underlying transport is owned by the caller and must be destroyed separately.
301
314
 
302
- ## Transport
315
+ ## Funding Rate
303
316
 
304
- Both `Chart` and `Orderbook` require a `Transport` instance for WebSocket communication. A single transport can be shared across multiple consumers on the same exchange.
317
+ ### Initialization
305
318
 
306
319
  ```typescript
307
- import { CreateTransport } from '@pear-protocol/market-sdk';
320
+ import { FundingRate, CreateTransport } from '@pear-protocol/market-sdk';
308
321
 
309
322
  const transport = CreateTransport('hyperliquid');
310
323
 
311
- // Use with Chart and Orderbook
312
- const chart = new Chart({ transport, /* ... */ });
313
- const orderbook = new Orderbook({ transport, symbol: 'BTC' });
324
+ const fundingRate = new FundingRate({
325
+ transport,
326
+ longTokens: [{ symbol: 'ETH', weight: 0.6 }],
327
+ shortTokens: [{ symbol: 'BTC', weight: 0.4 }],
328
+ });
329
+ ```
330
+
331
+ #### `FundingRateConfig`
332
+
333
+ | Field | Type | Default | Description |
334
+ |-------|------|---------|-------------|
335
+ | `transport` | `Transport` | — | Transport instance (used to determine the exchange) |
336
+ | `longTokens` | `TokenSelection[]` | `[]` | Tokens on the long side with weights |
337
+ | `shortTokens` | `TokenSelection[]` | `[]` | Tokens on the short side with weights |
338
+
339
+ ### Aggregation and Duration
340
+
341
+ Aggregation level and lookback duration are passed per call to `getAssetRates` and `getBasketRates`.
342
+
343
+ #### `FundingRateAggregation`
344
+
345
+ ```typescript
346
+ 'none' | '1d' | '1w' | '1M'
347
+ ```
348
+
349
+ When set to `'none'`, raw funding rates are returned at each exchange's native settlement frequency (1h for Hyperliquid, 8h for Binance/Bybit/OKX). For `'1d'`, `'1w'`, and `'1M'`, rates are summed within each period.
350
+
351
+ #### `FundingRateDuration`
352
+
353
+ ```typescript
354
+ '1w' | '1m' | '1y'
355
+ ```
356
+
357
+ The lookback window for fetching data, ending at the current time.
358
+
359
+ #### Minimum Duration per Aggregation
360
+
361
+ To avoid returning too few data points, each aggregation level enforces a minimum duration. If the requested duration is shorter than the minimum, it is automatically bumped up.
362
+
363
+ | Aggregation | Minimum Duration |
364
+ |-------------|------------------|
365
+ | `'none'` | `'1w'` |
366
+ | `'1d'` | `'1m'` |
367
+ | `'1w'` | `'1y'` |
368
+ | `'1M'` | `'1y'` |
369
+
370
+ ### Per-Asset Funding Rates
371
+
372
+ Fetch funding rates for a single token.
373
+
374
+ ```typescript
375
+ // Defaults: aggregation='none', duration='1m'
376
+ const rates = await fundingRate.getAssetRates('ETH');
377
+
378
+ // Daily aggregation over the last year
379
+ const dailyRates = await fundingRate.getAssetRates('ETH', '1d', '1y');
380
+ ```
381
+
382
+ #### Signature
383
+
384
+ ```typescript
385
+ getAssetRates(
386
+ symbol: string,
387
+ aggregation?: FundingRateAggregation, // default: 'none'
388
+ duration?: FundingRateDuration, // default: '1m'
389
+ ): Promise<FundingRateEntry[]>
390
+ ```
391
+
392
+ ### Basket Funding Rates
393
+
394
+ Compute the weighted net funding rate across all configured tokens. Positive rate = user earns funding, negative = user pays.
395
+
396
+ ```typescript
397
+ const basketRates = await fundingRate.getBasketRates();
398
+
399
+ const monthlyBasket = await fundingRate.getBasketRates('1M', '1y');
400
+ ```
401
+
402
+ The net rate at each time point is calculated as:
314
403
 
315
- // Cleanup — destroy transport after all consumers
316
- chart.destroy();
317
- orderbook.destroy();
318
- transport.destroy();
319
404
  ```
405
+ netRate = sum(shortWeight × rate) - sum(longWeight × rate)
406
+ ```
407
+
408
+ Only time points where all tokens have data are included.
409
+
410
+ #### Signature
411
+
412
+ ```typescript
413
+ getBasketRates(
414
+ aggregation?: FundingRateAggregation, // default: 'none'
415
+ duration?: FundingRateDuration, // default: '1m'
416
+ ): Promise<FundingRateEntry[]>
417
+ ```
418
+
419
+ ### `FundingRateEntry`
420
+
421
+ ```typescript
422
+ {
423
+ time: number; // bucket start timestamp (ms)
424
+ rate: number; // summed rate for the bucket (raw rate when aggregation='none')
425
+ annualizedRate?: number; // rate annualized to a yearly figure
426
+ }
427
+ ```
428
+
429
+ For fixed aggregations the annualization multiplier is `365` (`1d`), `52` (`1w`), or `12` (`1M`). For `'none'`, the multiplier is inferred from the average gap between raw entries (`MS_PER_YEAR / avgGapMs`); if there are fewer than two entries to infer from, `annualizedRate` is omitted.
430
+
431
+ ### Updating Tokens
432
+
433
+ ```typescript
434
+ fundingRate.setTokens(
435
+ [{ symbol: 'ETH', weight: 0.5 }, { symbol: 'SOL', weight: 0.5 }],
436
+ [{ symbol: 'BTC', weight: 1.0 }],
437
+ );
438
+ ```
439
+
440
+ ### Cleanup
441
+
442
+ ```typescript
443
+ fundingRate.destroy();
444
+ ```
@@ -0,0 +1,10 @@
1
+ import { FundingRateEntry } from '../types.js';
2
+ import '../../transport/index.js';
3
+ import '@pear-protocol/types';
4
+ import '../../transport/base-transport.js';
5
+ import 'partysocket';
6
+ import '../../shared/types.js';
7
+
8
+ declare function fetchHistoricalFundingRates(symbol: string, startTime: number, endTime: number): Promise<FundingRateEntry[]>;
9
+
10
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,26 @@
1
+ import { httpGet } from './http-client';
2
+
3
+ const BINANCE_FUTURES_URL = "https://fapi.binance.com/fapi/v1/fundingRate";
4
+ const MAX_LIMIT = 1e3;
5
+ async function fetchHistoricalFundingRates(symbol, startTime, endTime) {
6
+ const allRates = [];
7
+ let cursor = startTime;
8
+ while (cursor < endTime) {
9
+ const data = await httpGet(BINANCE_FUTURES_URL, {
10
+ symbol,
11
+ startTime: String(cursor),
12
+ endTime: String(endTime),
13
+ limit: String(MAX_LIMIT)
14
+ });
15
+ if (data.length === 0) break;
16
+ for (const entry of data) {
17
+ allRates.push({ time: entry.fundingTime, rate: Number(entry.fundingRate) });
18
+ }
19
+ const lastTime = data[data.length - 1]?.fundingTime;
20
+ if (!lastTime || lastTime <= cursor) break;
21
+ cursor = lastTime + 1;
22
+ }
23
+ return allRates.sort((a, b) => a.time - b.time);
24
+ }
25
+
26
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,10 @@
1
+ import { FundingRateEntry } from '../types.js';
2
+ import '../../transport/index.js';
3
+ import '@pear-protocol/types';
4
+ import '../../transport/base-transport.js';
5
+ import 'partysocket';
6
+ import '../../shared/types.js';
7
+
8
+ declare function fetchHistoricalFundingRates(symbol: string, startTime: number, endTime: number): Promise<FundingRateEntry[]>;
9
+
10
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,32 @@
1
+ import { httpGet } from './http-client';
2
+
3
+ const BYBIT_URL = "https://api.bybit.com/v5/market/funding/history";
4
+ const MAX_LIMIT = 200;
5
+ async function fetchHistoricalFundingRates(symbol, startTime, endTime) {
6
+ const allRates = [];
7
+ let cursor = endTime;
8
+ const validate = (d) => d.retCode !== 0 ? `Bybit API error: ${d.retMsg}` : void 0;
9
+ while (cursor > startTime) {
10
+ const data = await httpGet(
11
+ BYBIT_URL,
12
+ {
13
+ category: "linear",
14
+ symbol,
15
+ startTime: String(startTime),
16
+ endTime: String(cursor),
17
+ limit: String(MAX_LIMIT)
18
+ },
19
+ validate
20
+ );
21
+ if (data.result.list.length === 0) break;
22
+ for (const entry of data.result.list) {
23
+ allRates.push({ time: Number(entry.fundingRateTimestamp), rate: Number(entry.fundingRate) });
24
+ }
25
+ const oldestTime = Number(data.result.list[data.result.list.length - 1]?.fundingRateTimestamp);
26
+ if (!oldestTime || oldestTime >= cursor) break;
27
+ cursor = oldestTime - 1;
28
+ }
29
+ return allRates.sort((a, b) => a.time - b.time);
30
+ }
31
+
32
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,5 @@
1
+ type ResponseValidator<T> = (data: T) => string | undefined;
2
+ declare function httpGet<T>(url: string, params: Record<string, string>, validate?: ResponseValidator<T>): Promise<T>;
3
+ declare function httpPost<T>(url: string, body: unknown, validate?: ResponseValidator<T>): Promise<T>;
4
+
5
+ export { type ResponseValidator, httpGet, httpPost };
@@ -0,0 +1,40 @@
1
+ const BASE_DELAY_MS = 1e3;
2
+ const MAX_DELAY_MS = 6e4;
3
+ async function httpGet(url, params, validate) {
4
+ const qs = new URLSearchParams(params).toString();
5
+ return httpFetch(`${url}?${qs}`, void 0, validate);
6
+ }
7
+ async function httpPost(url, body, validate) {
8
+ return httpFetch(
9
+ url,
10
+ {
11
+ method: "POST",
12
+ headers: { "Content-Type": "application/json" },
13
+ body: JSON.stringify(body)
14
+ },
15
+ validate
16
+ );
17
+ }
18
+ async function httpFetch(url, init, validate) {
19
+ let attempt = 0;
20
+ while (true) {
21
+ const response = await fetch(url, init);
22
+ if (response.status === 429) {
23
+ const delay = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
24
+ attempt++;
25
+ await new Promise((resolve) => setTimeout(resolve, delay));
26
+ continue;
27
+ }
28
+ if (!response.ok) {
29
+ throw new Error(`HTTP ${response.status} ${response.statusText} from ${url}`);
30
+ }
31
+ const data = await response.json();
32
+ if (validate) {
33
+ const error = validate(data);
34
+ if (error) throw new Error(error);
35
+ }
36
+ return data;
37
+ }
38
+ }
39
+
40
+ export { httpGet, httpPost };
@@ -0,0 +1,10 @@
1
+ import { FundingRateEntry } from '../types.js';
2
+ import '../../transport/index.js';
3
+ import '@pear-protocol/types';
4
+ import '../../transport/base-transport.js';
5
+ import 'partysocket';
6
+ import '../../shared/types.js';
7
+
8
+ declare function fetchHistoricalFundingRates(symbol: string, startTime: number, endTime: number): Promise<FundingRateEntry[]>;
9
+
10
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,25 @@
1
+ import { httpPost } from './http-client';
2
+
3
+ async function fetchHistoricalFundingRates(symbol, startTime, endTime) {
4
+ const allRates = [];
5
+ let cursor = startTime;
6
+ while (cursor < endTime) {
7
+ const data = await httpPost("https://api.hyperliquid.xyz/info", {
8
+ type: "fundingHistory",
9
+ coin: symbol,
10
+ startTime: cursor,
11
+ endTime
12
+ });
13
+ if (data.length === 0) break;
14
+ for (const entry of data) {
15
+ if (entry.time >= endTime) continue;
16
+ allRates.push({ time: entry.time, rate: Number(entry.fundingRate) });
17
+ }
18
+ const lastTime = data[data.length - 1]?.time;
19
+ if (!lastTime || lastTime <= cursor) break;
20
+ cursor = lastTime + 1;
21
+ }
22
+ return allRates.sort((a, b) => a.time - b.time);
23
+ }
24
+
25
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,10 @@
1
+ import { Connector } from '@pear-protocol/types';
2
+ import { FundingRateDuration, FundingRateEntry } from '../types.js';
3
+ import '../../transport/index.js';
4
+ import '../../transport/base-transport.js';
5
+ import 'partysocket';
6
+ import '../../shared/types.js';
7
+
8
+ declare function fetchHistoricalFundingRates(connector: Connector, symbol: string, startTime: number, endTime: number, duration: FundingRateDuration): Promise<FundingRateEntry[]>;
9
+
10
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,24 @@
1
+ import { fetchHistoricalFundingRates as fetchHistoricalFundingRates$4 } from './binance';
2
+ import { fetchHistoricalFundingRates as fetchHistoricalFundingRates$3 } from './bybit';
3
+ import { fetchHistoricalFundingRates as fetchHistoricalFundingRates$5 } from './hyperliquid';
4
+ import { fetchHistoricalFundingRates as fetchHistoricalFundingRates$1 } from './lighter';
5
+ import { fetchHistoricalFundingRates as fetchHistoricalFundingRates$2 } from './okx';
6
+
7
+ async function fetchHistoricalFundingRates(connector, symbol, startTime, endTime, duration) {
8
+ switch (connector) {
9
+ case "hyperliquid":
10
+ return fetchHistoricalFundingRates$5(symbol, startTime, endTime);
11
+ case "binance":
12
+ return fetchHistoricalFundingRates$4(symbol, startTime, endTime);
13
+ case "bybit":
14
+ return fetchHistoricalFundingRates$3(symbol, startTime, endTime);
15
+ case "okx":
16
+ return fetchHistoricalFundingRates$2(symbol, startTime, endTime);
17
+ case "lighter":
18
+ return fetchHistoricalFundingRates$1(symbol, startTime, endTime, duration);
19
+ default:
20
+ throw new Error(`Unsupported connector: ${connector}`);
21
+ }
22
+ }
23
+
24
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,10 @@
1
+ import { FundingRateDuration, FundingRateEntry } from '../types.js';
2
+ import '../../transport/index.js';
3
+ import '@pear-protocol/types';
4
+ import '../../transport/base-transport.js';
5
+ import 'partysocket';
6
+ import '../../shared/types.js';
7
+
8
+ declare function fetchHistoricalFundingRates(symbol: string, startTime: number, endTime: number, _duration: FundingRateDuration): Promise<FundingRateEntry[]>;
9
+
10
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,35 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { httpGet } from './http-client';
3
+
4
+ const BASE_URL = "https://mainnet.zklighter.elliot.ai";
5
+ const MAX_LIMIT = 500;
6
+ async function fetchHistoricalFundingRates(symbol, startTime, endTime, _duration) {
7
+ const intervalMs = 60 * 60 * 1e3;
8
+ const allRates = [];
9
+ let cursor = startTime;
10
+ while (cursor < endTime) {
11
+ const countBack = Math.min(Math.ceil((endTime - cursor) / intervalMs) + 1, MAX_LIMIT);
12
+ const data = await httpGet(`${BASE_URL}/api/v1/fundings`, {
13
+ market_id: symbol,
14
+ resolution: "1h",
15
+ start_timestamp: String(cursor),
16
+ end_timestamp: String(endTime),
17
+ count_back: String(countBack)
18
+ });
19
+ const entries = data.fundings ?? [];
20
+ if (entries.length === 0) break;
21
+ for (const entry of entries) {
22
+ const timeMs = entry.timestamp * 1e3;
23
+ const rate = entry.rate ? new BigNumber(entry.rate).dividedBy(100).times(entry.direction === "long" ? -1 : 1).toNumber() : 0;
24
+ allRates.push({ time: timeMs, rate });
25
+ }
26
+ const lastTimestamp = entries[entries.length - 1]?.timestamp;
27
+ if (!lastTimestamp) break;
28
+ const lastTimeMs = lastTimestamp * 1e3;
29
+ if (lastTimeMs <= cursor) break;
30
+ cursor = lastTimeMs + 1;
31
+ }
32
+ return allRates.sort((a, b) => a.time - b.time);
33
+ }
34
+
35
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,10 @@
1
+ import { FundingRateEntry } from '../types.js';
2
+ import '../../transport/index.js';
3
+ import '@pear-protocol/types';
4
+ import '../../transport/base-transport.js';
5
+ import 'partysocket';
6
+ import '../../shared/types.js';
7
+
8
+ declare function fetchHistoricalFundingRates(instId: string, startTime: number, endTime: number): Promise<FundingRateEntry[]>;
9
+
10
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,33 @@
1
+ import { httpGet } from './http-client';
2
+
3
+ const OKX_URL = "https://www.okx.com/api/v5/public/funding-rate-history";
4
+ const MAX_LIMIT = 400;
5
+ async function fetchHistoricalFundingRates(instId, startTime, endTime) {
6
+ const allRates = [];
7
+ let cursor = endTime;
8
+ const validate = (d) => d.code !== "0" ? `OKX API error: ${d.msg}` : void 0;
9
+ while (cursor > startTime) {
10
+ const data = await httpGet(
11
+ OKX_URL,
12
+ {
13
+ instId,
14
+ before: String(startTime),
15
+ after: String(cursor),
16
+ limit: String(MAX_LIMIT)
17
+ },
18
+ validate
19
+ );
20
+ if (data.data.length === 0) break;
21
+ let oldestTime = Infinity;
22
+ for (const entry of data.data) {
23
+ const time = Number(entry.fundingTime);
24
+ allRates.push({ time, rate: Number(entry.realizedRate) });
25
+ if (time < oldestTime) oldestTime = time;
26
+ }
27
+ if (oldestTime >= cursor) break;
28
+ cursor = oldestTime - 1;
29
+ }
30
+ return allRates.filter((r) => r.time >= startTime && r.time < endTime).sort((a, b) => a.time - b.time);
31
+ }
32
+
33
+ export { fetchHistoricalFundingRates };
@@ -0,0 +1,28 @@
1
+ import { FundingRateConfig, TokenSelection, FundingRateAggregation, FundingRateDuration, FundingRateEntry } from './types.js';
2
+ import '../transport/index.js';
3
+ import '@pear-protocol/types';
4
+ import '../transport/base-transport.js';
5
+ import 'partysocket';
6
+ import '../shared/types.js';
7
+
8
+ declare class FundingRate {
9
+ private transport;
10
+ private longTokens;
11
+ private shortTokens;
12
+ private cache;
13
+ constructor(config: FundingRateConfig);
14
+ setTokens(longTokens: TokenSelection[], shortTokens: TokenSelection[]): void;
15
+ clearCache(): void;
16
+ destroy(): void;
17
+ getAssetRates(symbol: string, aggregation?: FundingRateAggregation, duration?: FundingRateDuration): Promise<FundingRateEntry[]>;
18
+ getBasketRates(aggregation?: FundingRateAggregation, duration?: FundingRateDuration): Promise<FundingRateEntry[]>;
19
+ private resolveAggregation;
20
+ private loadRates;
21
+ private getUncachedTokens;
22
+ private sliceCache;
23
+ private refetch;
24
+ private getAllTokens;
25
+ private assertTokenExists;
26
+ }
27
+
28
+ export { FundingRate };
@@ -0,0 +1,117 @@
1
+ import { fetchHistoricalFundingRates } from './fetcher';
2
+ import { aggregateRates, annualizeRates, computeBasketRates, computeStartTime } from './utils';
3
+
4
+ const DEFAULT_AGGREGATION = "1d";
5
+ const DEFAULT_DURATION = "1m";
6
+ class FundingRate {
7
+ transport;
8
+ longTokens;
9
+ shortTokens;
10
+ cache = {};
11
+ constructor(config) {
12
+ this.transport = config.transport;
13
+ this.longTokens = config.longTokens ?? [];
14
+ this.shortTokens = config.shortTokens ?? [];
15
+ }
16
+ setTokens(longTokens, shortTokens) {
17
+ const prevSymbols = new Set(this.getAllTokens().map((t) => t.symbol));
18
+ this.longTokens = longTokens;
19
+ this.shortTokens = shortTokens;
20
+ const nextSymbols = new Set(this.getAllTokens().map((t) => t.symbol));
21
+ for (const symbol of Object.keys(this.cache)) {
22
+ if (!nextSymbols.has(symbol)) {
23
+ delete this.cache[symbol];
24
+ }
25
+ }
26
+ for (const symbol of nextSymbols) {
27
+ if (!prevSymbols.has(symbol)) {
28
+ delete this.cache[symbol];
29
+ }
30
+ }
31
+ }
32
+ clearCache() {
33
+ this.cache = {};
34
+ }
35
+ destroy() {
36
+ this.clearCache();
37
+ }
38
+ async getAssetRates(symbol, aggregation = DEFAULT_AGGREGATION, duration = DEFAULT_DURATION) {
39
+ this.assertTokenExists(symbol);
40
+ const resolvedAgg = this.resolveAggregation(aggregation, duration);
41
+ const rawByToken = await this.loadRates(duration);
42
+ const raw = rawByToken[symbol] ?? [];
43
+ const aggregated = aggregateRates(raw, resolvedAgg);
44
+ return annualizeRates(aggregated, resolvedAgg, raw);
45
+ }
46
+ async getBasketRates(aggregation = DEFAULT_AGGREGATION, duration = DEFAULT_DURATION) {
47
+ const resolvedAgg = this.resolveAggregation(aggregation, duration);
48
+ const rawByToken = await this.loadRates(duration);
49
+ const aggregatedByToken = {};
50
+ for (const [symbol, entries] of Object.entries(rawByToken)) {
51
+ aggregatedByToken[symbol] = aggregateRates(entries, resolvedAgg);
52
+ }
53
+ const basket = computeBasketRates(this.longTokens, this.shortTokens, aggregatedByToken);
54
+ const rawSample = Object.values(rawByToken)[0];
55
+ return annualizeRates(basket, resolvedAgg, rawSample);
56
+ }
57
+ resolveAggregation(aggregation, duration) {
58
+ if (duration === "1y" && aggregation === "none") return "1d";
59
+ return aggregation;
60
+ }
61
+ async loadRates(duration) {
62
+ const end = Math.floor(Date.now() / 36e5) * 36e5;
63
+ const start = computeStartTime(end, duration);
64
+ const uncached = this.getUncachedTokens(start, end);
65
+ if (uncached.length > 0) {
66
+ await this.refetch(uncached, start, end, duration);
67
+ }
68
+ return this.sliceCache(start, end);
69
+ }
70
+ getUncachedTokens(start, end) {
71
+ return this.getAllTokens().filter((t) => {
72
+ const cached = this.cache[t.symbol];
73
+ if (!cached) return true;
74
+ return cached.start > start || cached.end < end;
75
+ });
76
+ }
77
+ sliceCache(start, end) {
78
+ const result = {};
79
+ for (const [symbol, cached] of Object.entries(this.cache)) {
80
+ result[symbol] = cached.entries.filter((r) => r.time >= start && r.time <= end);
81
+ }
82
+ return result;
83
+ }
84
+ async refetch(tokens, start, end, duration) {
85
+ if (tokens.length === 0) return;
86
+ await Promise.all(
87
+ tokens.map(async (token) => {
88
+ const existing = this.cache[token.symbol];
89
+ const fetchStart = existing ? Math.min(start, existing.start) : start;
90
+ const fetchEnd = existing ? Math.max(end, existing.end) : end;
91
+ try {
92
+ const rates = await fetchHistoricalFundingRates(
93
+ this.transport.connector,
94
+ token.symbol,
95
+ fetchStart,
96
+ fetchEnd,
97
+ duration
98
+ );
99
+ this.cache[token.symbol] = { entries: rates, start: fetchStart, end: fetchEnd };
100
+ } catch (error) {
101
+ console.warn(`Failed to fetch funding rate data for ${token.symbol}:`, error);
102
+ this.cache[token.symbol] = { entries: [], start: fetchStart, end: fetchEnd };
103
+ }
104
+ })
105
+ );
106
+ }
107
+ getAllTokens() {
108
+ return [...this.longTokens, ...this.shortTokens];
109
+ }
110
+ assertTokenExists(symbol) {
111
+ if (!this.getAllTokens().some((t) => t.symbol === symbol)) {
112
+ throw new Error(`Symbol "${symbol}" is not part of the configured long or short tokens`);
113
+ }
114
+ }
115
+ }
116
+
117
+ export { FundingRate };
@@ -0,0 +1,29 @@
1
+ import { Transport } from '../transport/index.js';
2
+ import '@pear-protocol/types';
3
+ import '../transport/base-transport.js';
4
+ import 'partysocket';
5
+ import '../shared/types.js';
6
+
7
+ interface TokenSelection {
8
+ symbol: string;
9
+ weight: number;
10
+ }
11
+ type FundingRateAggregation = 'none' | '1d' | '1w' | '1M';
12
+ type FundingRateDuration = '1w' | '1m' | '1y';
13
+ interface FundingRateEntry {
14
+ time: number;
15
+ rate: number;
16
+ annualizedRate?: number;
17
+ }
18
+ interface FundingRateCacheEntry {
19
+ entries: FundingRateEntry[];
20
+ start: number;
21
+ end: number;
22
+ }
23
+ interface FundingRateConfig {
24
+ transport: Transport;
25
+ longTokens?: TokenSelection[];
26
+ shortTokens?: TokenSelection[];
27
+ }
28
+
29
+ export type { FundingRateAggregation, FundingRateCacheEntry, FundingRateConfig, FundingRateDuration, FundingRateEntry, TokenSelection };
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,13 @@
1
+ import { FundingRateEntry, FundingRateAggregation, TokenSelection, FundingRateDuration } from './types.js';
2
+ import '../transport/index.js';
3
+ import '@pear-protocol/types';
4
+ import '../transport/base-transport.js';
5
+ import 'partysocket';
6
+ import '../shared/types.js';
7
+
8
+ declare function computeStartTime(endTime: number, duration: FundingRateDuration): number;
9
+ declare function aggregateRates(rawRates: FundingRateEntry[], aggregation: FundingRateAggregation): FundingRateEntry[];
10
+ declare function annualizeRates(entries: FundingRateEntry[], aggregation: FundingRateAggregation, rawEntriesForInference?: FundingRateEntry[]): FundingRateEntry[];
11
+ declare function computeBasketRates(longTokens: TokenSelection[], shortTokens: TokenSelection[], tokenRates: Record<string, FundingRateEntry[]>): FundingRateEntry[];
12
+
13
+ export { aggregateRates, annualizeRates, computeBasketRates, computeStartTime };
@@ -0,0 +1,120 @@
1
+ import BigNumber from 'bignumber.js';
2
+
3
+ function computeStartTime(endTime, duration) {
4
+ const date = new Date(endTime);
5
+ switch (duration) {
6
+ case "1w":
7
+ return endTime - 7 * 864e5;
8
+ case "1m":
9
+ date.setUTCMonth(date.getUTCMonth() - 1);
10
+ return date.getTime();
11
+ case "1y":
12
+ date.setUTCFullYear(date.getUTCFullYear() - 1);
13
+ return date.getTime();
14
+ }
15
+ }
16
+ function getBucketStart(timestamp, aggregation) {
17
+ const date = new Date(timestamp);
18
+ switch (aggregation) {
19
+ case "1d":
20
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
21
+ case "1w": {
22
+ const mondayOffset = (date.getUTCDay() + 6) % 7;
23
+ const monday = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
24
+ monday.setUTCDate(monday.getUTCDate() - mondayOffset);
25
+ return monday.getTime();
26
+ }
27
+ case "1M":
28
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
29
+ }
30
+ }
31
+ function aggregateRates(rawRates, aggregation) {
32
+ if (aggregation === "none") {
33
+ return [...rawRates].sort((a, b) => a.time - b.time);
34
+ }
35
+ const buckets = /* @__PURE__ */ new Map();
36
+ for (const rate of rawRates) {
37
+ const key = getBucketStart(rate.time, aggregation);
38
+ buckets.set(key, (buckets.get(key) ?? new BigNumber(0)).plus(rate.rate));
39
+ }
40
+ return Array.from(buckets.entries()).map(([time, rate]) => ({ time, rate: rate.toNumber() })).sort((a, b) => a.time - b.time);
41
+ }
42
+ function inferAvgIntervalMs(entries) {
43
+ if (entries.length < 2) return null;
44
+ const sorted = [...entries].sort((a, b) => a.time - b.time);
45
+ let totalGap = new BigNumber(0);
46
+ let count = 0;
47
+ for (let i = 1; i < sorted.length; i++) {
48
+ const prev = sorted[i - 1];
49
+ const curr = sorted[i];
50
+ if (prev === void 0 || curr === void 0) continue;
51
+ const gap = curr.time - prev.time;
52
+ if (gap > 0) {
53
+ totalGap = totalGap.plus(gap);
54
+ count++;
55
+ }
56
+ }
57
+ return count > 0 ? totalGap.div(count) : null;
58
+ }
59
+ const MS_PER_YEAR = 365 * 864e5;
60
+ function annualizeRates(entries, aggregation, rawEntriesForInference) {
61
+ let multiplier;
62
+ if (aggregation === "none") {
63
+ const sample = rawEntriesForInference ?? entries;
64
+ const avgMs = inferAvgIntervalMs(sample);
65
+ if (avgMs === null) return entries;
66
+ multiplier = new BigNumber(MS_PER_YEAR).div(avgMs);
67
+ } else {
68
+ const fixed = {
69
+ "1d": 365,
70
+ "1w": 52,
71
+ "1M": 12
72
+ };
73
+ multiplier = new BigNumber(fixed[aggregation]);
74
+ }
75
+ return entries.map((e) => ({
76
+ ...e,
77
+ annualizedRate: new BigNumber(e.rate).times(multiplier).toNumber()
78
+ }));
79
+ }
80
+ function computeBasketRates(longTokens, shortTokens, tokenRates) {
81
+ const allSymbols = [...longTokens, ...shortTokens].map((t) => t.symbol);
82
+ if (allSymbols.length === 0) return [];
83
+ const lookups = {};
84
+ const allTimestamps = /* @__PURE__ */ new Set();
85
+ for (const [symbol, entries] of Object.entries(tokenRates)) {
86
+ const lookup = /* @__PURE__ */ new Map();
87
+ for (const entry of entries) {
88
+ lookup.set(entry.time, entry.rate);
89
+ allTimestamps.add(entry.time);
90
+ }
91
+ lookups[symbol] = lookup;
92
+ }
93
+ const sorted = Array.from(allTimestamps).sort((a, b) => a - b);
94
+ const result = [];
95
+ for (const time of sorted) {
96
+ if (!allSymbols.every((s) => lookups[s]?.has(time))) continue;
97
+ let total = new BigNumber(0);
98
+ let count = 0;
99
+ for (const t of longTokens) {
100
+ const rate = lookups[t.symbol]?.get(time);
101
+ if (rate !== void 0 && t.weight > 0) {
102
+ total = total.plus(new BigNumber(rate).negated().times(new BigNumber(t.weight).div(100)));
103
+ count++;
104
+ }
105
+ }
106
+ for (const t of shortTokens) {
107
+ const rate = lookups[t.symbol]?.get(time);
108
+ if (rate !== void 0 && t.weight > 0) {
109
+ total = total.plus(new BigNumber(rate).times(new BigNumber(t.weight).div(100)));
110
+ count++;
111
+ }
112
+ }
113
+ if (count > 0) {
114
+ result.push({ time, rate: total.toNumber() });
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+
120
+ export { aggregateRates, annualizeRates, computeBasketRates, computeStartTime };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { Chart } from './chart/chart.js';
2
2
  export { Bar, CandleData, CandleInterval, ChartConfig, ChartType, RealtimeBarCallback, RealtimeCandleCallback, TokenSelection } from './chart/types.js';
3
+ export { FundingRate } from './funding-rate/funding-rate.js';
4
+ export { FundingRateAggregation, FundingRateConfig, FundingRateDuration, FundingRateEntry } from './funding-rate/types.js';
3
5
  export { Orderbook } from './orderbook/orderbook.js';
4
6
  export { AggregationConfig, BBO, CexAggregationConfig, HyperliquidAggregationConfig, OrderbookCallback, OrderbookConfig, OrderbookLevel, OrderbookSnapshot } from './orderbook/types.js';
5
7
  export { getAvailableAggregations } from './orderbook/utils.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Chart } from './chart/chart';
2
+ export { FundingRate } from './funding-rate/funding-rate';
2
3
  export { Orderbook } from './orderbook/orderbook';
3
4
  export { getAvailableAggregations } from './orderbook/utils';
4
5
  export { BaseTransport, CreateTransport } from './transport';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/market-sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Pear Protocol Market SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "typecheck": "tsc --noEmit"
26
26
  },
27
27
  "dependencies": {
28
- "@pear-protocol/types": "0.0.15",
28
+ "@pear-protocol/types": "0.0.16",
29
29
  "bignumber.js": "^9.1.2",
30
30
  "partysocket": "^1.0.3"
31
31
  },