laplace-api 1.3.4 → 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.4",
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,9 +1,50 @@
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
+ }
17
+
1
18
  export interface BISTStockLiveData {
19
+ id: number;
20
+ symbol: string;
21
+ closePrice: number;
22
+ tipId: string;
23
+ percentChange: number;
24
+ timestamp: number;
25
+ }
26
+
27
+ export interface USStockLiveData {
2
28
  symbol: string;
3
- cl: number; // Close
4
- c: number; // PercentChange
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",
5
39
  }
6
40
 
41
+ type StockLiveDataType<T extends LivePriceFeed> = T extends
42
+ | LivePriceFeed.LiveBist
43
+ | LivePriceFeed.DelayedBist
44
+ ? // | LivePriceFeed.DepthBist
45
+ BISTStockLiveData
46
+ : USStockLiveData;
47
+
7
48
  export enum LogLevel {
8
49
  Info = "info",
9
50
  Warn = "warn",
@@ -18,12 +59,7 @@ interface WebSocketOptions {
18
59
  maxReconnectDelay?: number;
19
60
  }
20
61
 
21
- type WebSocketMessageType = "heartbeat" | "error" | "warning" | "price_update";
22
-
23
- interface WebSocketMessage<T> {
24
- type: WebSocketMessageType;
25
- message?: T;
26
- }
62
+ type WebSocketMessageType = "heartbeat" | "error" | "warning" | "data";
27
63
 
28
64
  export enum WebSocketErrorType {
29
65
  MAX_RECONNECT_EXCEEDED = "MAX_RECONNECT_EXCEEDED",
@@ -60,7 +96,8 @@ export class LivePriceWebSocketClient {
60
96
  number,
61
97
  {
62
98
  symbols: string[];
63
- handler: (data: BISTStockLiveData) => void;
99
+ handler: (data: BISTStockLiveData | USStockLiveData) => void;
100
+ feed: LivePriceFeed;
64
101
  }
65
102
  >();
66
103
  private reconnectAttempts = 0;
@@ -117,10 +154,9 @@ export class LivePriceWebSocketClient {
117
154
  this.ws = new WebSocket(url);
118
155
  this.connectPromise = this.setupWebSocket();
119
156
 
120
- await this.connectPromise
157
+ await this.connectPromise;
121
158
 
122
159
  this.connectPromise = null;
123
-
124
160
  }
125
161
 
126
162
  return this.ws;
@@ -191,19 +227,48 @@ export class LivePriceWebSocketClient {
191
227
  this.ws.onmessage = (event) => {
192
228
  try {
193
229
  const rawData = JSON.parse(event.data.toString());
230
+
231
+ const feed = rawData.feed as LivePriceFeed;
194
232
  switch (rawData.type) {
195
- case "price_update":
196
- const priceData = rawData as WebSocketMessage<BISTStockLiveData>;
197
- const data = priceData.message;
198
- if (!data) {
233
+ case "data":
234
+ const messageData = rawData.message;
235
+ if (!messageData) {
199
236
  throw new WebSocketError(
200
237
  "Price update message is empty",
201
238
  WebSocketErrorType.MESSAGE_PARSE_ERROR
202
239
  );
203
240
  }
204
- if (data.symbol) {
205
- const handlers = this.getHandlersForSymbol(data.symbol);
206
- 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));
207
272
  }
208
273
  break;
209
274
  case "heartbeat":
@@ -226,14 +291,6 @@ export class LivePriceWebSocketClient {
226
291
  });
227
292
  }
228
293
 
229
- private getActiveSymbols(): string[] {
230
- const allSymbols = Array.from(this.subscriptions.values()).flatMap(
231
- (sub) => sub.symbols
232
- );
233
-
234
- return [...new Set(allSymbols)];
235
- }
236
-
237
294
  private async attemptReconnect() {
238
295
  const url = this.wsUrl;
239
296
  if (!url) {
@@ -267,11 +324,28 @@ export class LivePriceWebSocketClient {
267
324
  this.reconnectTimeout = setTimeout(async () => {
268
325
  try {
269
326
  await this.connect(url);
270
-
271
- this.isClosed = false
272
327
 
273
- const activeSymbols = this.getActiveSymbols();
274
- 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
+ });
275
349
 
276
350
  if (this.reconnectTimeout) {
277
351
  clearTimeout(this.reconnectTimeout);
@@ -288,40 +362,50 @@ export class LivePriceWebSocketClient {
288
362
  }
289
363
  }
290
364
 
291
- subscribe(
365
+ subscribe<F extends LivePriceFeed>(
292
366
  symbols: string[],
293
- handler: (data: BISTStockLiveData) => void
367
+ feed: F,
368
+ handler: (data: StockLiveDataType<F>) => void
294
369
  ): () => void {
295
370
  const subscriptionId = this.subscriptionCounter++;
296
371
  let symbolsToAdd: string[] = [];
297
372
 
298
- 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
+ });
299
382
 
300
383
  for (const symbol of symbols) {
301
- if (this.getHandlersForSymbol(symbol).length === 1) {
384
+ if (this.getHandlersForSymbol(symbol, feed).length === 1) {
302
385
  symbolsToAdd.push(symbol);
303
386
  }
304
387
  }
305
- this.addSymbols(symbolsToAdd);
388
+ this.addSymbols(symbolsToAdd, feed);
306
389
 
307
390
  return () => {
308
391
  this.subscriptions.delete(subscriptionId);
309
392
  const symbolsForRemove = symbols.filter(
310
- (s) => this.getHandlersForSymbol(s).length === 0
393
+ (s) => this.getHandlersForSymbol(s, feed).length === 0
311
394
  );
312
- this.removeSymbols(symbolsForRemove);
395
+ this.removeSymbols(symbolsForRemove, feed);
313
396
  };
314
397
  }
315
398
 
316
399
  private getHandlersForSymbol(
317
- symbol: string
318
- ): ((data: BISTStockLiveData) => void)[] {
400
+ symbol: string,
401
+ feed: LivePriceFeed
402
+ ): ((data: BISTStockLiveData | USStockLiveData) => void)[] {
319
403
  return Array.from(this.subscriptions.values())
320
- .filter((s) => s.symbols.includes(symbol))
404
+ .filter((s) => s.symbols.includes(symbol) && s.feed === feed)
321
405
  .map((s) => s.handler);
322
406
  }
323
407
 
324
- private async removeSymbols(symbols: string[]) {
408
+ private async removeSymbols(symbols: string[], feed: LivePriceFeed) {
325
409
  if (symbols.length === 0) return;
326
410
 
327
411
  if (!this.ws) {
@@ -332,7 +416,7 @@ export class LivePriceWebSocketClient {
332
416
  }
333
417
 
334
418
  if (this.connectPromise) {
335
- await this.connectPromise
419
+ await this.connectPromise;
336
420
  }
337
421
 
338
422
  if (this.ws.readyState !== WebSocket.OPEN) {
@@ -341,15 +425,17 @@ export class LivePriceWebSocketClient {
341
425
  WebSocketErrorType.WEBSOCKET_NOT_CONNECTED
342
426
  );
343
427
  }
428
+
344
429
  this.ws.send(
345
430
  JSON.stringify({
346
431
  type: "unsubscribe",
347
432
  symbols: symbols,
433
+ feed: feed,
348
434
  })
349
435
  );
350
436
  }
351
437
 
352
- private async addSymbols(symbols: string[]) {
438
+ private async addSymbols(symbols: string[], feed: LivePriceFeed) {
353
439
  if (symbols.length === 0) return;
354
440
 
355
441
  if (!this.ws) {
@@ -360,7 +446,7 @@ export class LivePriceWebSocketClient {
360
446
  }
361
447
 
362
448
  if (this.connectPromise) {
363
- await this.connectPromise
449
+ await this.connectPromise;
364
450
  }
365
451
 
366
452
  if (this.ws.readyState !== WebSocket.OPEN) {
@@ -374,6 +460,7 @@ export class LivePriceWebSocketClient {
374
460
  JSON.stringify({
375
461
  type: "subscribe",
376
462
  symbols: symbols,
463
+ feed: feed,
377
464
  })
378
465
  );
379
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
  });
@@ -1,2 +1,2 @@
1
- BASE_URL=https://api.finfree.app
2
- API_KEY=api-6fa6cefb-16df-4a19-8351-54f83c6bbe2f
1
+ BASE_URL=
2
+ API_KEY=