@pear-protocol/market-sdk 0.1.0 → 0.2.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.
|
@@ -81,7 +81,7 @@ class CandleCache {
|
|
|
81
81
|
const key = createKey(symbol, interval);
|
|
82
82
|
const tokenData = this.historicalPriceData[key];
|
|
83
83
|
if (!tokenData) return [];
|
|
84
|
-
return tokenData.candles.filter((candle) => candle.
|
|
84
|
+
return tokenData.candles.filter((candle) => candle.T >= startTime && candle.t < endTime);
|
|
85
85
|
}
|
|
86
86
|
getEffectiveBoundary(symbols, interval) {
|
|
87
87
|
if (symbols.length === 0) return null;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getIntervalSeconds } from '../utils';
|
|
2
|
+
import { getWeekStart, aggregateDailyCandlesToWeekly } from '../weekly';
|
|
2
3
|
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
6
|
const BASE_URL = "https://mainnet.zklighter.elliot.ai";
|
|
6
7
|
const MAX_LIMIT = 500;
|
|
8
|
+
const DAY_MS = getIntervalSeconds("1d") * 1e3;
|
|
7
9
|
const RESOLUTION_MAP = {
|
|
8
10
|
"1m": "1m",
|
|
9
11
|
"5m": "5m",
|
|
@@ -15,6 +17,24 @@ const RESOLUTION_MAP = {
|
|
|
15
17
|
"1d": "1d"
|
|
16
18
|
};
|
|
17
19
|
async function fetchHistoricalCandles(symbol, startTime, endTime, interval, authToken) {
|
|
20
|
+
if (interval === "1w") {
|
|
21
|
+
const dailyCandles = await fetchDailyCandlesChunked(symbol, getWeekStart(startTime), endTime, authToken);
|
|
22
|
+
return aggregateDailyCandlesToWeekly(dailyCandles);
|
|
23
|
+
}
|
|
24
|
+
return fetchResolutionCandles(symbol, startTime, endTime, interval, authToken);
|
|
25
|
+
}
|
|
26
|
+
__name(fetchHistoricalCandles, "fetchHistoricalCandles");
|
|
27
|
+
async function fetchDailyCandlesChunked(symbol, startTime, endTime, authToken) {
|
|
28
|
+
const chunkMs = MAX_LIMIT * DAY_MS;
|
|
29
|
+
const chunkStarts = [];
|
|
30
|
+
for (let chunkStart = startTime; chunkStart < endTime; chunkStart += chunkMs) {
|
|
31
|
+
chunkStarts.push(chunkStart);
|
|
32
|
+
}
|
|
33
|
+
const chunks = await Promise.all(chunkStarts.map((chunkStart) => fetchResolutionCandles(symbol, chunkStart, Math.min(chunkStart + chunkMs, endTime), "1d", authToken)));
|
|
34
|
+
return chunks.flat();
|
|
35
|
+
}
|
|
36
|
+
__name(fetchDailyCandlesChunked, "fetchDailyCandlesChunked");
|
|
37
|
+
async function fetchResolutionCandles(symbol, startTime, endTime, interval, authToken) {
|
|
18
38
|
const resolution = RESOLUTION_MAP[interval];
|
|
19
39
|
if (!resolution) throw new Error(`Lighter does not support the '${interval}' interval`);
|
|
20
40
|
const intervalMs = getIntervalSeconds(interval) * 1e3;
|
|
@@ -43,6 +63,6 @@ async function fetchHistoricalCandles(symbol, startTime, endTime, interval, auth
|
|
|
43
63
|
c: candle.c ?? 0
|
|
44
64
|
}));
|
|
45
65
|
}
|
|
46
|
-
__name(
|
|
66
|
+
__name(fetchResolutionCandles, "fetchResolutionCandles");
|
|
47
67
|
|
|
48
68
|
export { fetchHistoricalCandles };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CandleData } from './types.js';
|
|
2
|
+
import '../transport/index.js';
|
|
3
|
+
import '@pear-protocol/types';
|
|
4
|
+
import '../shared/types.js';
|
|
5
|
+
import '../transport/base-transport.js';
|
|
6
|
+
import 'partysocket';
|
|
7
|
+
|
|
8
|
+
/** Start (ms epoch, UTC) of the Monday-aligned week containing `timestamp`. */
|
|
9
|
+
declare function getWeekStart(timestamp: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Folds one daily candle into its week's running OHLC bar. Extends `existing` when it already
|
|
12
|
+
* covers the same week (open stays, high/low/close roll forward); otherwise starts a fresh bar.
|
|
13
|
+
* Shared by the REST batch aggregator below and LighterCandleWs's live incremental merge, so
|
|
14
|
+
* historical and realtime weekly candles are always built the same way.
|
|
15
|
+
*/
|
|
16
|
+
declare function foldDayIntoWeek(existing: CandleData | undefined, day: CandleData): CandleData;
|
|
17
|
+
/** Reduces a batch of daily candles into one OHLC candle per UTC week, oldest first. */
|
|
18
|
+
declare function aggregateDailyCandlesToWeekly(dailyCandles: CandleData[]): CandleData[];
|
|
19
|
+
|
|
20
|
+
export { aggregateDailyCandlesToWeekly, foldDayIntoWeek, getWeekStart };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getIntervalSeconds } from './utils';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
|
+
const WEEK_MS = getIntervalSeconds("1w") * 1e3;
|
|
6
|
+
function getWeekStart(timestamp) {
|
|
7
|
+
const date = new Date(timestamp);
|
|
8
|
+
const mondayOffset = (date.getUTCDay() + 6) % 7;
|
|
9
|
+
const monday = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
10
|
+
monday.setUTCDate(monday.getUTCDate() - mondayOffset);
|
|
11
|
+
return monday.getTime();
|
|
12
|
+
}
|
|
13
|
+
__name(getWeekStart, "getWeekStart");
|
|
14
|
+
function foldDayIntoWeek(existing, day) {
|
|
15
|
+
const weekStart = getWeekStart(day.t);
|
|
16
|
+
if (existing && existing.t === weekStart) {
|
|
17
|
+
return {
|
|
18
|
+
...existing,
|
|
19
|
+
h: Math.max(existing.h, day.h),
|
|
20
|
+
l: Math.min(existing.l, day.l),
|
|
21
|
+
c: day.c
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
t: weekStart,
|
|
26
|
+
T: weekStart + WEEK_MS - 1,
|
|
27
|
+
o: day.o,
|
|
28
|
+
h: day.h,
|
|
29
|
+
l: day.l,
|
|
30
|
+
c: day.c
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
__name(foldDayIntoWeek, "foldDayIntoWeek");
|
|
34
|
+
function aggregateDailyCandlesToWeekly(dailyCandles) {
|
|
35
|
+
const weeklyByStart = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const day of [
|
|
37
|
+
...dailyCandles
|
|
38
|
+
].sort((a, b) => a.t - b.t)) {
|
|
39
|
+
const folded = foldDayIntoWeek(weeklyByStart.get(getWeekStart(day.t)), day);
|
|
40
|
+
weeklyByStart.set(folded.t, folded);
|
|
41
|
+
}
|
|
42
|
+
return Array.from(weeklyByStart.values()).sort((a, b) => a.t - b.t);
|
|
43
|
+
}
|
|
44
|
+
__name(aggregateDailyCandlesToWeekly, "aggregateDailyCandlesToWeekly");
|
|
45
|
+
|
|
46
|
+
export { aggregateDailyCandlesToWeekly, foldDayIntoWeek, getWeekStart };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WsMessage } from '../../shared/types.js';
|
|
2
|
-
import { CandleInterval } from '../types.js';
|
|
2
|
+
import { CandleInterval, CandleData } from '../types.js';
|
|
3
3
|
import { BaseCandleWs, ParsedCandle } from './base-candle.js';
|
|
4
4
|
import '../../transport/index.js';
|
|
5
5
|
import '@pear-protocol/types';
|
|
@@ -11,13 +11,29 @@ import 'partysocket';
|
|
|
11
11
|
* them onto CandleData — it never aggregates the trade tape and never seeds itself over REST, because
|
|
12
12
|
* the subscribe acknowledgement already carries the current live candle.
|
|
13
13
|
* Docs: https://apidocs.lighter.xyz/docs/websocket-reference#candlesticks
|
|
14
|
+
*
|
|
15
|
+
* Exception: Lighter has no weekly channel. A '1w' subscription rides the native daily channel
|
|
16
|
+
* instead and this class folds each incoming daily candle into a running weekly bar client-side
|
|
17
|
+
* (see foldDayIntoWeek in chart/weekly.ts) — still no REST call of its own; the weekly seed comes
|
|
18
|
+
* from onHistoricalDataFetched, fed by DataCollector's already-synthesized weekly history.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
declare class LighterCandleWs extends BaseCandleWs {
|
|
22
|
+
private weeklyCandles;
|
|
23
|
+
private weeklyTrackedSymbols;
|
|
17
24
|
/** Subscribes to a market's candle stream; `symbol` is Lighter's numeric market id. */
|
|
18
25
|
protected buildSubscribeMessage(symbol: string, interval: CandleInterval): WsMessage;
|
|
19
26
|
/** Mirrors the subscribe channel — Lighter keys unsubscribes by the same slash-delimited name. */
|
|
20
27
|
protected buildUnsubscribeMessage(symbol: string, interval: CandleInterval): WsMessage;
|
|
28
|
+
/**
|
|
29
|
+
* Seeds the running weekly bar from the REST-aggregated weekly history, so the first live
|
|
30
|
+
* daily tick extends the correct week's open/high/low instead of starting a bar from today.
|
|
31
|
+
*
|
|
32
|
+
* DataCollector calls this for every historical fetch, including a backfill for an older
|
|
33
|
+
* range while a live subscription is already running (e.g. scrolling a chart back in time) —
|
|
34
|
+
* so a batch's last candle is not necessarily the current live week.
|
|
35
|
+
*/
|
|
36
|
+
onHistoricalDataFetched(symbol: string, interval: CandleInterval, candles: CandleData[]): void;
|
|
21
37
|
/**
|
|
22
38
|
* Translates one candle message into CandleData, oldest first.
|
|
23
39
|
*
|
|
@@ -25,6 +41,8 @@ declare class LighterCandleWs extends BaseCandleWs {
|
|
|
25
41
|
* followed by the newly opened one — both are emitted so the closed bar keeps its final trades.
|
|
26
42
|
*/
|
|
27
43
|
protected parseMessages(data: string): ParsedCandle[];
|
|
44
|
+
/** Folds one live daily candle into the symbol's running weekly bar and records the result. */
|
|
45
|
+
private mergeIntoWeeklyCandle;
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
export { LighterCandleWs };
|
package/dist/chart/ws/lighter.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getIntervalSeconds } from '../utils';
|
|
2
|
+
import { foldDayIntoWeek } from '../weekly';
|
|
2
3
|
import { BaseCandleWs } from './base-candle';
|
|
3
4
|
|
|
4
5
|
var __defProp = Object.defineProperty;
|
|
@@ -17,29 +18,74 @@ class LighterCandleWs extends BaseCandleWs {
|
|
|
17
18
|
static {
|
|
18
19
|
__name(this, "LighterCandleWs");
|
|
19
20
|
}
|
|
21
|
+
// Per-symbol running weekly bar, kept only while that symbol is subscribed at the synthesized '1w'.
|
|
22
|
+
weeklyCandles = /* @__PURE__ */ new Map();
|
|
23
|
+
weeklyTrackedSymbols = /* @__PURE__ */ new Set();
|
|
20
24
|
/** Subscribes to a market's candle stream; `symbol` is Lighter's numeric market id. */
|
|
21
25
|
buildSubscribeMessage(symbol, interval) {
|
|
26
|
+
const resolution = interval === "1w" ? "1d" : interval;
|
|
27
|
+
if (interval === "1w") {
|
|
28
|
+
this.weeklyTrackedSymbols.add(symbol);
|
|
29
|
+
}
|
|
22
30
|
const authToken = this.transport.authToken;
|
|
23
31
|
if (!authToken) {
|
|
24
32
|
return {
|
|
25
33
|
type: "subscribe",
|
|
26
|
-
channel: `candle/${symbol}/${
|
|
34
|
+
channel: `candle/${symbol}/${resolution}`
|
|
27
35
|
};
|
|
28
36
|
}
|
|
29
37
|
return {
|
|
30
38
|
type: "subscribe",
|
|
31
|
-
channel: `candle/${symbol}/${
|
|
39
|
+
channel: `candle/${symbol}/${resolution}`,
|
|
32
40
|
auth: authToken
|
|
33
41
|
};
|
|
34
42
|
}
|
|
35
43
|
/** Mirrors the subscribe channel — Lighter keys unsubscribes by the same slash-delimited name. */
|
|
36
44
|
buildUnsubscribeMessage(symbol, interval) {
|
|
45
|
+
const resolution = interval === "1w" ? "1d" : interval;
|
|
46
|
+
if (interval === "1w") {
|
|
47
|
+
this.weeklyTrackedSymbols.delete(symbol);
|
|
48
|
+
this.weeklyCandles.delete(symbol);
|
|
49
|
+
}
|
|
37
50
|
return {
|
|
38
51
|
type: "unsubscribe",
|
|
39
|
-
channel: `candle/${symbol}/${
|
|
52
|
+
channel: `candle/${symbol}/${resolution}`
|
|
40
53
|
};
|
|
41
54
|
}
|
|
42
55
|
/**
|
|
56
|
+
* Seeds the running weekly bar from the REST-aggregated weekly history, so the first live
|
|
57
|
+
* daily tick extends the correct week's open/high/low instead of starting a bar from today.
|
|
58
|
+
*
|
|
59
|
+
* DataCollector calls this for every historical fetch, including a backfill for an older
|
|
60
|
+
* range while a live subscription is already running (e.g. scrolling a chart back in time) —
|
|
61
|
+
* so a batch's last candle is not necessarily the current live week.
|
|
62
|
+
*/
|
|
63
|
+
onHistoricalDataFetched(symbol, interval, candles) {
|
|
64
|
+
if (interval !== "1w") {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const latest = candles[candles.length - 1];
|
|
68
|
+
if (!latest) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const tracked = this.weeklyCandles.get(symbol);
|
|
72
|
+
if (tracked && latest.t < tracked.t) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (tracked && latest.t === tracked.t) {
|
|
76
|
+
this.weeklyCandles.set(symbol, {
|
|
77
|
+
t: tracked.t,
|
|
78
|
+
T: tracked.T,
|
|
79
|
+
o: tracked.o,
|
|
80
|
+
h: Math.max(tracked.h, latest.h),
|
|
81
|
+
l: Math.min(tracked.l, latest.l),
|
|
82
|
+
c: tracked.c
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.weeklyCandles.set(symbol, latest);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
43
89
|
* Translates one candle message into CandleData, oldest first.
|
|
44
90
|
*
|
|
45
91
|
* Lighter batches updates every 500ms and, when a period rolls over, sends the just-closed candle
|
|
@@ -63,11 +109,18 @@ class LighterCandleWs extends BaseCandleWs {
|
|
|
63
109
|
return [];
|
|
64
110
|
}
|
|
65
111
|
const intervalMs = getIntervalSeconds(route.interval) * 1e3;
|
|
112
|
+
const isWeeklyTracked = route.interval === "1d" && this.weeklyTrackedSymbols.has(route.symbol);
|
|
66
113
|
return message.candles.map((candle) => ({
|
|
67
114
|
symbol: route.symbol,
|
|
68
|
-
candle: toCandleData(candle, intervalMs)
|
|
115
|
+
candle: isWeeklyTracked ? this.mergeIntoWeeklyCandle(route.symbol, toCandleData(candle, intervalMs)) : toCandleData(candle, intervalMs)
|
|
69
116
|
}));
|
|
70
117
|
}
|
|
118
|
+
/** Folds one live daily candle into the symbol's running weekly bar and records the result. */
|
|
119
|
+
mergeIntoWeeklyCandle(symbol, day) {
|
|
120
|
+
const weekly = foldDayIntoWeek(this.weeklyCandles.get(symbol), day);
|
|
121
|
+
this.weeklyCandles.set(symbol, weekly);
|
|
122
|
+
return weekly;
|
|
123
|
+
}
|
|
71
124
|
}
|
|
72
125
|
function parseChannel(channel) {
|
|
73
126
|
if (!channel) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pear-protocol/market-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pear Protocol Market SDK",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"typecheck": "tsc --noEmit"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@pear-protocol/types": "^1.
|
|
29
|
+
"@pear-protocol/types": "^1.30.0",
|
|
30
30
|
"bignumber.js": "^9.1.2",
|
|
31
31
|
"partysocket": "^1.0.3"
|
|
32
32
|
},
|