@pear-protocol/market-sdk 0.0.2 → 0.0.4
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/chart/cache/index.d.ts +1 -0
- package/dist/chart/cache/index.js +6 -0
- package/dist/chart/collector/helpers.js +10 -7
- package/dist/chart/collector/index.js +5 -0
- package/dist/chart/collector/lighter.d.ts +10 -0
- package/dist/chart/collector/lighter.js +42 -0
- package/dist/chart/ws/base-candle.d.ts +3 -2
- package/dist/chart/ws/base-candle.js +4 -0
- package/dist/chart/ws/index.js +3 -0
- package/dist/chart/ws/lighter.d.ts +30 -0
- package/dist/chart/ws/lighter.js +214 -0
- package/dist/orderbook/ws/index.js +3 -0
- package/dist/orderbook/ws/lighter.d.ts +21 -0
- package/dist/orderbook/ws/lighter.js +72 -0
- package/dist/transport/index.js +3 -0
- package/dist/transport/lighter.d.ts +13 -0
- package/dist/transport/lighter.js +16 -0
- package/package.json +2 -2
|
@@ -14,6 +14,7 @@ declare class CandleCache {
|
|
|
14
14
|
hasData(symbol: string, interval: CandleInterval, startTime: number, endTime: number): boolean;
|
|
15
15
|
getData(symbol: string, interval: CandleInterval, startTime: number, endTime: number): CandleData[];
|
|
16
16
|
getEffectiveBoundary(symbols: string[], interval: CandleInterval): number | null;
|
|
17
|
+
getLatestCandle(symbol: string, interval: CandleInterval): CandleData | null;
|
|
17
18
|
removeToken(symbol: string, interval: CandleInterval): void;
|
|
18
19
|
clear(): void;
|
|
19
20
|
}
|
|
@@ -87,6 +87,12 @@ class CandleCache {
|
|
|
87
87
|
}
|
|
88
88
|
return maxBoundary;
|
|
89
89
|
}
|
|
90
|
+
getLatestCandle(symbol, interval) {
|
|
91
|
+
const key = createKey(symbol, interval);
|
|
92
|
+
const tokenData = this.historicalPriceData[key];
|
|
93
|
+
if (!tokenData || tokenData.candles.length === 0) return null;
|
|
94
|
+
return tokenData.candles[tokenData.candles.length - 1] ?? null;
|
|
95
|
+
}
|
|
90
96
|
removeToken(symbol, interval) {
|
|
91
97
|
const key = createKey(symbol, interval);
|
|
92
98
|
delete this.historicalPriceData[key];
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { fetchHistoricalCandles as fetchHistoricalCandles$
|
|
2
|
-
import { fetchHistoricalCandles as fetchHistoricalCandles$
|
|
3
|
-
import { fetchHistoricalCandles as fetchHistoricalCandles$
|
|
4
|
-
import { fetchHistoricalCandles as fetchHistoricalCandles$1 } from './
|
|
1
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$4 } from './binance';
|
|
2
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$3 } from './bybit';
|
|
3
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$5 } from './hyperliquid';
|
|
4
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$1 } from './lighter';
|
|
5
|
+
import { fetchHistoricalCandles as fetchHistoricalCandles$2 } from './okx';
|
|
5
6
|
|
|
6
7
|
async function fetchHistoricalCandles(connector, symbol, startTime, endTime, interval) {
|
|
7
8
|
switch (connector) {
|
|
8
9
|
case "hyperliquid":
|
|
9
|
-
return fetchHistoricalCandles$
|
|
10
|
+
return fetchHistoricalCandles$5(symbol, startTime, endTime, interval);
|
|
10
11
|
case "binance":
|
|
11
|
-
return fetchHistoricalCandles$
|
|
12
|
+
return fetchHistoricalCandles$4(symbol, startTime, endTime, interval);
|
|
12
13
|
case "bybit":
|
|
13
|
-
return fetchHistoricalCandles$
|
|
14
|
+
return fetchHistoricalCandles$3(symbol, startTime, endTime, interval);
|
|
14
15
|
case "okx":
|
|
16
|
+
return fetchHistoricalCandles$2(symbol, startTime, endTime, interval);
|
|
17
|
+
case "lighter":
|
|
15
18
|
return fetchHistoricalCandles$1(symbol, startTime, endTime, interval);
|
|
16
19
|
default:
|
|
17
20
|
throw new Error(`Unsupported connector: ${connector}`);
|
|
@@ -54,6 +54,10 @@ class DataCollector {
|
|
|
54
54
|
});
|
|
55
55
|
this.candleWs.start();
|
|
56
56
|
for (const token of this.getAllTokens()) {
|
|
57
|
+
const latestCandle = this.cache.getLatestCandle(token.symbol, this.candleInterval);
|
|
58
|
+
if (latestCandle) {
|
|
59
|
+
this.candleWs.onHistoricalDataFetched(token.symbol, this.candleInterval, [latestCandle]);
|
|
60
|
+
}
|
|
57
61
|
this.candleWs.subscribe(token.symbol, this.candleInterval);
|
|
58
62
|
}
|
|
59
63
|
}
|
|
@@ -127,6 +131,7 @@ class DataCollector {
|
|
|
127
131
|
interval
|
|
128
132
|
);
|
|
129
133
|
this.cache.addHistoricalPriceData(token.symbol, interval, candles, { start: startTime, end: endTime });
|
|
134
|
+
this.candleWs?.onHistoricalDataFetched(token.symbol, interval, candles);
|
|
130
135
|
return { symbol: token.symbol, candles, success: true };
|
|
131
136
|
} catch (error) {
|
|
132
137
|
console.warn(`Failed to fetch historical data for ${token.symbol}:`, error);
|
|
@@ -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,42 @@
|
|
|
1
|
+
import { getIntervalSeconds } from '../utils';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://mainnet.zklighter.elliot.ai";
|
|
4
|
+
const MAX_LIMIT = 500;
|
|
5
|
+
const RESOLUTION_MAP = {
|
|
6
|
+
"1m": "1m",
|
|
7
|
+
"5m": "5m",
|
|
8
|
+
"15m": "15m",
|
|
9
|
+
"30m": "30m",
|
|
10
|
+
"1h": "1h",
|
|
11
|
+
"4h": "4h",
|
|
12
|
+
"12h": "12h",
|
|
13
|
+
"1d": "1d",
|
|
14
|
+
"1w": "1w"
|
|
15
|
+
};
|
|
16
|
+
async function fetchHistoricalCandles(symbol, startTime, endTime, interval) {
|
|
17
|
+
const resolution = RESOLUTION_MAP[interval];
|
|
18
|
+
if (!resolution) throw new Error(`Lighter does not support the '${interval}' interval`);
|
|
19
|
+
const intervalMs = getIntervalSeconds(interval) * 1e3;
|
|
20
|
+
const countBack = Math.min(Math.ceil((endTime - startTime) / intervalMs) + 1, MAX_LIMIT);
|
|
21
|
+
const params = new URLSearchParams({
|
|
22
|
+
market_id: symbol,
|
|
23
|
+
resolution,
|
|
24
|
+
start_timestamp: String(startTime),
|
|
25
|
+
end_timestamp: String(endTime),
|
|
26
|
+
count_back: String(countBack)
|
|
27
|
+
});
|
|
28
|
+
const response = await fetch(`${BASE_URL}/api/v1/candles?${params}`);
|
|
29
|
+
if (!response.ok) throw new Error(`Lighter API error: ${response.status} ${response.statusText}`);
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
if (data.code !== 200) throw new Error(`Lighter API error: ${data.message ?? String(data.code)}`);
|
|
32
|
+
return data.c.map((candle) => ({
|
|
33
|
+
t: candle.t,
|
|
34
|
+
T: candle.t + intervalMs - 1,
|
|
35
|
+
o: candle.o ?? 0,
|
|
36
|
+
h: candle.h ?? 0,
|
|
37
|
+
l: candle.l ?? 0,
|
|
38
|
+
c: candle.c ?? 0
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { fetchHistoricalCandles };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WsMessage } from '../../shared/types.js';
|
|
2
2
|
import { BaseTransport } from '../../transport/base-transport.js';
|
|
3
|
-
import {
|
|
3
|
+
import { CandleData, CandleInterval } from '../types.js';
|
|
4
4
|
import '@pear-protocol/types';
|
|
5
5
|
import 'partysocket';
|
|
6
6
|
import '../../transport/index.js';
|
|
@@ -8,7 +8,7 @@ import '../../transport/index.js';
|
|
|
8
8
|
type CandleHandler = (symbol: string, candle: CandleData) => void;
|
|
9
9
|
declare abstract class BaseCandleWs {
|
|
10
10
|
protected transport: BaseTransport;
|
|
11
|
-
|
|
11
|
+
protected onCandle: CandleHandler;
|
|
12
12
|
private messageListenerId;
|
|
13
13
|
private openListenerId;
|
|
14
14
|
private subscribedKeys;
|
|
@@ -23,6 +23,7 @@ declare abstract class BaseCandleWs {
|
|
|
23
23
|
stop(): void;
|
|
24
24
|
get connected(): boolean;
|
|
25
25
|
subscribe(symbol: string, interval: CandleInterval): void;
|
|
26
|
+
onHistoricalDataFetched(_symbol: string, _interval: CandleInterval, _candles: CandleData[]): void;
|
|
26
27
|
unsubscribe(symbol: string, interval: CandleInterval): void;
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -58,6 +58,10 @@ class BaseCandleWs {
|
|
|
58
58
|
this.transport.send(this.buildSubscribeMessage(symbol, interval));
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
// Called by DataCollector after historical candles are fetched. Implementations may use this
|
|
62
|
+
// to seed their realtime state so the first WS update builds on the correct open/high/low.
|
|
63
|
+
onHistoricalDataFetched(_symbol, _interval, _candles) {
|
|
64
|
+
}
|
|
61
65
|
unsubscribe(symbol, interval) {
|
|
62
66
|
const key = `${symbol}::${interval}`;
|
|
63
67
|
if (!this.subscribedKeys.has(key)) return;
|
package/dist/chart/ws/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { BaseCandleWs } from './base-candle';
|
|
|
2
2
|
import { BinanceCandleWs } from './binance';
|
|
3
3
|
import { BybitCandleWs } from './bybit';
|
|
4
4
|
import { HyperliquidCandleWs } from './hyperliquid';
|
|
5
|
+
import { LighterCandleWs } from './lighter';
|
|
5
6
|
import { OkxCandleWs } from './okx';
|
|
6
7
|
|
|
7
8
|
function createCandleWs(transport, connector, onCandle) {
|
|
@@ -14,6 +15,8 @@ function createCandleWs(transport, connector, onCandle) {
|
|
|
14
15
|
return new BybitCandleWs(transport, onCandle);
|
|
15
16
|
case "okx":
|
|
16
17
|
return new OkxCandleWs(transport, onCandle);
|
|
18
|
+
case "lighter":
|
|
19
|
+
return new LighterCandleWs(transport, onCandle);
|
|
17
20
|
default: {
|
|
18
21
|
const _exhaustive = connector;
|
|
19
22
|
throw new Error(`Unsupported exchange: ${String(_exhaustive)}`);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { WsMessage } from '../../shared/types.js';
|
|
2
|
+
import { CandleInterval, CandleData } from '../types.js';
|
|
3
|
+
import { BaseCandleWs } from './base-candle.js';
|
|
4
|
+
import '../../transport/index.js';
|
|
5
|
+
import '@pear-protocol/types';
|
|
6
|
+
import '../../transport/base-transport.js';
|
|
7
|
+
import 'partysocket';
|
|
8
|
+
|
|
9
|
+
declare class LighterCandleWs extends BaseCandleWs {
|
|
10
|
+
private symbolIntervals;
|
|
11
|
+
private candleState;
|
|
12
|
+
private ownMessageListenerId;
|
|
13
|
+
private ownOpenListenerId;
|
|
14
|
+
protected buildSubscribeMessage(symbol: string, _interval: CandleInterval): WsMessage;
|
|
15
|
+
protected buildUnsubscribeMessage(symbol: string, _interval: CandleInterval): WsMessage;
|
|
16
|
+
protected parseMessage(_data: string): {
|
|
17
|
+
symbol: string;
|
|
18
|
+
candle: CandleData;
|
|
19
|
+
} | null;
|
|
20
|
+
start(): void;
|
|
21
|
+
stop(): void;
|
|
22
|
+
subscribe(symbol: string, interval: CandleInterval): void;
|
|
23
|
+
unsubscribe(symbol: string, interval: CandleInterval): void;
|
|
24
|
+
onHistoricalDataFetched(symbol: string, interval: CandleInterval, candles: CandleData[]): void;
|
|
25
|
+
private applyCandleState;
|
|
26
|
+
private seedFromHistory;
|
|
27
|
+
private handleTradeMessage;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { LighterCandleWs };
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { getIntervalSeconds } from '../utils';
|
|
2
|
+
import { BaseCandleWs } from './base-candle';
|
|
3
|
+
|
|
4
|
+
const LIGHTER_BASE_URL = "https://mainnet.zklighter.elliot.ai";
|
|
5
|
+
const RESOLUTION_MAP = {
|
|
6
|
+
"1m": "1m",
|
|
7
|
+
"5m": "5m",
|
|
8
|
+
"15m": "15m",
|
|
9
|
+
"30m": "30m",
|
|
10
|
+
"1h": "1h",
|
|
11
|
+
"4h": "4h",
|
|
12
|
+
"12h": "12h",
|
|
13
|
+
"1d": "1d",
|
|
14
|
+
"1w": "1w"
|
|
15
|
+
};
|
|
16
|
+
function toIntervalOpenTime(ts, intervalMs) {
|
|
17
|
+
return Math.floor(ts / intervalMs) * intervalMs;
|
|
18
|
+
}
|
|
19
|
+
class LighterCandleWs extends BaseCandleWs {
|
|
20
|
+
symbolIntervals = /* @__PURE__ */ new Map();
|
|
21
|
+
candleState = /* @__PURE__ */ new Map();
|
|
22
|
+
ownMessageListenerId = null;
|
|
23
|
+
ownOpenListenerId = null;
|
|
24
|
+
buildSubscribeMessage(symbol, _interval) {
|
|
25
|
+
return { type: "subscribe", channel: `trade/${symbol}` };
|
|
26
|
+
}
|
|
27
|
+
buildUnsubscribeMessage(symbol, _interval) {
|
|
28
|
+
return { type: "unsubscribe", channel: `trade/${symbol}` };
|
|
29
|
+
}
|
|
30
|
+
parseMessage(_data) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
start() {
|
|
34
|
+
if (this.ownMessageListenerId) return;
|
|
35
|
+
this.ownMessageListenerId = this.transport.addMessageListener((data) => {
|
|
36
|
+
this.handleTradeMessage(data);
|
|
37
|
+
});
|
|
38
|
+
this.ownOpenListenerId = this.transport.addOpenListener(() => {
|
|
39
|
+
this.candleState.clear();
|
|
40
|
+
for (const [symbol, intervals] of this.symbolIntervals) {
|
|
41
|
+
this.transport.send(this.buildSubscribeMessage(symbol, "1m"));
|
|
42
|
+
for (const interval of intervals) {
|
|
43
|
+
void this.seedFromHistory(symbol, interval);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (!this.transport.connected) {
|
|
48
|
+
this.transport.connect();
|
|
49
|
+
} else {
|
|
50
|
+
for (const symbol of this.symbolIntervals.keys()) {
|
|
51
|
+
this.transport.send(this.buildSubscribeMessage(symbol, "1m"));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
stop() {
|
|
56
|
+
if (this.ownMessageListenerId) {
|
|
57
|
+
this.transport.removeMessageListener(this.ownMessageListenerId);
|
|
58
|
+
this.ownMessageListenerId = null;
|
|
59
|
+
}
|
|
60
|
+
if (this.ownOpenListenerId) {
|
|
61
|
+
this.transport.removeOpenListener(this.ownOpenListenerId);
|
|
62
|
+
this.ownOpenListenerId = null;
|
|
63
|
+
}
|
|
64
|
+
if (this.transport.connected) {
|
|
65
|
+
for (const symbol of this.symbolIntervals.keys()) {
|
|
66
|
+
this.transport.send(this.buildUnsubscribeMessage(symbol, "1m"));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.symbolIntervals.clear();
|
|
70
|
+
this.candleState.clear();
|
|
71
|
+
super.stop();
|
|
72
|
+
}
|
|
73
|
+
subscribe(symbol, interval) {
|
|
74
|
+
let intervals = this.symbolIntervals.get(symbol);
|
|
75
|
+
if (!intervals) {
|
|
76
|
+
intervals = /* @__PURE__ */ new Set();
|
|
77
|
+
this.symbolIntervals.set(symbol, intervals);
|
|
78
|
+
}
|
|
79
|
+
const isFirstForSymbol = intervals.size === 0;
|
|
80
|
+
intervals.add(interval);
|
|
81
|
+
if (isFirstForSymbol && this.transport.connected) {
|
|
82
|
+
this.transport.send(this.buildSubscribeMessage(symbol, interval));
|
|
83
|
+
}
|
|
84
|
+
void this.seedFromHistory(symbol, interval);
|
|
85
|
+
}
|
|
86
|
+
unsubscribe(symbol, interval) {
|
|
87
|
+
const intervals = this.symbolIntervals.get(symbol);
|
|
88
|
+
if (!intervals) return;
|
|
89
|
+
intervals.delete(interval);
|
|
90
|
+
if (intervals.size === 0) {
|
|
91
|
+
this.symbolIntervals.delete(symbol);
|
|
92
|
+
this.candleState.delete(symbol);
|
|
93
|
+
if (this.transport.connected) {
|
|
94
|
+
this.transport.send(this.buildUnsubscribeMessage(symbol, interval));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Called before subscribe() in attachCandleWs so seedFromHistory can skip its fetch when
|
|
99
|
+
// the current period is already covered by historical data.
|
|
100
|
+
onHistoricalDataFetched(symbol, interval, candles) {
|
|
101
|
+
if (candles.length === 0) return;
|
|
102
|
+
const lastCandle = candles[candles.length - 1];
|
|
103
|
+
if (!lastCandle) return;
|
|
104
|
+
const intervalMs = getIntervalSeconds(interval) * 1e3;
|
|
105
|
+
if (lastCandle.t !== toIntervalOpenTime(Date.now(), intervalMs)) return;
|
|
106
|
+
this.applyCandleState(symbol, interval, {
|
|
107
|
+
openTime: lastCandle.t,
|
|
108
|
+
closeTime: lastCandle.T,
|
|
109
|
+
open: lastCandle.o,
|
|
110
|
+
high: lastCandle.h,
|
|
111
|
+
low: lastCandle.l,
|
|
112
|
+
close: lastCandle.c
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
applyCandleState(symbol, interval, state) {
|
|
116
|
+
let symbolState = this.candleState.get(symbol);
|
|
117
|
+
if (!symbolState) {
|
|
118
|
+
symbolState = /* @__PURE__ */ new Map();
|
|
119
|
+
this.candleState.set(symbol, symbolState);
|
|
120
|
+
}
|
|
121
|
+
const existing = symbolState.get(interval);
|
|
122
|
+
if (!existing || existing.openTime !== state.openTime) {
|
|
123
|
+
symbolState.set(interval, state);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async seedFromHistory(symbol, interval) {
|
|
127
|
+
const resolution = RESOLUTION_MAP[interval];
|
|
128
|
+
if (!resolution) return;
|
|
129
|
+
const intervalMs = getIntervalSeconds(interval) * 1e3;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const openTime = toIntervalOpenTime(now, intervalMs);
|
|
132
|
+
if (this.candleState.get(symbol)?.get(interval)?.openTime === openTime) return;
|
|
133
|
+
try {
|
|
134
|
+
const params = new URLSearchParams({
|
|
135
|
+
market_id: symbol,
|
|
136
|
+
resolution,
|
|
137
|
+
start_timestamp: String(openTime),
|
|
138
|
+
end_timestamp: String(now),
|
|
139
|
+
count_back: "1"
|
|
140
|
+
});
|
|
141
|
+
const response = await fetch(`${LIGHTER_BASE_URL}/api/v1/candles?${params}`);
|
|
142
|
+
if (!response.ok) return;
|
|
143
|
+
const data = await response.json();
|
|
144
|
+
if (data.code !== 200 || !data.c?.length) return;
|
|
145
|
+
const candle = data.c[data.c.length - 1];
|
|
146
|
+
if (!this.symbolIntervals.get(symbol)?.has(interval)) return;
|
|
147
|
+
this.applyCandleState(symbol, interval, {
|
|
148
|
+
openTime: candle.t,
|
|
149
|
+
closeTime: candle.t + intervalMs - 1,
|
|
150
|
+
open: candle.o ?? 0,
|
|
151
|
+
high: candle.h ?? 0,
|
|
152
|
+
low: candle.l ?? 0,
|
|
153
|
+
close: candle.c ?? 0
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
handleTradeMessage(data) {
|
|
159
|
+
let msg;
|
|
160
|
+
try {
|
|
161
|
+
msg = JSON.parse(data);
|
|
162
|
+
} catch {
|
|
163
|
+
console.log("Received non-JSON message from Lighter WS, ignoring:", data);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (msg.type !== "update/trade") return;
|
|
167
|
+
if (!Array.isArray(msg.trades) || msg.trades.length === 0) return;
|
|
168
|
+
const colonIdx = msg.channel.indexOf(":");
|
|
169
|
+
if (colonIdx === -1 || !msg.channel.startsWith("trade")) return;
|
|
170
|
+
const symbol = msg.channel.slice(colonIdx + 1);
|
|
171
|
+
const intervals = this.symbolIntervals.get(symbol);
|
|
172
|
+
if (!intervals || intervals.size === 0) return;
|
|
173
|
+
let symbolState = this.candleState.get(symbol);
|
|
174
|
+
if (!symbolState) {
|
|
175
|
+
symbolState = /* @__PURE__ */ new Map();
|
|
176
|
+
this.candleState.set(symbol, symbolState);
|
|
177
|
+
}
|
|
178
|
+
const intervalMsMap = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const interval of intervals) {
|
|
180
|
+
intervalMsMap.set(interval, getIntervalSeconds(interval) * 1e3);
|
|
181
|
+
}
|
|
182
|
+
for (const trade of msg.trades) {
|
|
183
|
+
const price = Number(trade.price);
|
|
184
|
+
const ts = trade.timestamp;
|
|
185
|
+
for (const interval of intervals) {
|
|
186
|
+
const intervalMs = intervalMsMap.get(interval);
|
|
187
|
+
if (intervalMs === void 0) continue;
|
|
188
|
+
const openTime = toIntervalOpenTime(ts, intervalMs);
|
|
189
|
+
const closeTime = openTime + intervalMs - 1;
|
|
190
|
+
const existing = symbolState.get(interval);
|
|
191
|
+
let state;
|
|
192
|
+
if (!existing || existing.openTime !== openTime) {
|
|
193
|
+
state = { openTime, closeTime, open: price, high: price, low: price, close: price };
|
|
194
|
+
symbolState.set(interval, state);
|
|
195
|
+
} else {
|
|
196
|
+
if (price > existing.high) existing.high = price;
|
|
197
|
+
if (price < existing.low) existing.low = price;
|
|
198
|
+
existing.close = price;
|
|
199
|
+
state = existing;
|
|
200
|
+
}
|
|
201
|
+
this.onCandle(symbol, {
|
|
202
|
+
t: state.openTime,
|
|
203
|
+
T: state.closeTime,
|
|
204
|
+
o: state.open,
|
|
205
|
+
h: state.high,
|
|
206
|
+
l: state.low,
|
|
207
|
+
c: state.close
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { LighterCandleWs };
|
|
@@ -2,6 +2,7 @@ export { BaseDepthWs } from './base-depth';
|
|
|
2
2
|
import { BinanceDepthWs } from './binance';
|
|
3
3
|
import { BybitDepthWs } from './bybit';
|
|
4
4
|
import { HyperliquidDepthWs } from './hyperliquid';
|
|
5
|
+
import { LighterDepthWs } from './lighter';
|
|
5
6
|
import { OkxDepthWs } from './okx';
|
|
6
7
|
|
|
7
8
|
function createDepthWs(transport, connector, onUpdate) {
|
|
@@ -14,6 +15,8 @@ function createDepthWs(transport, connector, onUpdate) {
|
|
|
14
15
|
return new BybitDepthWs(transport, onUpdate);
|
|
15
16
|
case "okx":
|
|
16
17
|
return new OkxDepthWs(transport, onUpdate);
|
|
18
|
+
case "lighter":
|
|
19
|
+
return new LighterDepthWs(transport, onUpdate);
|
|
17
20
|
default: {
|
|
18
21
|
const _exhaustive = connector;
|
|
19
22
|
throw new Error(`Unsupported exchange: ${String(_exhaustive)}`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WsMessage } from '../../shared/types.js';
|
|
2
|
+
import { DepthUpdate } from '../types.js';
|
|
3
|
+
import { BaseDepthWs } from './base-depth.js';
|
|
4
|
+
import '../../transport/index.js';
|
|
5
|
+
import '@pear-protocol/types';
|
|
6
|
+
import '../../transport/base-transport.js';
|
|
7
|
+
import 'partysocket';
|
|
8
|
+
|
|
9
|
+
declare class LighterDepthWs extends BaseDepthWs {
|
|
10
|
+
private snapshotReceived;
|
|
11
|
+
private lastNonce;
|
|
12
|
+
private resetListenerId;
|
|
13
|
+
protected buildSubscribeMessage(symbol: string): WsMessage;
|
|
14
|
+
protected buildUnsubscribeMessage(symbol: string): WsMessage;
|
|
15
|
+
start(): void;
|
|
16
|
+
stop(): void;
|
|
17
|
+
protected parseMessage(data: string): DepthUpdate | null;
|
|
18
|
+
private resubscribe;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { LighterDepthWs };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { BaseDepthWs, parseStringLevels } from './base-depth';
|
|
2
|
+
|
|
3
|
+
class LighterDepthWs extends BaseDepthWs {
|
|
4
|
+
snapshotReceived = /* @__PURE__ */ new Map();
|
|
5
|
+
lastNonce = /* @__PURE__ */ new Map();
|
|
6
|
+
resetListenerId = null;
|
|
7
|
+
buildSubscribeMessage(symbol) {
|
|
8
|
+
return { type: "subscribe", channel: `order_book@tier2/${symbol}` };
|
|
9
|
+
}
|
|
10
|
+
buildUnsubscribeMessage(symbol) {
|
|
11
|
+
return { type: "unsubscribe", channel: `order_book@tier2/${symbol}` };
|
|
12
|
+
}
|
|
13
|
+
start() {
|
|
14
|
+
super.start();
|
|
15
|
+
this.resetListenerId = this.transport.addOpenListener(() => {
|
|
16
|
+
this.snapshotReceived.clear();
|
|
17
|
+
this.lastNonce.clear();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
stop() {
|
|
21
|
+
if (this.resetListenerId) {
|
|
22
|
+
this.transport.removeOpenListener(this.resetListenerId);
|
|
23
|
+
this.resetListenerId = null;
|
|
24
|
+
}
|
|
25
|
+
this.snapshotReceived.clear();
|
|
26
|
+
this.lastNonce.clear();
|
|
27
|
+
super.stop();
|
|
28
|
+
}
|
|
29
|
+
parseMessage(data) {
|
|
30
|
+
let msg;
|
|
31
|
+
try {
|
|
32
|
+
msg = JSON.parse(data);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (msg.type === "pong") return null;
|
|
37
|
+
if (msg.type !== "update/order_book" && msg.type !== "subscribed/order_book") return null;
|
|
38
|
+
const colonIdx = msg.channel.indexOf(":");
|
|
39
|
+
if (colonIdx === -1 || !msg.channel.startsWith("order_book")) return null;
|
|
40
|
+
const symbol = msg.channel.slice(colonIdx + 1);
|
|
41
|
+
const book = msg.order_book;
|
|
42
|
+
if (!book || book.code !== 0) return null;
|
|
43
|
+
const isSnapshot = !this.snapshotReceived.get(symbol);
|
|
44
|
+
if (!isSnapshot) {
|
|
45
|
+
const prevNonce = this.lastNonce.get(symbol);
|
|
46
|
+
if (prevNonce !== void 0 && book.begin_nonce !== prevNonce) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`[LighterDepthWs] Sequence break on symbol ${symbol}: expected begin_nonce ${prevNonce}, got ${book.begin_nonce}. Resubscribing.`
|
|
49
|
+
);
|
|
50
|
+
this.resubscribe(symbol);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.snapshotReceived.set(symbol, true);
|
|
55
|
+
this.lastNonce.set(symbol, book.nonce);
|
|
56
|
+
return {
|
|
57
|
+
symbol,
|
|
58
|
+
type: isSnapshot ? "snapshot" : "delta",
|
|
59
|
+
bids: parseStringLevels(book.bids.map((l) => [l.price, l.size])),
|
|
60
|
+
asks: parseStringLevels(book.asks.map((l) => [l.price, l.size])),
|
|
61
|
+
ts: msg.timestamp
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
resubscribe(symbol) {
|
|
65
|
+
this.snapshotReceived.delete(symbol);
|
|
66
|
+
this.lastNonce.delete(symbol);
|
|
67
|
+
this.transport.send(this.buildUnsubscribeMessage(symbol));
|
|
68
|
+
this.transport.send(this.buildSubscribeMessage(symbol));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { LighterDepthWs };
|
package/dist/transport/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { BaseTransport } from './base-transport';
|
|
|
2
2
|
import { BinanceTransport } from './binance';
|
|
3
3
|
import { BybitTransport } from './bybit';
|
|
4
4
|
import { HyperliquidTransport } from './hyperliquid';
|
|
5
|
+
import { LighterTransport } from './lighter';
|
|
5
6
|
import { OkxTransport } from './okx';
|
|
6
7
|
|
|
7
8
|
function CreateTransport(connector) {
|
|
@@ -14,6 +15,8 @@ function CreateTransport(connector) {
|
|
|
14
15
|
return new OkxTransport();
|
|
15
16
|
case "hyperliquid":
|
|
16
17
|
return new HyperliquidTransport();
|
|
18
|
+
case "lighter":
|
|
19
|
+
return new LighterTransport();
|
|
17
20
|
default: {
|
|
18
21
|
const _exhaustive = connector;
|
|
19
22
|
throw new Error(`Unsupported connector: ${String(_exhaustive)}`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Connector } from '@pear-protocol/types';
|
|
2
|
+
import { WsMessage } from '../shared/types.js';
|
|
3
|
+
import { BaseTransport } from './base-transport.js';
|
|
4
|
+
import 'partysocket';
|
|
5
|
+
|
|
6
|
+
declare class LighterTransport extends BaseTransport {
|
|
7
|
+
readonly connector: Connector;
|
|
8
|
+
protected getWsUrl(): string;
|
|
9
|
+
protected getPingIntervalMs(): number;
|
|
10
|
+
protected buildPingMessage(): WsMessage;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { LighterTransport };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseTransport } from './base-transport';
|
|
2
|
+
|
|
3
|
+
class LighterTransport extends BaseTransport {
|
|
4
|
+
connector = "lighter";
|
|
5
|
+
getWsUrl() {
|
|
6
|
+
return "wss://mainnet.zklighter.elliot.ai/stream";
|
|
7
|
+
}
|
|
8
|
+
getPingIntervalMs() {
|
|
9
|
+
return 6e4;
|
|
10
|
+
}
|
|
11
|
+
buildPingMessage() {
|
|
12
|
+
return { type: "ping" };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { LighterTransport };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pear-protocol/market-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
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.
|
|
28
|
+
"@pear-protocol/types": "0.0.15",
|
|
29
29
|
"bignumber.js": "^9.1.2",
|
|
30
30
|
"partysocket": "^1.0.3"
|
|
31
31
|
},
|