laplace-api 1.3.3 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laplace-api",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Client library for Laplace API for the US stock market and BIST (Istanbul stock market) fundamental financial data.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,23 +1,65 @@
1
+ import { WebSocket } from "ws";
2
+
3
+ interface RawBISTStockLiveData {
4
+ _id: number;
5
+ symbol: string;
6
+ cl: number;
7
+ _i: string;
8
+ c: number;
9
+ d: number;
10
+ }
11
+
12
+ interface RawUSStockLiveData {
13
+ s: string;
14
+ p: number;
15
+ t: number;
16
+ }
1
17
 
2
18
  export interface BISTStockLiveData {
19
+ id: number;
3
20
  symbol: string;
4
- cl: number; // Close
5
- c: number; // PercentChange
21
+ closePrice: number;
22
+ tipId: string;
23
+ percentChange: number;
24
+ timestamp: number;
25
+ }
26
+
27
+ export interface USStockLiveData {
28
+ symbol: string;
29
+ closePrice: number;
30
+ timestamp: number;
31
+ }
32
+
33
+ export enum LivePriceFeed {
34
+ LiveBist = "live_price_tr",
35
+ LiveUs = "live_price_us",
36
+ DelayedBist = "delayed_price_tr",
37
+ DelayedUs = "delayed_price_us",
38
+ // DepthBist = "depth_tr",
39
+ }
40
+
41
+ type StockLiveDataType<T extends LivePriceFeed> = T extends
42
+ | LivePriceFeed.LiveBist
43
+ | LivePriceFeed.DelayedBist
44
+ ? // | LivePriceFeed.DepthBist
45
+ BISTStockLiveData
46
+ : USStockLiveData;
47
+
48
+ export enum LogLevel {
49
+ Info = "info",
50
+ Warn = "warn",
51
+ Error = "error",
6
52
  }
7
53
 
8
54
  interface WebSocketOptions {
9
55
  enableLogging?: boolean;
56
+ logLevel?: LogLevel;
10
57
  reconnectAttempts?: number;
11
58
  reconnectDelay?: number;
12
59
  maxReconnectDelay?: number;
13
60
  }
14
61
 
15
- type WebSocketMessageType = "heartbeat" | "error" | "warning" | "price_update";
16
-
17
- interface WebSocketMessage<T> {
18
- type: WebSocketMessageType;
19
- message?: T;
20
- }
62
+ type WebSocketMessageType = "heartbeat" | "error" | "warning" | "data";
21
63
 
22
64
  export enum WebSocketErrorType {
23
65
  MAX_RECONNECT_EXCEEDED = "MAX_RECONNECT_EXCEEDED",
@@ -54,7 +96,8 @@ export class LivePriceWebSocketClient {
54
96
  number,
55
97
  {
56
98
  symbols: string[];
57
- handler: (data: BISTStockLiveData) => void;
99
+ handler: (data: BISTStockLiveData | USStockLiveData) => void;
100
+ feed: LivePriceFeed;
58
101
  }
59
102
  >();
60
103
  private reconnectAttempts = 0;
@@ -68,6 +111,7 @@ export class LivePriceWebSocketClient {
68
111
  constructor(options: WebSocketOptions = {}) {
69
112
  this.options = {
70
113
  enableLogging: true,
114
+ logLevel: LogLevel.Error,
71
115
  reconnectAttempts: 5,
72
116
  reconnectDelay: 5000,
73
117
  maxReconnectDelay: 30000,
@@ -79,6 +123,15 @@ export class LivePriceWebSocketClient {
79
123
  if (!this.options.enableLogging) return;
80
124
 
81
125
  const prefix = `[LivePriceWebSocket][${level.toUpperCase()}]`;
126
+ const logLevel = this.options.logLevel;
127
+
128
+ if (logLevel === LogLevel.Error && level !== "error") {
129
+ return;
130
+ }
131
+
132
+ if (logLevel === LogLevel.Warn && level === "info") {
133
+ return;
134
+ }
82
135
 
83
136
  switch (level) {
84
137
  case "error":
@@ -101,10 +154,9 @@ export class LivePriceWebSocketClient {
101
154
  this.ws = new WebSocket(url);
102
155
  this.connectPromise = this.setupWebSocket();
103
156
 
104
- await this.connectPromise
157
+ await this.connectPromise;
105
158
 
106
159
  this.connectPromise = null;
107
-
108
160
  }
109
161
 
110
162
  return this.ws;
@@ -175,19 +227,48 @@ export class LivePriceWebSocketClient {
175
227
  this.ws.onmessage = (event) => {
176
228
  try {
177
229
  const rawData = JSON.parse(event.data.toString());
230
+
231
+ const feed = rawData.feed as LivePriceFeed;
178
232
  switch (rawData.type) {
179
- case "price_update":
180
- const priceData = rawData as WebSocketMessage<BISTStockLiveData>;
181
- const data = priceData.message;
182
- if (!data) {
233
+ case "data":
234
+ const messageData = rawData.message;
235
+ if (!messageData) {
183
236
  throw new WebSocketError(
184
237
  "Price update message is empty",
185
238
  WebSocketErrorType.MESSAGE_PARSE_ERROR
186
239
  );
187
240
  }
188
- if (data.symbol) {
189
- const handlers = this.getHandlersForSymbol(data.symbol);
190
- handlers.forEach((handler) => handler(data));
241
+ let priceData: BISTStockLiveData | USStockLiveData;
242
+
243
+ if (
244
+ feed === LivePriceFeed.DelayedBist ||
245
+ feed === LivePriceFeed.LiveBist
246
+ // ||
247
+ // feed === LivePriceFeed.DepthBist
248
+ ) {
249
+ const message = messageData as RawBISTStockLiveData;
250
+ priceData = {
251
+ symbol: message?.symbol,
252
+ id: message?._id,
253
+ tipId: message?._i,
254
+ closePrice: message?.cl,
255
+ timestamp: message?.d,
256
+ percentChange: message?.c,
257
+ } as BISTStockLiveData;
258
+ } else {
259
+ const message = messageData as RawUSStockLiveData;
260
+ priceData = {
261
+ symbol: message.s,
262
+ closePrice: message.p,
263
+ timestamp: message.t,
264
+ } as USStockLiveData;
265
+ }
266
+ if (priceData.symbol) {
267
+ const handlers = this.getHandlersForSymbol(
268
+ priceData.symbol,
269
+ feed
270
+ );
271
+ handlers.forEach((handler) => handler(priceData));
191
272
  }
192
273
  break;
193
274
  case "heartbeat":
@@ -210,14 +291,6 @@ export class LivePriceWebSocketClient {
210
291
  });
211
292
  }
212
293
 
213
- private getActiveSymbols(): string[] {
214
- const allSymbols = Array.from(this.subscriptions.values()).flatMap(
215
- (sub) => sub.symbols
216
- );
217
-
218
- return [...new Set(allSymbols)];
219
- }
220
-
221
294
  private async attemptReconnect() {
222
295
  const url = this.wsUrl;
223
296
  if (!url) {
@@ -251,11 +324,28 @@ export class LivePriceWebSocketClient {
251
324
  this.reconnectTimeout = setTimeout(async () => {
252
325
  try {
253
326
  await this.connect(url);
254
-
255
- this.isClosed = false
256
327
 
257
- const activeSymbols = this.getActiveSymbols();
258
- this.addSymbols(activeSymbols);
328
+ this.isClosed = false;
329
+
330
+ const symbolsByFeed = new Map<LivePriceFeed, string[]>();
331
+
332
+ this.subscriptions.forEach((subscription) => {
333
+ const { symbols, feed } = subscription;
334
+ if (!symbolsByFeed.has(feed)) {
335
+ symbolsByFeed.set(feed, []);
336
+ }
337
+ symbols.forEach((symbol) => {
338
+ const currentSymbols = symbolsByFeed.get(feed) || [];
339
+ if (!currentSymbols.includes(symbol)) {
340
+ currentSymbols.push(symbol);
341
+ symbolsByFeed.set(feed, currentSymbols);
342
+ }
343
+ });
344
+ });
345
+
346
+ symbolsByFeed.forEach((symbols, feed) => {
347
+ this.addSymbols(symbols, feed);
348
+ });
259
349
 
260
350
  if (this.reconnectTimeout) {
261
351
  clearTimeout(this.reconnectTimeout);
@@ -272,40 +362,50 @@ export class LivePriceWebSocketClient {
272
362
  }
273
363
  }
274
364
 
275
- subscribe(
365
+ subscribe<F extends LivePriceFeed>(
276
366
  symbols: string[],
277
- handler: (data: BISTStockLiveData) => void
367
+ feed: F,
368
+ handler: (data: StockLiveDataType<F>) => void
278
369
  ): () => void {
279
370
  const subscriptionId = this.subscriptionCounter++;
280
371
  let symbolsToAdd: string[] = [];
281
372
 
282
- this.subscriptions.set(subscriptionId, { symbols, handler });
373
+ const typedHandler = (data: BISTStockLiveData | USStockLiveData) => {
374
+ handler(data as StockLiveDataType<F>);
375
+ };
376
+
377
+ this.subscriptions.set(subscriptionId, {
378
+ symbols,
379
+ feed,
380
+ handler: typedHandler,
381
+ });
283
382
 
284
383
  for (const symbol of symbols) {
285
- if (this.getHandlersForSymbol(symbol).length === 1) {
384
+ if (this.getHandlersForSymbol(symbol, feed).length === 1) {
286
385
  symbolsToAdd.push(symbol);
287
386
  }
288
387
  }
289
- this.addSymbols(symbolsToAdd);
388
+ this.addSymbols(symbolsToAdd, feed);
290
389
 
291
390
  return () => {
292
391
  this.subscriptions.delete(subscriptionId);
293
392
  const symbolsForRemove = symbols.filter(
294
- (s) => this.getHandlersForSymbol(s).length === 0
393
+ (s) => this.getHandlersForSymbol(s, feed).length === 0
295
394
  );
296
- this.removeSymbols(symbolsForRemove);
395
+ this.removeSymbols(symbolsForRemove, feed);
297
396
  };
298
397
  }
299
398
 
300
399
  private getHandlersForSymbol(
301
- symbol: string
302
- ): ((data: BISTStockLiveData) => void)[] {
400
+ symbol: string,
401
+ feed: LivePriceFeed
402
+ ): ((data: BISTStockLiveData | USStockLiveData) => void)[] {
303
403
  return Array.from(this.subscriptions.values())
304
- .filter((s) => s.symbols.includes(symbol))
404
+ .filter((s) => s.symbols.includes(symbol) && s.feed === feed)
305
405
  .map((s) => s.handler);
306
406
  }
307
407
 
308
- private async removeSymbols(symbols: string[]) {
408
+ private async removeSymbols(symbols: string[], feed: LivePriceFeed) {
309
409
  if (symbols.length === 0) return;
310
410
 
311
411
  if (!this.ws) {
@@ -316,7 +416,7 @@ export class LivePriceWebSocketClient {
316
416
  }
317
417
 
318
418
  if (this.connectPromise) {
319
- await this.connectPromise
419
+ await this.connectPromise;
320
420
  }
321
421
 
322
422
  if (this.ws.readyState !== WebSocket.OPEN) {
@@ -325,15 +425,17 @@ export class LivePriceWebSocketClient {
325
425
  WebSocketErrorType.WEBSOCKET_NOT_CONNECTED
326
426
  );
327
427
  }
428
+
328
429
  this.ws.send(
329
430
  JSON.stringify({
330
431
  type: "unsubscribe",
331
432
  symbols: symbols,
433
+ feed: feed,
332
434
  })
333
435
  );
334
436
  }
335
437
 
336
- private async addSymbols(symbols: string[]) {
438
+ private async addSymbols(symbols: string[], feed: LivePriceFeed) {
337
439
  if (symbols.length === 0) return;
338
440
 
339
441
  if (!this.ws) {
@@ -344,7 +446,7 @@ export class LivePriceWebSocketClient {
344
446
  }
345
447
 
346
448
  if (this.connectPromise) {
347
- await this.connectPromise
449
+ await this.connectPromise;
348
450
  }
349
451
 
350
452
  if (this.ws.readyState !== WebSocket.OPEN) {
@@ -358,6 +460,7 @@ export class LivePriceWebSocketClient {
358
460
  JSON.stringify({
359
461
  type: "subscribe",
360
462
  symbols: symbols,
463
+ feed: feed,
361
464
  })
362
465
  );
363
466
  }
@@ -1,6 +1,7 @@
1
1
  import { Client } from "./client";
2
2
  import { Region } from "./collections";
3
3
  import { v4 as uuidv4 } from 'uuid';
4
+ import { LivePriceFeed } from "./live-price-web-socket";
4
5
 
5
6
  interface WebSocketUrlResponse {
6
7
  url: string;
@@ -8,6 +9,7 @@ interface WebSocketUrlResponse {
8
9
 
9
10
  interface WebSocketUrlParams {
10
11
  externalUserId: string;
12
+ feeds: LivePriceFeed[];
11
13
  }
12
14
 
13
15
  export interface BISTStockLiveData {
@@ -39,14 +41,15 @@ function getSSELivePrice<T>(
39
41
  export class LivePriceClient extends Client {
40
42
  async getWebSocketUrl(
41
43
  externalUserId: string,
42
- region: Region
44
+ region: Region,
45
+ feeds: LivePriceFeed[]
43
46
  ): Promise<string> {
44
- const url = new URL(`${this["baseUrl"]}/api/v1/ws/url`);
47
+ const url = new URL(`${this["baseUrl"]}/api/v2/ws/url`);
45
48
  url.searchParams.append("region", region);
46
- url.searchParams.append("accessLevel", "KRMD1");
47
49
 
48
50
  const params: WebSocketUrlParams = {
49
51
  externalUserId,
52
+ feeds
50
53
  };
51
54
 
52
55
  const response = await this.sendRequest<WebSocketUrlResponse>({
@@ -5,7 +5,9 @@ import "./client_test_suite";
5
5
  import { LivePriceClient } from "../client/live-price";
6
6
  import {
7
7
  BISTStockLiveData,
8
+ LivePriceFeed,
8
9
  LivePriceWebSocketClient,
10
+ USStockLiveData,
9
11
  } from "../client/live-price-web-socket";
10
12
 
11
13
  describe("LivePrice", () => {
@@ -28,7 +30,9 @@ describe("LivePrice", () => {
28
30
  } as unknown as Logger;
29
31
 
30
32
  livePriceUrlClient = new LivePriceClient(config, logger);
31
- url = await livePriceUrlClient.getWebSocketUrl("2459", Region.Tr);
33
+ url = await livePriceUrlClient.getWebSocketUrl("2459", Region.Tr, [
34
+ LivePriceFeed.LiveBist,
35
+ ]);
32
36
 
33
37
  ws = new LivePriceWebSocketClient({
34
38
  enableLogging: true,
@@ -47,22 +51,28 @@ describe("LivePrice", () => {
47
51
 
48
52
  describe("BIST Live Price Tests", () => {
49
53
  const symbols = ["TUPRS", "SASA", "THYAO", "GARAN", "YKBNK"];
50
- // const newSymbols = ["AKBNK", "KCHOL"];
51
54
 
52
55
  it(
53
56
  "should receive data for initial and updated symbols",
54
57
  async () => {
55
58
  const receivedData: BISTStockLiveData[] = [];
56
59
 
57
- let unsubscribe: (() => void) | null = ws.subscribe(symbols, (data) => {
58
- console.log("RECEIVED DATA", data);
59
- receivedData.push(data);
60
- });
60
+ let unsubscribe: (() => void) | null =
61
+ ws.subscribe<LivePriceFeed.LiveBist>(
62
+ symbols,
63
+ LivePriceFeed.LiveBist,
64
+ (data) => {
65
+ console.log("RECEIVED DATA", data);
66
+ receivedData.push(data);
67
+ }
68
+ );
61
69
 
62
70
  await new Promise((resolve) => setTimeout(resolve, 20000));
63
71
 
64
72
  for (const symbol of symbols) {
65
- const symbolData = receivedData.filter((data) => data.symbol === symbol);
73
+ const symbolData = receivedData.filter(
74
+ (data) => data.symbol === symbol
75
+ );
66
76
  expect(symbolData.length).toBeGreaterThan(0);
67
77
  }
68
78
 
@@ -70,72 +80,39 @@ describe("LivePrice", () => {
70
80
  },
71
81
  TEST_CONSTANTS.JEST_TIMEOUT
72
82
  );
73
-
74
- // it(
75
- // "should handle multiple subscriptions for the same symbol",
76
- // async () => {
77
- // const symbol = "GARAN";
78
- // const receivedData1: BISTStockLiveData[] = [];
79
- // const receivedData2: BISTStockLiveData[] = [];
80
-
81
- // await new Promise<void>((resolve, reject) => {
82
- // const timeoutId = setTimeout(() => {
83
- // reject(new Error("Test timeout: No data received"));
84
- // }, TEST_CONSTANTS.MAIN_TIMEOUT).unref();
85
-
86
- // const unsubscribe1 = ws.subscribe([symbol], (data) => {
87
- // receivedData1.push(data);
88
- // });
89
-
90
- // const unsubscribe2 = ws.subscribe([symbol], (data) => {
91
- // receivedData2.push(data);
92
- // if (receivedData2.length >= 2) {
93
- // clearTimeout(timeoutId);
94
- // unsubscribe1();
95
- // unsubscribe2();
96
- // resolve();
97
- // }
98
- // });
99
- // });
100
-
101
- // expect(receivedData1.length).toBeGreaterThan(0);
102
- // expect(receivedData2.length).toBeGreaterThan(0);
103
- // expect(receivedData1).toEqual(receivedData2);
104
- // },
105
- // TEST_CONSTANTS.JEST_TIMEOUT
106
- // );
107
83
  });
108
84
 
109
- // const testLivePrice = async (
110
- // symbols: string[],
111
- // region: Region,
112
- // getLivePriceFunc: (symbols: string[], region: Region) => AsyncGenerator<BISTStockLiveData | USStockLiveData, void, unknown>
113
- // ) => {
114
- // const livePriceGenerator = getLivePriceFunc.call(livePriceClient, symbols, region);
115
- // let livePriceCount = 0;
116
-
117
- // try {
118
- // for await (const livePrice of livePriceGenerator) {
119
- // expect(livePrice).not.toBeEmpty();
120
- // livePriceCount++;
121
- // if (livePriceCount > 3) {
122
- // break;
123
- // }
124
- // }
125
- // } catch (error) {
126
- // throw new Error(`Error occurred during live price retrieval: ${error}`);
127
- // }
128
-
129
- // expect(livePriceCount).toBeGreaterThan(0);
130
- // };
131
-
132
- // test('BISTLivePrice', async () => {
133
- // const symbols = ['TUPRS', 'SASA', 'THYAO', 'GARAN', 'YKBN'];
134
- // await testLivePrice(symbols, Region.Tr, livePriceClient.getLivePriceForBIST);
135
- // }, 10000);
136
-
137
- // test('USLivePrice', async () => {
138
- // const symbols = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'META'];
139
- // await testLivePrice(symbols, Region.Us, livePriceClient.getLivePriceForUS);
140
- // }, 10000);
85
+ //TODO: Use this test after region issue fixed
86
+ // describe("US Live Price Tests", () => {
87
+ // const symbols = ["AAPL"];
88
+
89
+ // it(
90
+ // "should receive data for initial and updated symbols for us",
91
+ // async () => {
92
+ // const receivedData: USStockLiveData[] = [];
93
+
94
+ // let unsubscribe: (() => void) | null =
95
+ // ws.subscribe<LivePriceFeed.LiveUs>(
96
+ // symbols,
97
+ // LivePriceFeed.LiveUs,
98
+ // (data) => {
99
+ // console.log("RECEIVED DATA FOR US", data);
100
+ // receivedData.push(data);
101
+ // }
102
+ // );
103
+
104
+ // await new Promise((resolve) => setTimeout(resolve, 20000));
105
+
106
+ // for (const symbol of symbols) {
107
+ // const symbolData = receivedData.filter(
108
+ // (data) => data.symbol === symbol
109
+ // );
110
+ // expect(symbolData.length).toBeGreaterThan(0);
111
+ // }
112
+
113
+ // unsubscribe();
114
+ // },
115
+ // TEST_CONSTANTS.JEST_TIMEOUT
116
+ // );
117
+ // });
141
118
  });