@pear-protocol/market-sdk 0.1.0 → 0.2.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.
@@ -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.t >= startTime && candle.t < endTime);
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(fetchHistoricalCandles, "fetchHistoricalCandles");
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 };
@@ -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}/${interval}`
34
+ channel: `candle/${symbol}/${resolution}`
27
35
  };
28
36
  }
29
37
  return {
30
38
  type: "subscribe",
31
- channel: `candle/${symbol}/${interval}`,
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}/${interval}`
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.1.0",
3
+ "version": "0.2.1",
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.28.0",
29
+ "@pear-protocol/types": "^2.0.0",
30
30
  "bignumber.js": "^9.1.2",
31
31
  "partysocket": "^1.0.3"
32
32
  },