@pear-protocol/chart-sdk 0.0.1
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/dist/cache/index.d.ts +17 -0
- package/dist/cache/index.js +101 -0
- package/dist/chart.d.ts +21 -0
- package/dist/chart.js +119 -0
- package/dist/collector/binance.d.ts +6 -0
- package/dist/collector/binance.js +27 -0
- package/dist/collector/bybit.d.ts +6 -0
- package/dist/collector/bybit.js +39 -0
- package/dist/collector/helpers.d.ts +6 -0
- package/dist/collector/helpers.js +21 -0
- package/dist/collector/hyperliquid.d.ts +6 -0
- package/dist/collector/hyperliquid.js +15 -0
- package/dist/collector/index.d.ts +39 -0
- package/dist/collector/index.js +211 -0
- package/dist/collector/okx.d.ts +6 -0
- package/dist/collector/okx.js +38 -0
- package/dist/compute/asset.d.ts +6 -0
- package/dist/compute/asset.js +13 -0
- package/dist/compute/index.d.ts +6 -0
- package/dist/compute/index.js +4 -0
- package/dist/compute/performance.d.ts +7 -0
- package/dist/compute/performance.js +81 -0
- package/dist/compute/price-ratio.d.ts +7 -0
- package/dist/compute/price-ratio.js +107 -0
- package/dist/compute/weighted-ratio.d.ts +7 -0
- package/dist/compute/weighted-ratio.js +105 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +91 -0
- package/dist/ws/base-candle.d.ts +26 -0
- package/dist/ws/base-candle.js +71 -0
- package/dist/ws/base-exchange.d.ts +28 -0
- package/dist/ws/base-exchange.js +93 -0
- package/dist/ws/binance.d.ts +21 -0
- package/dist/ws/binance.js +55 -0
- package/dist/ws/bybit.d.ts +20 -0
- package/dist/ws/bybit.js +75 -0
- package/dist/ws/hyperliquid.d.ts +33 -0
- package/dist/ws/hyperliquid.js +52 -0
- package/dist/ws/index.d.ts +12 -0
- package/dist/ws/index.js +42 -0
- package/dist/ws/okx.d.ts +20 -0
- package/dist/ws/okx.js +72 -0
- package/package.json +35 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CandleInterval, CandleData, HistoricalRange } from '../types.js';
|
|
2
|
+
import '@pear-protocol/types';
|
|
3
|
+
|
|
4
|
+
declare class CandleCache {
|
|
5
|
+
private historicalPriceData;
|
|
6
|
+
private loadingTokens;
|
|
7
|
+
addLoadingToken(symbol: string): void;
|
|
8
|
+
removeLoadingToken(symbol: string): void;
|
|
9
|
+
addHistoricalPriceData(symbol: string, interval: CandleInterval, candles: CandleData[], range: HistoricalRange): void;
|
|
10
|
+
hasData(symbol: string, interval: CandleInterval, startTime: number, endTime: number): boolean;
|
|
11
|
+
getData(symbol: string, interval: CandleInterval, startTime: number, endTime: number): CandleData[];
|
|
12
|
+
getEffectiveBoundary(symbols: string[], interval: CandleInterval): number | null;
|
|
13
|
+
removeToken(symbol: string, interval: CandleInterval): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { CandleCache };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createKey, mergeRanges, getIntervalSeconds } from '../utils';
|
|
2
|
+
|
|
3
|
+
class CandleCache {
|
|
4
|
+
historicalPriceData = {};
|
|
5
|
+
loadingTokens = /* @__PURE__ */ new Set();
|
|
6
|
+
addLoadingToken(symbol) {
|
|
7
|
+
this.loadingTokens.add(symbol);
|
|
8
|
+
}
|
|
9
|
+
removeLoadingToken(symbol) {
|
|
10
|
+
this.loadingTokens.delete(symbol);
|
|
11
|
+
}
|
|
12
|
+
addHistoricalPriceData(symbol, interval, candles, range) {
|
|
13
|
+
const key = createKey(symbol, interval);
|
|
14
|
+
const existing = this.historicalPriceData[key];
|
|
15
|
+
if (!existing) {
|
|
16
|
+
const sortedCandles = [...candles].sort((a, b) => a.t - b.t);
|
|
17
|
+
const noDataBefore2 = sortedCandles.length === 0 ? range.end : null;
|
|
18
|
+
this.historicalPriceData[key] = {
|
|
19
|
+
symbol,
|
|
20
|
+
interval,
|
|
21
|
+
candles: sortedCandles,
|
|
22
|
+
oldestTime: sortedCandles.length > 0 ? sortedCandles[0]?.t ?? null : null,
|
|
23
|
+
latestTime: sortedCandles.length > 0 ? sortedCandles[sortedCandles.length - 1]?.t ?? null : null,
|
|
24
|
+
requestedRanges: [range],
|
|
25
|
+
noDataBefore: noDataBefore2
|
|
26
|
+
};
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const existingTimes = new Set(existing.candles.map((c) => c.t));
|
|
30
|
+
const newCandles = candles.filter((c) => !existingTimes.has(c.t));
|
|
31
|
+
const mergedCandles = [...existing.candles, ...newCandles].sort((a, b) => a.t - b.t);
|
|
32
|
+
const oldestTime = mergedCandles.length > 0 ? mergedCandles[0]?.t ?? null : null;
|
|
33
|
+
const latestTime = mergedCandles.length > 0 ? mergedCandles[mergedCandles.length - 1]?.t ?? null : null;
|
|
34
|
+
const mergedRanges = mergeRanges(existing.requestedRanges, range);
|
|
35
|
+
let noDataBefore = existing.noDataBefore;
|
|
36
|
+
if (candles.length === 0 && existing.oldestTime !== null && range.end <= existing.oldestTime) {
|
|
37
|
+
noDataBefore = existing.oldestTime;
|
|
38
|
+
}
|
|
39
|
+
this.historicalPriceData[key] = {
|
|
40
|
+
...existing,
|
|
41
|
+
candles: mergedCandles,
|
|
42
|
+
oldestTime,
|
|
43
|
+
latestTime,
|
|
44
|
+
requestedRanges: mergedRanges,
|
|
45
|
+
noDataBefore
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
hasData(symbol, interval, startTime, endTime) {
|
|
49
|
+
const key = createKey(symbol, interval);
|
|
50
|
+
const tokenData = this.historicalPriceData[key];
|
|
51
|
+
if (!tokenData) return false;
|
|
52
|
+
if (tokenData.noDataBefore !== null && endTime <= tokenData.noDataBefore) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
for (const range of tokenData.requestedRanges) {
|
|
56
|
+
if (range.start <= startTime && range.end >= endTime) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (tokenData.oldestTime === null || tokenData.latestTime === null) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const intervalMs = getIntervalSeconds(interval) * 1e3;
|
|
64
|
+
const hasStartCoverage = tokenData.oldestTime <= startTime;
|
|
65
|
+
const hasEndCoverage = tokenData.latestTime >= endTime || tokenData.latestTime + intervalMs >= endTime;
|
|
66
|
+
return hasStartCoverage && hasEndCoverage;
|
|
67
|
+
}
|
|
68
|
+
getData(symbol, interval, startTime, endTime) {
|
|
69
|
+
const key = createKey(symbol, interval);
|
|
70
|
+
const tokenData = this.historicalPriceData[key];
|
|
71
|
+
if (!tokenData) return [];
|
|
72
|
+
return tokenData.candles.filter((candle) => candle.t >= startTime && candle.t < endTime);
|
|
73
|
+
}
|
|
74
|
+
getEffectiveBoundary(symbols, interval) {
|
|
75
|
+
if (symbols.length === 0) return null;
|
|
76
|
+
let maxBoundary = null;
|
|
77
|
+
for (const symbol of symbols) {
|
|
78
|
+
const key = createKey(symbol, interval);
|
|
79
|
+
const tokenData = this.historicalPriceData[key];
|
|
80
|
+
if (!tokenData) continue;
|
|
81
|
+
const boundary = tokenData.noDataBefore ?? tokenData.oldestTime;
|
|
82
|
+
if (boundary !== null) {
|
|
83
|
+
if (maxBoundary === null || boundary > maxBoundary) {
|
|
84
|
+
maxBoundary = boundary;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return maxBoundary;
|
|
89
|
+
}
|
|
90
|
+
removeToken(symbol, interval) {
|
|
91
|
+
const key = createKey(symbol, interval);
|
|
92
|
+
delete this.historicalPriceData[key];
|
|
93
|
+
this.loadingTokens.delete(symbol);
|
|
94
|
+
}
|
|
95
|
+
clear() {
|
|
96
|
+
this.historicalPriceData = {};
|
|
97
|
+
this.loadingTokens = /* @__PURE__ */ new Set();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { CandleCache };
|
package/dist/chart.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Connector } from '@pear-protocol/types';
|
|
2
|
+
import { ChartConfig, TokenSelection, CandleInterval, ChartType, Bar, RealtimeBarCallback } from './types.js';
|
|
3
|
+
|
|
4
|
+
declare class Chart {
|
|
5
|
+
private collector;
|
|
6
|
+
private subscriptions;
|
|
7
|
+
private baselinePrices;
|
|
8
|
+
constructor(config?: ChartConfig);
|
|
9
|
+
setTokens(longTokens: TokenSelection[], shortTokens: TokenSelection[]): void;
|
|
10
|
+
setCandleInterval(interval: CandleInterval): void;
|
|
11
|
+
setExchange(connector: Connector): void;
|
|
12
|
+
getBars(chartType: ChartType, startTime: number, endTime: number): Promise<Bar[]>;
|
|
13
|
+
getAssetBars(symbol: string, startTime: number, endTime: number): Promise<Bar[]>;
|
|
14
|
+
getEffectiveDataBoundary(): number | null;
|
|
15
|
+
subscribeRealtimeBars(chartType: ChartType, callback: RealtimeBarCallback): string;
|
|
16
|
+
unsubscribeRealtimeBars(id: string): void;
|
|
17
|
+
clearCache(): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Chart };
|
package/dist/chart.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { DataCollector } from './collector';
|
|
2
|
+
import { computePerformanceCandles, computePriceRatioCandles, computeWeightedRatioCandles, computeAssetBars, computeRealtimePerformanceBar, computeRealtimePriceRatioBar, computeRealtimeWeightedRatioBar } from './compute/index';
|
|
3
|
+
|
|
4
|
+
class Chart {
|
|
5
|
+
collector;
|
|
6
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
7
|
+
baselinePrices = {};
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.collector = new DataCollector(config);
|
|
10
|
+
}
|
|
11
|
+
setTokens(longTokens, shortTokens) {
|
|
12
|
+
this.collector.setTokens(longTokens, shortTokens);
|
|
13
|
+
this.baselinePrices = {};
|
|
14
|
+
}
|
|
15
|
+
setCandleInterval(interval) {
|
|
16
|
+
this.collector.setCandleInterval(interval);
|
|
17
|
+
this.baselinePrices = {};
|
|
18
|
+
}
|
|
19
|
+
setExchange(connector) {
|
|
20
|
+
this.collector.setExchange(connector);
|
|
21
|
+
this.baselinePrices = {};
|
|
22
|
+
}
|
|
23
|
+
async getBars(chartType, startTime, endTime) {
|
|
24
|
+
const interval = this.collector.getCandleInterval();
|
|
25
|
+
const tokenCandles = await this.collector.fetchHistoricalPriceData(startTime, endTime, interval);
|
|
26
|
+
switch (chartType) {
|
|
27
|
+
case "weighted-ratio": {
|
|
28
|
+
return computeWeightedRatioCandles(
|
|
29
|
+
this.collector.getLongTokens(),
|
|
30
|
+
this.collector.getShortTokens(),
|
|
31
|
+
tokenCandles
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
case "price-ratio": {
|
|
35
|
+
return computePriceRatioCandles(this.collector.getLongTokens(), this.collector.getShortTokens(), tokenCandles);
|
|
36
|
+
}
|
|
37
|
+
case "performance": {
|
|
38
|
+
const result = computePerformanceCandles(
|
|
39
|
+
this.collector.getLongTokens(),
|
|
40
|
+
this.collector.getShortTokens(),
|
|
41
|
+
tokenCandles
|
|
42
|
+
);
|
|
43
|
+
if (result.bars.length > 0) {
|
|
44
|
+
this.baselinePrices = result.baselinePrices;
|
|
45
|
+
}
|
|
46
|
+
return result.bars;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async getAssetBars(symbol, startTime, endTime) {
|
|
51
|
+
const interval = this.collector.getCandleInterval();
|
|
52
|
+
const tokenCandles = await this.collector.fetchHistoricalPriceData(startTime, endTime, interval);
|
|
53
|
+
return computeAssetBars(tokenCandles, symbol);
|
|
54
|
+
}
|
|
55
|
+
getEffectiveDataBoundary() {
|
|
56
|
+
return this.collector.getEffectiveDataBoundary(this.collector.getCandleInterval());
|
|
57
|
+
}
|
|
58
|
+
subscribeRealtimeBars(chartType, callback) {
|
|
59
|
+
if (!this.collector.isWsConnected) {
|
|
60
|
+
this.collector.connectWs();
|
|
61
|
+
}
|
|
62
|
+
const rawListenerId = this.collector.addRealtimeListener(() => {
|
|
63
|
+
const snapshot = this.collector.getLatestCandles();
|
|
64
|
+
if (!snapshot) return;
|
|
65
|
+
let bar = null;
|
|
66
|
+
switch (chartType) {
|
|
67
|
+
case "weighted-ratio":
|
|
68
|
+
bar = computeRealtimeWeightedRatioBar(
|
|
69
|
+
this.collector.getLongTokens(),
|
|
70
|
+
this.collector.getShortTokens(),
|
|
71
|
+
snapshot
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
case "price-ratio":
|
|
75
|
+
bar = computeRealtimePriceRatioBar(this.collector.getLongTokens(), this.collector.getShortTokens(), snapshot);
|
|
76
|
+
break;
|
|
77
|
+
case "performance":
|
|
78
|
+
if (Object.keys(this.baselinePrices).length === 0) break;
|
|
79
|
+
bar = computeRealtimePerformanceBar(
|
|
80
|
+
this.collector.getLongTokens(),
|
|
81
|
+
this.collector.getShortTokens(),
|
|
82
|
+
snapshot,
|
|
83
|
+
this.baselinePrices
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (bar) {
|
|
88
|
+
try {
|
|
89
|
+
callback(bar);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const id = Math.random().toString(36).slice(2);
|
|
95
|
+
this.subscriptions.set(id, { chartType, callback, rawListenerId });
|
|
96
|
+
return id;
|
|
97
|
+
}
|
|
98
|
+
unsubscribeRealtimeBars(id) {
|
|
99
|
+
const sub = this.subscriptions.get(id);
|
|
100
|
+
if (!sub) return;
|
|
101
|
+
this.collector.removeRealtimeListener(sub.rawListenerId);
|
|
102
|
+
this.subscriptions.delete(id);
|
|
103
|
+
if (this.subscriptions.size === 0) {
|
|
104
|
+
this.collector.disconnectWs();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
clearCache() {
|
|
108
|
+
this.collector.clearCache();
|
|
109
|
+
this.baselinePrices = {};
|
|
110
|
+
}
|
|
111
|
+
destroy() {
|
|
112
|
+
for (const [id] of this.subscriptions) {
|
|
113
|
+
this.unsubscribeRealtimeBars(id);
|
|
114
|
+
}
|
|
115
|
+
this.collector.destroy();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { Chart };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CandleInterval, CandleData } from '../types.js';
|
|
2
|
+
import '@pear-protocol/types';
|
|
3
|
+
|
|
4
|
+
declare function fetchHistoricalCandles(symbol: string, startTime: number, endTime: number, interval: CandleInterval): Promise<CandleData[]>;
|
|
5
|
+
|
|
6
|
+
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const BINANCE_FUTURES_URL = "https://fapi.binance.com/fapi/v1/klines";
|
|
2
|
+
const MAX_LIMIT = 1e3;
|
|
3
|
+
function toCandleData(kline) {
|
|
4
|
+
return {
|
|
5
|
+
t: kline[0],
|
|
6
|
+
T: kline[6],
|
|
7
|
+
o: Number(kline[1]),
|
|
8
|
+
h: Number(kline[2]),
|
|
9
|
+
l: Number(kline[3]),
|
|
10
|
+
c: Number(kline[4])
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function fetchHistoricalCandles(symbol, startTime, endTime, interval) {
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
symbol,
|
|
16
|
+
interval,
|
|
17
|
+
startTime: String(startTime),
|
|
18
|
+
endTime: String(endTime),
|
|
19
|
+
limit: String(MAX_LIMIT)
|
|
20
|
+
});
|
|
21
|
+
const response = await fetch(`${BINANCE_FUTURES_URL}?${params}`);
|
|
22
|
+
if (!response.ok) throw new Error(`Binance API error: ${response.status} ${response.statusText}`);
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
return data.map(toCandleData);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CandleInterval, CandleData } from '../types.js';
|
|
2
|
+
import '@pear-protocol/types';
|
|
3
|
+
|
|
4
|
+
declare function fetchHistoricalCandles(symbol: string, startTime: number, endTime: number, interval: CandleInterval): Promise<CandleData[]>;
|
|
5
|
+
|
|
6
|
+
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const BYBIT_URL = "https://api.bybit.com/v5/market/kline";
|
|
2
|
+
const MAX_LIMIT = 1e3;
|
|
3
|
+
const intervalMap = {
|
|
4
|
+
"1m": "1",
|
|
5
|
+
"3m": "3",
|
|
6
|
+
"5m": "5",
|
|
7
|
+
"15m": "15",
|
|
8
|
+
"30m": "30",
|
|
9
|
+
"1h": "60",
|
|
10
|
+
"2h": "120",
|
|
11
|
+
"4h": "240",
|
|
12
|
+
"8h": "480",
|
|
13
|
+
"12h": "720",
|
|
14
|
+
"1d": "D",
|
|
15
|
+
"3d": "D",
|
|
16
|
+
"1w": "W",
|
|
17
|
+
"1M": "M"
|
|
18
|
+
};
|
|
19
|
+
function toCandleData(item) {
|
|
20
|
+
const startMs = Number(item[0]);
|
|
21
|
+
return { t: startMs, T: startMs, o: Number(item[1]), h: Number(item[2]), l: Number(item[3]), c: Number(item[4]) };
|
|
22
|
+
}
|
|
23
|
+
async function fetchHistoricalCandles(symbol, startTime, endTime, interval) {
|
|
24
|
+
const params = new URLSearchParams({
|
|
25
|
+
category: "linear",
|
|
26
|
+
symbol,
|
|
27
|
+
interval: intervalMap[interval],
|
|
28
|
+
start: String(startTime),
|
|
29
|
+
end: String(endTime),
|
|
30
|
+
limit: String(MAX_LIMIT)
|
|
31
|
+
});
|
|
32
|
+
const response = await fetch(`${BYBIT_URL}?${params}`);
|
|
33
|
+
if (!response.ok) throw new Error(`Bybit API error: ${response.status} ${response.statusText}`);
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
if (data.retCode !== 0) throw new Error(`Bybit API error: ${data.retMsg}`);
|
|
36
|
+
return data.result.list.reverse().map((item) => toCandleData(item));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Connector } from '@pear-protocol/types';
|
|
2
|
+
import { CandleInterval, CandleData } from '../types.js';
|
|
3
|
+
|
|
4
|
+
declare function fetchHistoricalCandles(connector: Connector, symbol: string, startTime: number, endTime: number, interval: CandleInterval): Promise<CandleData[]>;
|
|
5
|
+
|
|
6
|
+
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$3 } from './binance';
|
|
2
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$2 } from './bybit';
|
|
3
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$4 } from './hyperliquid';
|
|
4
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$1 } from './okx';
|
|
5
|
+
|
|
6
|
+
async function fetchHistoricalCandles(connector, symbol, startTime, endTime, interval) {
|
|
7
|
+
switch (connector) {
|
|
8
|
+
case "hyperliquid":
|
|
9
|
+
return fetchHistoricalCandles$4(symbol, startTime, endTime, interval);
|
|
10
|
+
case "binanceusdm":
|
|
11
|
+
return fetchHistoricalCandles$3(symbol, startTime, endTime, interval);
|
|
12
|
+
case "bybit":
|
|
13
|
+
return fetchHistoricalCandles$2(symbol, startTime, endTime, interval);
|
|
14
|
+
case "okx":
|
|
15
|
+
return fetchHistoricalCandles$1(symbol, startTime, endTime, interval);
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unsupported connector: ${connector}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CandleInterval, CandleData } from '../types.js';
|
|
2
|
+
import '@pear-protocol/types';
|
|
3
|
+
|
|
4
|
+
declare function fetchHistoricalCandles(symbol: string, startTime: number, endTime: number, interval: CandleInterval): Promise<CandleData[]>;
|
|
5
|
+
|
|
6
|
+
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
async function fetchHistoricalCandles(symbol, startTime, endTime, interval) {
|
|
2
|
+
const request = {
|
|
3
|
+
req: { coin: symbol, startTime, endTime, interval },
|
|
4
|
+
type: "candleSnapshot"
|
|
5
|
+
};
|
|
6
|
+
const response = await fetch("https://api.hyperliquid.xyz/info", {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: { "Content-Type": "application/json" },
|
|
9
|
+
body: JSON.stringify(request)
|
|
10
|
+
});
|
|
11
|
+
if (!response.ok) throw new Error(`Hyperliquid API error: ${response.status} ${response.statusText}`);
|
|
12
|
+
return response.json();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Connector } from '@pear-protocol/types';
|
|
2
|
+
import { ChartConfig, TokenSelection, CandleInterval, RealtimeCandleCallback, CandleData } from '../types.js';
|
|
3
|
+
|
|
4
|
+
declare class DataCollector {
|
|
5
|
+
private longTokens;
|
|
6
|
+
private shortTokens;
|
|
7
|
+
private candleInterval;
|
|
8
|
+
private exchange;
|
|
9
|
+
private prevTokenSymbols;
|
|
10
|
+
private prevInterval;
|
|
11
|
+
private exchangeWs;
|
|
12
|
+
private candleWs;
|
|
13
|
+
private latestCandles;
|
|
14
|
+
private realtimeListeners;
|
|
15
|
+
private cache;
|
|
16
|
+
constructor(config?: ChartConfig);
|
|
17
|
+
setTokens(longTokens: TokenSelection[], shortTokens: TokenSelection[]): void;
|
|
18
|
+
setCandleInterval(interval: CandleInterval): void;
|
|
19
|
+
setExchange(connector: Connector): void;
|
|
20
|
+
connectWs(): void;
|
|
21
|
+
disconnectWs(): void;
|
|
22
|
+
get isWsConnected(): boolean;
|
|
23
|
+
addRealtimeListener(cb: RealtimeCandleCallback): string;
|
|
24
|
+
removeRealtimeListener(id: string): void;
|
|
25
|
+
getLatestCandles(): Record<string, CandleData> | null;
|
|
26
|
+
getLongTokens(): TokenSelection[];
|
|
27
|
+
getShortTokens(): TokenSelection[];
|
|
28
|
+
getCandleInterval(): CandleInterval;
|
|
29
|
+
getEffectiveDataBoundary(interval: CandleInterval): number | null;
|
|
30
|
+
clearCache(): void;
|
|
31
|
+
fetchHistoricalPriceData(startTime: number, endTime: number, interval: CandleInterval, callback?: (data: Record<string, CandleData[]>) => void): Promise<Record<string, CandleData[]>>;
|
|
32
|
+
destroy(): void;
|
|
33
|
+
private getAllTokens;
|
|
34
|
+
private syncTokenChanges;
|
|
35
|
+
private getHistoricalPriceData;
|
|
36
|
+
private syncWsSubscriptions;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { DataCollector };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { CandleCache } from '../cache';
|
|
2
|
+
import { createCandleWs } from '../ws';
|
|
3
|
+
import { fetchHistoricalCandles } from './helpers';
|
|
4
|
+
|
|
5
|
+
class DataCollector {
|
|
6
|
+
longTokens;
|
|
7
|
+
shortTokens;
|
|
8
|
+
candleInterval;
|
|
9
|
+
exchange;
|
|
10
|
+
prevTokenSymbols = /* @__PURE__ */ new Set();
|
|
11
|
+
prevInterval = null;
|
|
12
|
+
exchangeWs = null;
|
|
13
|
+
candleWs = null;
|
|
14
|
+
latestCandles = /* @__PURE__ */ new Map();
|
|
15
|
+
realtimeListeners = /* @__PURE__ */ new Map();
|
|
16
|
+
cache = new CandleCache();
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.longTokens = config?.longTokens ?? [];
|
|
19
|
+
this.shortTokens = config?.shortTokens ?? [];
|
|
20
|
+
this.candleInterval = config?.candleInterval ?? "1h";
|
|
21
|
+
this.exchange = config?.connector ?? "hyperliquid";
|
|
22
|
+
this.syncTokenChanges();
|
|
23
|
+
}
|
|
24
|
+
setTokens(longTokens, shortTokens) {
|
|
25
|
+
this.longTokens = longTokens;
|
|
26
|
+
this.shortTokens = shortTokens;
|
|
27
|
+
this.syncTokenChanges();
|
|
28
|
+
this.syncWsSubscriptions();
|
|
29
|
+
}
|
|
30
|
+
setCandleInterval(interval) {
|
|
31
|
+
const prevInterval = this.candleInterval;
|
|
32
|
+
this.candleInterval = interval;
|
|
33
|
+
this.syncTokenChanges();
|
|
34
|
+
if (this.candleWs && prevInterval !== interval) {
|
|
35
|
+
for (const symbol of this.prevTokenSymbols) {
|
|
36
|
+
this.candleWs.unsubscribe(symbol, prevInterval);
|
|
37
|
+
}
|
|
38
|
+
this.latestCandles.clear();
|
|
39
|
+
for (const symbol of this.prevTokenSymbols) {
|
|
40
|
+
this.candleWs.subscribe(symbol, interval);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
setExchange(connector) {
|
|
45
|
+
if (this.exchange === connector) return;
|
|
46
|
+
const wasStreaming = this.candleWs !== null;
|
|
47
|
+
this.disconnectWs();
|
|
48
|
+
this.exchange = connector;
|
|
49
|
+
this.latestCandles.clear();
|
|
50
|
+
this.cache.clear();
|
|
51
|
+
if (wasStreaming) {
|
|
52
|
+
this.connectWs();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
connectWs() {
|
|
56
|
+
if (this.candleWs) return;
|
|
57
|
+
const bundle = createCandleWs(this.exchange, (symbol, candle) => {
|
|
58
|
+
this.latestCandles.set(symbol, candle);
|
|
59
|
+
for (const cb of this.realtimeListeners.values()) {
|
|
60
|
+
try {
|
|
61
|
+
cb(symbol, candle);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
this.exchangeWs = bundle.exchangeWs;
|
|
67
|
+
this.candleWs = bundle.candleWs;
|
|
68
|
+
this.exchangeWs.connect();
|
|
69
|
+
this.candleWs.start();
|
|
70
|
+
for (const token of this.getAllTokens()) {
|
|
71
|
+
this.candleWs.subscribe(token.symbol, this.candleInterval);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
disconnectWs() {
|
|
75
|
+
if (this.candleWs) {
|
|
76
|
+
this.candleWs.stop();
|
|
77
|
+
this.candleWs = null;
|
|
78
|
+
}
|
|
79
|
+
if (this.exchangeWs) {
|
|
80
|
+
this.exchangeWs.disconnect();
|
|
81
|
+
this.exchangeWs = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
get isWsConnected() {
|
|
85
|
+
return this.exchangeWs?.connected ?? false;
|
|
86
|
+
}
|
|
87
|
+
addRealtimeListener(cb) {
|
|
88
|
+
const id = Math.random().toString(36).slice(2);
|
|
89
|
+
this.realtimeListeners.set(id, cb);
|
|
90
|
+
return id;
|
|
91
|
+
}
|
|
92
|
+
removeRealtimeListener(id) {
|
|
93
|
+
this.realtimeListeners.delete(id);
|
|
94
|
+
}
|
|
95
|
+
getLatestCandles() {
|
|
96
|
+
const allTokens = this.getAllTokens();
|
|
97
|
+
if (allTokens.length === 0) return null;
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const token of allTokens) {
|
|
100
|
+
const candle = this.latestCandles.get(token.symbol);
|
|
101
|
+
if (!candle) return null;
|
|
102
|
+
result[token.symbol] = candle;
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
getLongTokens() {
|
|
107
|
+
return this.longTokens;
|
|
108
|
+
}
|
|
109
|
+
getShortTokens() {
|
|
110
|
+
return this.shortTokens;
|
|
111
|
+
}
|
|
112
|
+
getCandleInterval() {
|
|
113
|
+
return this.candleInterval;
|
|
114
|
+
}
|
|
115
|
+
getEffectiveDataBoundary(interval) {
|
|
116
|
+
const symbols = this.getAllTokens().map((t) => t.symbol);
|
|
117
|
+
return this.cache.getEffectiveBoundary(symbols, interval);
|
|
118
|
+
}
|
|
119
|
+
clearCache() {
|
|
120
|
+
this.cache.clear();
|
|
121
|
+
this.latestCandles.clear();
|
|
122
|
+
}
|
|
123
|
+
async fetchHistoricalPriceData(startTime, endTime, interval, callback) {
|
|
124
|
+
const allTokens = this.getAllTokens();
|
|
125
|
+
if (allTokens.length === 0) {
|
|
126
|
+
const emptyResult = {};
|
|
127
|
+
callback?.(emptyResult);
|
|
128
|
+
return emptyResult;
|
|
129
|
+
}
|
|
130
|
+
const tokensToFetch = allTokens.filter((token) => !this.cache.hasData(token.symbol, interval, startTime, endTime));
|
|
131
|
+
if (tokensToFetch.length === 0) {
|
|
132
|
+
const cachedData = this.getHistoricalPriceData(startTime, endTime, interval);
|
|
133
|
+
callback?.(cachedData);
|
|
134
|
+
return cachedData;
|
|
135
|
+
}
|
|
136
|
+
for (const token of tokensToFetch) {
|
|
137
|
+
this.cache.addLoadingToken(token.symbol);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const fetchPromises = tokensToFetch.map(async (token) => {
|
|
141
|
+
try {
|
|
142
|
+
const candles = await fetchHistoricalCandles(this.exchange, token.symbol, startTime, endTime, interval);
|
|
143
|
+
this.cache.addHistoricalPriceData(token.symbol, interval, candles, { start: startTime, end: endTime });
|
|
144
|
+
return { symbol: token.symbol, candles, success: true };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn(`Failed to fetch historical data for ${token.symbol}:`, error);
|
|
147
|
+
return { symbol: token.symbol, candles: [], success: false };
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
await Promise.all(fetchPromises);
|
|
151
|
+
const allData = this.getHistoricalPriceData(startTime, endTime, interval);
|
|
152
|
+
callback?.(allData);
|
|
153
|
+
return allData;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("Failed to fetch historical data:", error);
|
|
156
|
+
throw error;
|
|
157
|
+
} finally {
|
|
158
|
+
for (const token of tokensToFetch) {
|
|
159
|
+
this.cache.removeLoadingToken(token.symbol);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
destroy() {
|
|
164
|
+
this.disconnectWs();
|
|
165
|
+
this.realtimeListeners.clear();
|
|
166
|
+
this.latestCandles.clear();
|
|
167
|
+
this.cache.clear();
|
|
168
|
+
}
|
|
169
|
+
getAllTokens() {
|
|
170
|
+
return [...this.longTokens, ...this.shortTokens];
|
|
171
|
+
}
|
|
172
|
+
syncTokenChanges() {
|
|
173
|
+
const currentSymbols = new Set(this.getAllTokens().map((t) => t.symbol));
|
|
174
|
+
if (this.prevInterval !== null && this.prevInterval !== this.candleInterval) {
|
|
175
|
+
this.cache.clear();
|
|
176
|
+
this.prevTokenSymbols = currentSymbols;
|
|
177
|
+
this.prevInterval = this.candleInterval;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
for (const symbol of this.prevTokenSymbols) {
|
|
181
|
+
if (!currentSymbols.has(symbol)) {
|
|
182
|
+
this.cache.removeToken(symbol, this.candleInterval);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
this.prevTokenSymbols = currentSymbols;
|
|
186
|
+
this.prevInterval = this.candleInterval;
|
|
187
|
+
}
|
|
188
|
+
getHistoricalPriceData(startTime, endTime, interval) {
|
|
189
|
+
const allTokens = this.getAllTokens();
|
|
190
|
+
const result = {};
|
|
191
|
+
for (const token of allTokens) {
|
|
192
|
+
result[token.symbol] = this.cache.getData(token.symbol, interval, startTime, endTime);
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
syncWsSubscriptions() {
|
|
197
|
+
if (!this.candleWs) return;
|
|
198
|
+
const currentSymbols = new Set(this.getAllTokens().map((t) => t.symbol));
|
|
199
|
+
for (const [symbol] of this.latestCandles) {
|
|
200
|
+
if (!currentSymbols.has(symbol)) {
|
|
201
|
+
this.candleWs.unsubscribe(symbol, this.candleInterval);
|
|
202
|
+
this.latestCandles.delete(symbol);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const symbol of currentSymbols) {
|
|
206
|
+
this.candleWs.subscribe(symbol, this.candleInterval);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export { DataCollector };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CandleInterval, CandleData } from '../types.js';
|
|
2
|
+
import '@pear-protocol/types';
|
|
3
|
+
|
|
4
|
+
declare function fetchHistoricalCandles(instId: string, startTime: number, endTime: number, interval: CandleInterval): Promise<CandleData[]>;
|
|
5
|
+
|
|
6
|
+
export { fetchHistoricalCandles };
|