@pear-protocol/market-sdk 0.0.1-preview.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.md +319 -0
- package/dist/chart/cache/index.d.ts +21 -0
- package/dist/chart/cache/index.js +101 -0
- package/dist/chart/chart.d.ts +25 -0
- package/dist/chart/chart.js +141 -0
- package/dist/chart/collector/binance.d.ts +10 -0
- package/dist/chart/collector/binance.js +27 -0
- package/dist/chart/collector/bybit.d.ts +10 -0
- package/dist/chart/collector/bybit.js +39 -0
- package/dist/chart/collector/helpers.d.ts +10 -0
- package/dist/chart/collector/helpers.js +21 -0
- package/dist/chart/collector/hyperliquid.d.ts +10 -0
- package/dist/chart/collector/hyperliquid.js +15 -0
- package/dist/chart/collector/index.d.ts +41 -0
- package/dist/chart/collector/index.js +202 -0
- package/dist/chart/collector/okx.d.ts +10 -0
- package/dist/chart/collector/okx.js +38 -0
- package/dist/chart/compute/asset.d.ts +11 -0
- package/dist/chart/compute/asset.js +24 -0
- package/dist/chart/compute/index.d.ts +10 -0
- package/dist/chart/compute/index.js +4 -0
- package/dist/chart/compute/performance.d.ts +11 -0
- package/dist/chart/compute/performance.js +81 -0
- package/dist/chart/compute/price-ratio.d.ts +11 -0
- package/dist/chart/compute/price-ratio.js +107 -0
- package/dist/chart/compute/weighted-ratio.d.ts +11 -0
- package/dist/chart/compute/weighted-ratio.js +109 -0
- package/dist/chart/types.d.ts +55 -0
- package/dist/chart/types.js +1 -0
- package/dist/chart/utils.d.ts +14 -0
- package/dist/chart/utils.js +91 -0
- package/dist/chart/ws/base-candle.d.ts +29 -0
- package/dist/chart/ws/base-candle.js +71 -0
- package/dist/chart/ws/binance.d.ts +19 -0
- package/dist/chart/ws/binance.js +43 -0
- package/dist/chart/ws/bybit.d.ts +18 -0
- package/dist/chart/ws/bybit.js +63 -0
- package/dist/chart/ws/hyperliquid.d.ts +31 -0
- package/dist/chart/ws/hyperliquid.js +40 -0
- package/dist/chart/ws/index.d.ts +11 -0
- package/dist/chart/ws/index.js +24 -0
- package/dist/chart/ws/okx.d.ts +18 -0
- package/dist/chart/ws/okx.js +60 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +4 -0
- package/dist/orderbook/book/aggregate.d.ts +26 -0
- package/dist/orderbook/book/aggregate.js +38 -0
- package/dist/orderbook/book/local-book.d.ts +37 -0
- package/dist/orderbook/book/local-book.js +90 -0
- package/dist/orderbook/orderbook.d.ts +48 -0
- package/dist/orderbook/orderbook.js +111 -0
- package/dist/orderbook/types.d.ts +67 -0
- package/dist/orderbook/types.js +4 -0
- package/dist/orderbook/utils.d.ts +12 -0
- package/dist/orderbook/utils.js +35 -0
- package/dist/orderbook/ws/base-depth.d.ts +41 -0
- package/dist/orderbook/ws/base-depth.js +89 -0
- package/dist/orderbook/ws/binance.d.ts +23 -0
- package/dist/orderbook/ws/binance.js +126 -0
- package/dist/orderbook/ws/bybit.d.ts +15 -0
- package/dist/orderbook/ws/bybit.js +40 -0
- package/dist/orderbook/ws/hyperliquid.d.ts +20 -0
- package/dist/orderbook/ws/hyperliquid.js +87 -0
- package/dist/orderbook/ws/index.d.ts +11 -0
- package/dist/orderbook/ws/index.js +24 -0
- package/dist/orderbook/ws/okx.d.ts +15 -0
- package/dist/orderbook/ws/okx.js +33 -0
- package/dist/shared/types.d.ts +6 -0
- package/dist/shared/types.js +1 -0
- package/dist/transport/base-transport.d.ts +28 -0
- package/dist/transport/base-transport.js +95 -0
- package/dist/transport/binance.d.ts +13 -0
- package/dist/transport/binance.js +16 -0
- package/dist/transport/bybit.d.ts +13 -0
- package/dist/transport/bybit.js +16 -0
- package/dist/transport/hyperliquid.d.ts +13 -0
- package/dist/transport/hyperliquid.js +16 -0
- package/dist/transport/index.d.ts +10 -0
- package/dist/transport/index.js +24 -0
- package/dist/transport/okx.d.ts +13 -0
- package/dist/transport/okx.js +16 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# @pear-protocol/market-sdk
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Supported Exchanges
|
|
6
|
+
|
|
7
|
+
| Exchange | Connector Name |
|
|
8
|
+
|----------|---------------|
|
|
9
|
+
| Binance | `binance` |
|
|
10
|
+
| Bybit | `bybit` |
|
|
11
|
+
| Hyperliquid | `hyperliquid` |
|
|
12
|
+
| OKX | `okx` |
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @pear-protocol/market-sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Chart, Orderbook, CreateTransport } from '@pear-protocol/market-sdk';
|
|
24
|
+
|
|
25
|
+
const transport = CreateTransport('hyperliquid');
|
|
26
|
+
|
|
27
|
+
// Chart
|
|
28
|
+
const chart = new Chart({
|
|
29
|
+
transport,
|
|
30
|
+
longTokens: [{ symbol: 'BTC', weight: 50 }, { symbol: 'ETH', weight: 50 }],
|
|
31
|
+
shortTokens: [{ symbol: 'SOL', weight: 100 }],
|
|
32
|
+
candleInterval: '1h',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const bars = await chart.getBars('weighted-ratio', startTime, endTime);
|
|
36
|
+
|
|
37
|
+
const subId = chart.subscribeRealtimeBars('weighted-ratio', (bar) => {
|
|
38
|
+
console.log(bar.time, bar.open, bar.high, bar.low, bar.close);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Order book
|
|
42
|
+
const orderbook = new Orderbook({
|
|
43
|
+
transport,
|
|
44
|
+
symbol: 'BTC',
|
|
45
|
+
aggregation: 10,
|
|
46
|
+
depth: 15,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
orderbook.subscribe((snapshot) => {
|
|
50
|
+
console.log('bids:', snapshot.bids, 'asks:', snapshot.asks);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Cleanup
|
|
54
|
+
chart.unsubscribeRealtimeBars(subId);
|
|
55
|
+
chart.destroy();
|
|
56
|
+
orderbook.destroy();
|
|
57
|
+
transport.destroy();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Chart
|
|
61
|
+
|
|
62
|
+
### Initialization
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { Chart, CreateTransport } from '@pear-protocol/market-sdk';
|
|
66
|
+
|
|
67
|
+
const transport = CreateTransport('binance');
|
|
68
|
+
|
|
69
|
+
const chart = new Chart({
|
|
70
|
+
transport,
|
|
71
|
+
longTokens: [{ symbol: 'BTC', weight: 60 }, { symbol: 'ETH', weight: 40 }],
|
|
72
|
+
shortTokens: [{ symbol: 'SOL', weight: 100 }],
|
|
73
|
+
candleInterval: '1h',
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### `ChartConfig`
|
|
78
|
+
|
|
79
|
+
| Field | Type | Default | Description |
|
|
80
|
+
|-------|------|---------|-------------|
|
|
81
|
+
| `transport` | `Transport` | — | WebSocket transport instance |
|
|
82
|
+
| `longTokens` | `TokenSelection[]` | `[]` | Tokens on the long side with weights |
|
|
83
|
+
| `shortTokens` | `TokenSelection[]` | `[]` | Tokens on the short side with weights |
|
|
84
|
+
| `candleInterval` | `CandleInterval` | `'1h'` | Candle timeframe |
|
|
85
|
+
|
|
86
|
+
#### `TokenSelection`
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
{ symbol: string; weight: number }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### `CandleInterval`
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
'1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' | '8h' | '12h' | '1d' | '3d' | '1w' | '1M'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Updating Tokens and Interval
|
|
99
|
+
|
|
100
|
+
Change the tokens or candle interval at any time. Both clear cached data and baseline prices.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
chart.setTokens(
|
|
104
|
+
[{ symbol: 'BTC', weight: 100 }],
|
|
105
|
+
[{ symbol: 'ETH', weight: 100 }],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
chart.setCandleInterval('15m');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Fetching Historical Bars
|
|
112
|
+
|
|
113
|
+
Fetch historical OHLC bars for a given chart type and time range.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const bars = await chart.getBars('weighted-ratio', startTime, endTime);
|
|
117
|
+
|
|
118
|
+
for (const bar of bars) {
|
|
119
|
+
bar.time; // timestamp (ms)
|
|
120
|
+
bar.open; // open price
|
|
121
|
+
bar.high; // high price
|
|
122
|
+
bar.low; // low price
|
|
123
|
+
bar.close; // close price
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Chart Types
|
|
128
|
+
|
|
129
|
+
| Type | Description |
|
|
130
|
+
|------|-------------|
|
|
131
|
+
| `'weighted-ratio'` | Geometric mean ratio using token weights as exponents |
|
|
132
|
+
| `'price-ratio'` | Weighted sum of long prices divided by weighted sum of short prices |
|
|
133
|
+
| `'performance'` | Percentage performance relative to the earliest bar in the range |
|
|
134
|
+
|
|
135
|
+
#### Single-Asset Bars
|
|
136
|
+
|
|
137
|
+
Fetch bars for one specific symbol in the configured token set.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const bars = await chart.getAssetBars('BTC', startTime, endTime);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Subscribing to Real-Time Bars
|
|
144
|
+
|
|
145
|
+
Subscribe to live bar updates. The WebSocket connection is established automatically on the first subscription.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const subId = chart.subscribeRealtimeBars('weighted-ratio', (bar) => {
|
|
149
|
+
console.log(bar.time, bar.open, bar.high, bar.low, bar.close);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Single-asset real-time bars
|
|
153
|
+
const assetSubId = chart.subscribeRealtimeAssetBars('ETH', (bar) => {
|
|
154
|
+
console.log(bar.time, bar.close);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Stop receiving updates
|
|
158
|
+
chart.unsubscribeRealtimeBars(subId);
|
|
159
|
+
chart.unsubscribeRealtimeBars(assetSubId);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Data Boundary
|
|
163
|
+
|
|
164
|
+
Get the earliest timestamp for which historical data is available.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const boundary = chart.getEffectiveDataBoundary(); // number | null
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Cache Management
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// Clear all cached historical data and baseline prices
|
|
174
|
+
chart.clearCache();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Cleanup
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
chart.destroy();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Order Book
|
|
184
|
+
|
|
185
|
+
### Initialization
|
|
186
|
+
|
|
187
|
+
The `Orderbook` connects to the exchange WebSocket immediately on construction and begins receiving depth updates.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { Orderbook, CreateTransport } from '@pear-protocol/market-sdk';
|
|
191
|
+
|
|
192
|
+
const transport = CreateTransport('hyperliquid');
|
|
193
|
+
|
|
194
|
+
const orderbook = new Orderbook({
|
|
195
|
+
transport,
|
|
196
|
+
symbol: 'BTC',
|
|
197
|
+
aggregation: 10,
|
|
198
|
+
depth: 15,
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### `OrderbookConfig`
|
|
203
|
+
|
|
204
|
+
| Field | Type | Default | Description |
|
|
205
|
+
|-------|------|---------|-------------|
|
|
206
|
+
| `transport` | `Transport` | — | WebSocket transport instance |
|
|
207
|
+
| `symbol` | `string` | — | Trading pair symbol |
|
|
208
|
+
| `aggregation` | `number` | `0` | Bucket size for price aggregation (0 = no aggregation) |
|
|
209
|
+
| `depth` | `number` | `10` | Number of price levels per side |
|
|
210
|
+
| `snapshottedPrice` | `number` | — | Current mid-price for server-side aggregation params |
|
|
211
|
+
|
|
212
|
+
### Subscribing to Updates
|
|
213
|
+
|
|
214
|
+
Subscribe to real-time order book snapshots. The callback fires on every depth update from the exchange.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
const subId = orderbook.subscribe((snapshot) => {
|
|
218
|
+
snapshot.symbol; // "BTC"
|
|
219
|
+
snapshot.aggregation; // active bucket size
|
|
220
|
+
snapshot.ts; // last update timestamp (ms)
|
|
221
|
+
|
|
222
|
+
for (const bid of snapshot.bids) { // price-descending
|
|
223
|
+
bid.price; // price level
|
|
224
|
+
bid.size; // base-asset quantity
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const ask of snapshot.asks) { // price-ascending
|
|
228
|
+
ask.price;
|
|
229
|
+
ask.size;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Stop receiving updates
|
|
234
|
+
orderbook.unsubscribe(subId);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Reading the Current Snapshot
|
|
238
|
+
|
|
239
|
+
Access the latest order book state without subscribing. Returns `null` if no data has been received yet.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
const snapshot = orderbook.getSnapshot();
|
|
243
|
+
if (snapshot) {
|
|
244
|
+
console.log(snapshot.bids, snapshot.asks);
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Changing Aggregation
|
|
249
|
+
|
|
250
|
+
Change the price aggregation bucket size at any time. Listeners are notified immediately with a re-aggregated snapshot.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
orderbook.setAggregation(100); // aggregate to $100 buckets
|
|
254
|
+
orderbook.setAggregation(0); // disable aggregation
|
|
255
|
+
|
|
256
|
+
const current = orderbook.getAggregation();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Best Bid / Offer (BBO)
|
|
260
|
+
|
|
261
|
+
Access the current best bid, best ask, spread, and spread percentage. Returns `null` values when the book has not loaded yet.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const { bestBid, bestAsk, spread, spreadPct } = orderbook.bbo;
|
|
265
|
+
// bestBid: "65000.5" | null
|
|
266
|
+
// bestAsk: "65001.0" | null
|
|
267
|
+
// spread: "0.5" | null
|
|
268
|
+
// spreadPct: "0.00077" | null
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Available Aggregation Levels
|
|
272
|
+
|
|
273
|
+
Compute recommended aggregation bucket sizes for a given connector and price level.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { getAvailableAggregations } from '@pear-protocol/market-sdk';
|
|
277
|
+
|
|
278
|
+
// CEX exchanges (Binance, Bybit, OKX) — uses tick size
|
|
279
|
+
const levels = getAvailableAggregations('binance', { tickSize: 0.1, midPrice: 65000 });
|
|
280
|
+
// e.g. [0.1, 1, 10, 100, 1000]
|
|
281
|
+
|
|
282
|
+
// Hyperliquid — uses max decimal places
|
|
283
|
+
const levels = getAvailableAggregations('hyperliquid', { maxDecimals: 2, midPrice: 65000 });
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Updating Snapshotted Price
|
|
287
|
+
|
|
288
|
+
Update the mid-price used for server-side aggregation parameters (e.g. Hyperliquid).
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
orderbook.setSnapshottedPrice(65250.5);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Cleanup
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
orderbook.destroy();
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The underlying transport is owned by the caller and must be destroyed separately.
|
|
301
|
+
|
|
302
|
+
## Transport
|
|
303
|
+
|
|
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.
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { CreateTransport } from '@pear-protocol/market-sdk';
|
|
308
|
+
|
|
309
|
+
const transport = CreateTransport('hyperliquid');
|
|
310
|
+
|
|
311
|
+
// Use with Chart and Orderbook
|
|
312
|
+
const chart = new Chart({ transport, /* ... */ });
|
|
313
|
+
const orderbook = new Orderbook({ transport, symbol: 'BTC' });
|
|
314
|
+
|
|
315
|
+
// Cleanup — destroy transport after all consumers
|
|
316
|
+
chart.destroy();
|
|
317
|
+
orderbook.destroy();
|
|
318
|
+
transport.destroy();
|
|
319
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CandleInterval, CandleData, HistoricalRange } 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 CandleCache {
|
|
9
|
+
private historicalPriceData;
|
|
10
|
+
private loadingTokens;
|
|
11
|
+
addLoadingToken(symbol: string): void;
|
|
12
|
+
removeLoadingToken(symbol: string): void;
|
|
13
|
+
addHistoricalPriceData(symbol: string, interval: CandleInterval, candles: CandleData[], range: HistoricalRange): void;
|
|
14
|
+
hasData(symbol: string, interval: CandleInterval, startTime: number, endTime: number): boolean;
|
|
15
|
+
getData(symbol: string, interval: CandleInterval, startTime: number, endTime: number): CandleData[];
|
|
16
|
+
getEffectiveBoundary(symbols: string[], interval: CandleInterval): number | null;
|
|
17
|
+
removeToken(symbol: string, interval: CandleInterval): void;
|
|
18
|
+
clear(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
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 };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ChartConfig, TokenSelection, CandleInterval, ChartType, Bar, RealtimeBarCallback } 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 Chart {
|
|
9
|
+
private collector;
|
|
10
|
+
private baselinePrices;
|
|
11
|
+
constructor(config: ChartConfig);
|
|
12
|
+
setTokens(longTokens: TokenSelection[], shortTokens: TokenSelection[]): void;
|
|
13
|
+
setCandleInterval(interval: CandleInterval): void;
|
|
14
|
+
getBars(chartType: ChartType, startTime: number, endTime: number): Promise<Bar[]>;
|
|
15
|
+
getAssetBars(symbol: string, startTime: number, endTime: number): Promise<Bar[]>;
|
|
16
|
+
getEffectiveDataBoundary(): number | null;
|
|
17
|
+
subscribeRealtimeBars(chartType: ChartType, callback: RealtimeBarCallback): string;
|
|
18
|
+
subscribeRealtimeAssetBars(symbol: string, callback: RealtimeBarCallback): string;
|
|
19
|
+
unsubscribeRealtimeBars(id: string): void;
|
|
20
|
+
private assertTokenExists;
|
|
21
|
+
clearCache(): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { Chart };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DataCollector } from './collector';
|
|
2
|
+
import { computePerformanceCandles, computePriceRatioCandles, computeWeightedRatioCandles, computeAssetBars, computeRealtimePerformanceBar, computeRealtimePriceRatioBar, computeRealtimeWeightedRatioBar, computeRealtimeAssetBar } from './compute/index';
|
|
3
|
+
|
|
4
|
+
class Chart {
|
|
5
|
+
collector;
|
|
6
|
+
baselinePrices = {};
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.collector = new DataCollector(config);
|
|
9
|
+
}
|
|
10
|
+
setTokens(longTokens, shortTokens) {
|
|
11
|
+
this.collector.setTokens(longTokens, shortTokens);
|
|
12
|
+
this.baselinePrices = {};
|
|
13
|
+
}
|
|
14
|
+
setCandleInterval(interval) {
|
|
15
|
+
this.collector.setCandleInterval(interval);
|
|
16
|
+
this.baselinePrices = {};
|
|
17
|
+
}
|
|
18
|
+
async getBars(chartType, startTime, endTime) {
|
|
19
|
+
const interval = this.collector.getCandleInterval();
|
|
20
|
+
const tokenCandles = await this.collector.fetchHistoricalPriceData(startTime, endTime, interval);
|
|
21
|
+
switch (chartType) {
|
|
22
|
+
case "weighted-ratio": {
|
|
23
|
+
return computeWeightedRatioCandles(
|
|
24
|
+
this.collector.getLongTokens(),
|
|
25
|
+
this.collector.getShortTokens(),
|
|
26
|
+
tokenCandles
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
case "price-ratio": {
|
|
30
|
+
return computePriceRatioCandles(this.collector.getLongTokens(), this.collector.getShortTokens(), tokenCandles);
|
|
31
|
+
}
|
|
32
|
+
case "performance": {
|
|
33
|
+
const result = computePerformanceCandles(
|
|
34
|
+
this.collector.getLongTokens(),
|
|
35
|
+
this.collector.getShortTokens(),
|
|
36
|
+
tokenCandles
|
|
37
|
+
);
|
|
38
|
+
if (result.bars.length > 0) {
|
|
39
|
+
this.baselinePrices = result.baselinePrices;
|
|
40
|
+
}
|
|
41
|
+
return result.bars;
|
|
42
|
+
}
|
|
43
|
+
default:
|
|
44
|
+
throw new Error(`Unsupported chart type: ${chartType}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async getAssetBars(symbol, startTime, endTime) {
|
|
48
|
+
this.assertTokenExists(symbol);
|
|
49
|
+
const interval = this.collector.getCandleInterval();
|
|
50
|
+
const tokenCandles = await this.collector.fetchHistoricalPriceData(startTime, endTime, interval);
|
|
51
|
+
return computeAssetBars(tokenCandles, symbol);
|
|
52
|
+
}
|
|
53
|
+
getEffectiveDataBoundary() {
|
|
54
|
+
return this.collector.getEffectiveDataBoundary(this.collector.getCandleInterval());
|
|
55
|
+
}
|
|
56
|
+
subscribeRealtimeBars(chartType, callback) {
|
|
57
|
+
if (!this.collector.isWsAttached) {
|
|
58
|
+
this.collector.attachCandleWs();
|
|
59
|
+
}
|
|
60
|
+
const formatTokens = (tokens) => tokens.map((t) => `${t.symbol}:${t.weight}`).join(",");
|
|
61
|
+
const id = `${chartType}-${formatTokens(this.collector.getLongTokens())}-${formatTokens(this.collector.getShortTokens())}`;
|
|
62
|
+
this.collector.addRealtimeListener(id, () => {
|
|
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 (error) {
|
|
91
|
+
console.warn("Error in realtime bar callback, skipping this update", {
|
|
92
|
+
chartType,
|
|
93
|
+
error
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return id;
|
|
99
|
+
}
|
|
100
|
+
subscribeRealtimeAssetBars(symbol, callback) {
|
|
101
|
+
this.assertTokenExists(symbol);
|
|
102
|
+
if (!this.collector.isWsAttached) {
|
|
103
|
+
this.collector.attachCandleWs();
|
|
104
|
+
}
|
|
105
|
+
const id = `price-${symbol}`;
|
|
106
|
+
this.collector.addRealtimeListener(id, () => {
|
|
107
|
+
const snapshot = this.collector.getLatestCandles();
|
|
108
|
+
if (!snapshot) return;
|
|
109
|
+
const bar = computeRealtimeAssetBar(snapshot, symbol);
|
|
110
|
+
if (bar) {
|
|
111
|
+
try {
|
|
112
|
+
callback(bar);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.warn("Error in realtime asset bar callback, skipping this update", {
|
|
115
|
+
symbol,
|
|
116
|
+
error
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return id;
|
|
122
|
+
}
|
|
123
|
+
unsubscribeRealtimeBars(id) {
|
|
124
|
+
this.collector.removeRealtimeListener(id);
|
|
125
|
+
}
|
|
126
|
+
assertTokenExists(symbol) {
|
|
127
|
+
const allTokens = [...this.collector.getLongTokens(), ...this.collector.getShortTokens()];
|
|
128
|
+
if (!allTokens.some((t) => t.symbol === symbol)) {
|
|
129
|
+
throw new Error(`Symbol "${symbol}" is not part of the configured long or short tokens`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
clearCache() {
|
|
133
|
+
this.collector.clearCache();
|
|
134
|
+
this.baselinePrices = {};
|
|
135
|
+
}
|
|
136
|
+
destroy() {
|
|
137
|
+
this.collector.destroy();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { Chart };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CandleInterval, CandleData } 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 fetchHistoricalCandles(symbol: string, startTime: number, endTime: number, interval: CandleInterval): Promise<CandleData[]>;
|
|
9
|
+
|
|
10
|
+
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,10 @@
|
|
|
1
|
+
import { CandleInterval, CandleData } 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 fetchHistoricalCandles(symbol: string, startTime: number, endTime: number, interval: CandleInterval): Promise<CandleData[]>;
|
|
9
|
+
|
|
10
|
+
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 };
|