laplace-api 1.3.4 → 1.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laplace-api",
3
- "version": "1.3.4",
3
+ "version": "1.4.1",
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,49 @@
1
+
2
+ interface RawBISTStockLiveData {
3
+ _id: number;
4
+ symbol: string;
5
+ cl: number;
6
+ _i: string;
7
+ c: number;
8
+ d: number;
9
+ }
10
+
11
+ interface RawUSStockLiveData {
12
+ s: string;
13
+ p: number;
14
+ t: number;
15
+ }
16
+
1
17
  export interface BISTStockLiveData {
18
+ id: number;
19
+ symbol: string;
20
+ closePrice: number;
21
+ tipId: string;
22
+ percentChange: number;
23
+ timestamp: number;
24
+ }
25
+
26
+ export interface USStockLiveData {
2
27
  symbol: string;
3
- cl: number; // Close
4
- c: number; // PercentChange
28
+ closePrice: number;
29
+ timestamp: number;
30
+ }
31
+
32
+ export enum LivePriceFeed {
33
+ LiveBist = "live_price_tr",
34
+ LiveUs = "live_price_us",
35
+ DelayedBist = "delayed_price_tr",
36
+ DelayedUs = "delayed_price_us",
37
+ // DepthBist = "depth_tr",
5
38
  }
6
39
 
40
+ type StockLiveDataType<T extends LivePriceFeed> = T extends
41
+ | LivePriceFeed.LiveBist
42
+ | LivePriceFeed.DelayedBist
43
+ ? // | LivePriceFeed.DepthBist
44
+ BISTStockLiveData
45
+ : USStockLiveData;
46
+
7
47
  export enum LogLevel {
8
48
  Info = "info",
9
49
  Warn = "warn",
@@ -18,12 +58,7 @@ interface WebSocketOptions {
18
58
  maxReconnectDelay?: number;
19
59
  }
20
60
 
21
- type WebSocketMessageType = "heartbeat" | "error" | "warning" | "price_update";
22
-
23
- interface WebSocketMessage<T> {
24
- type: WebSocketMessageType;
25
- message?: T;
26
- }
61
+ type WebSocketMessageType = "heartbeat" | "error" | "warning" | "data";
27
62
 
28
63
  export enum WebSocketErrorType {
29
64
  MAX_RECONNECT_EXCEEDED = "MAX_RECONNECT_EXCEEDED",
@@ -60,7 +95,8 @@ export class LivePriceWebSocketClient {
60
95
  number,
61
96
  {
62
97
  symbols: string[];
63
- handler: (data: BISTStockLiveData) => void;
98
+ handler: (data: BISTStockLiveData | USStockLiveData) => void;
99
+ feed: LivePriceFeed;
64
100
  }
65
101
  >();
66
102
  private reconnectAttempts = 0;
@@ -117,10 +153,9 @@ export class LivePriceWebSocketClient {
117
153
  this.ws = new WebSocket(url);
118
154
  this.connectPromise = this.setupWebSocket();
119
155
 
120
- await this.connectPromise
156
+ await this.connectPromise;
121
157
 
122
158
  this.connectPromise = null;
123
-
124
159
  }
125
160
 
126
161
  return this.ws;
@@ -191,19 +226,48 @@ export class LivePriceWebSocketClient {
191
226
  this.ws.onmessage = (event) => {
192
227
  try {
193
228
  const rawData = JSON.parse(event.data.toString());
229
+
230
+ const feed = rawData.feed as LivePriceFeed;
194
231
  switch (rawData.type) {
195
- case "price_update":
196
- const priceData = rawData as WebSocketMessage<BISTStockLiveData>;
197
- const data = priceData.message;
198
- if (!data) {
232
+ case "data":
233
+ const messageData = rawData.message;
234
+ if (!messageData) {
199
235
  throw new WebSocketError(
200
236
  "Price update message is empty",
201
237
  WebSocketErrorType.MESSAGE_PARSE_ERROR
202
238
  );
203
239
  }
204
- if (data.symbol) {
205
- const handlers = this.getHandlersForSymbol(data.symbol);
206
- handlers.forEach((handler) => handler(data));
240
+ let priceData: BISTStockLiveData | USStockLiveData;
241
+
242
+ if (
243
+ feed === LivePriceFeed.DelayedBist ||
244
+ feed === LivePriceFeed.LiveBist
245
+ // ||
246
+ // feed === LivePriceFeed.DepthBist
247
+ ) {
248
+ const message = messageData as RawBISTStockLiveData;
249
+ priceData = {
250
+ symbol: message?.symbol,
251
+ id: message?._id,
252
+ tipId: message?._i,
253
+ closePrice: message?.cl,
254
+ timestamp: message?.d,
255
+ percentChange: message?.c,
256
+ } as BISTStockLiveData;
257
+ } else {
258
+ const message = messageData as RawUSStockLiveData;
259
+ priceData = {
260
+ symbol: message.s,
261
+ closePrice: message.p,
262
+ timestamp: message.t,
263
+ } as USStockLiveData;
264
+ }
265
+ if (priceData.symbol) {
266
+ const handlers = this.getHandlersForSymbol(
267
+ priceData.symbol,
268
+ feed
269
+ );
270
+ handlers.forEach((handler) => handler(priceData));
207
271
  }
208
272
  break;
209
273
  case "heartbeat":
@@ -226,14 +290,6 @@ export class LivePriceWebSocketClient {
226
290
  });
227
291
  }
228
292
 
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
293
  private async attemptReconnect() {
238
294
  const url = this.wsUrl;
239
295
  if (!url) {
@@ -267,11 +323,28 @@ export class LivePriceWebSocketClient {
267
323
  this.reconnectTimeout = setTimeout(async () => {
268
324
  try {
269
325
  await this.connect(url);
270
-
271
- this.isClosed = false
272
326
 
273
- const activeSymbols = this.getActiveSymbols();
274
- this.addSymbols(activeSymbols);
327
+ this.isClosed = false;
328
+
329
+ const symbolsByFeed = new Map<LivePriceFeed, string[]>();
330
+
331
+ this.subscriptions.forEach((subscription) => {
332
+ const { symbols, feed } = subscription;
333
+ if (!symbolsByFeed.has(feed)) {
334
+ symbolsByFeed.set(feed, []);
335
+ }
336
+ symbols.forEach((symbol) => {
337
+ const currentSymbols = symbolsByFeed.get(feed) || [];
338
+ if (!currentSymbols.includes(symbol)) {
339
+ currentSymbols.push(symbol);
340
+ symbolsByFeed.set(feed, currentSymbols);
341
+ }
342
+ });
343
+ });
344
+
345
+ symbolsByFeed.forEach((symbols, feed) => {
346
+ this.addSymbols(symbols, feed);
347
+ });
275
348
 
276
349
  if (this.reconnectTimeout) {
277
350
  clearTimeout(this.reconnectTimeout);
@@ -288,40 +361,50 @@ export class LivePriceWebSocketClient {
288
361
  }
289
362
  }
290
363
 
291
- subscribe(
364
+ subscribe<F extends LivePriceFeed>(
292
365
  symbols: string[],
293
- handler: (data: BISTStockLiveData) => void
366
+ feed: F,
367
+ handler: (data: StockLiveDataType<F>) => void
294
368
  ): () => void {
295
369
  const subscriptionId = this.subscriptionCounter++;
296
370
  let symbolsToAdd: string[] = [];
297
371
 
298
- this.subscriptions.set(subscriptionId, { symbols, handler });
372
+ const typedHandler = (data: BISTStockLiveData | USStockLiveData) => {
373
+ handler(data as StockLiveDataType<F>);
374
+ };
375
+
376
+ this.subscriptions.set(subscriptionId, {
377
+ symbols,
378
+ feed,
379
+ handler: typedHandler,
380
+ });
299
381
 
300
382
  for (const symbol of symbols) {
301
- if (this.getHandlersForSymbol(symbol).length === 1) {
383
+ if (this.getHandlersForSymbol(symbol, feed).length === 1) {
302
384
  symbolsToAdd.push(symbol);
303
385
  }
304
386
  }
305
- this.addSymbols(symbolsToAdd);
387
+ this.addSymbols(symbolsToAdd, feed);
306
388
 
307
389
  return () => {
308
390
  this.subscriptions.delete(subscriptionId);
309
391
  const symbolsForRemove = symbols.filter(
310
- (s) => this.getHandlersForSymbol(s).length === 0
392
+ (s) => this.getHandlersForSymbol(s, feed).length === 0
311
393
  );
312
- this.removeSymbols(symbolsForRemove);
394
+ this.removeSymbols(symbolsForRemove, feed);
313
395
  };
314
396
  }
315
397
 
316
398
  private getHandlersForSymbol(
317
- symbol: string
318
- ): ((data: BISTStockLiveData) => void)[] {
399
+ symbol: string,
400
+ feed: LivePriceFeed
401
+ ): ((data: BISTStockLiveData | USStockLiveData) => void)[] {
319
402
  return Array.from(this.subscriptions.values())
320
- .filter((s) => s.symbols.includes(symbol))
403
+ .filter((s) => s.symbols.includes(symbol) && s.feed === feed)
321
404
  .map((s) => s.handler);
322
405
  }
323
406
 
324
- private async removeSymbols(symbols: string[]) {
407
+ private async removeSymbols(symbols: string[], feed: LivePriceFeed) {
325
408
  if (symbols.length === 0) return;
326
409
 
327
410
  if (!this.ws) {
@@ -332,7 +415,7 @@ export class LivePriceWebSocketClient {
332
415
  }
333
416
 
334
417
  if (this.connectPromise) {
335
- await this.connectPromise
418
+ await this.connectPromise;
336
419
  }
337
420
 
338
421
  if (this.ws.readyState !== WebSocket.OPEN) {
@@ -341,15 +424,17 @@ export class LivePriceWebSocketClient {
341
424
  WebSocketErrorType.WEBSOCKET_NOT_CONNECTED
342
425
  );
343
426
  }
427
+
344
428
  this.ws.send(
345
429
  JSON.stringify({
346
430
  type: "unsubscribe",
347
431
  symbols: symbols,
432
+ feed: feed,
348
433
  })
349
434
  );
350
435
  }
351
436
 
352
- private async addSymbols(symbols: string[]) {
437
+ private async addSymbols(symbols: string[], feed: LivePriceFeed) {
353
438
  if (symbols.length === 0) return;
354
439
 
355
440
  if (!this.ws) {
@@ -360,7 +445,7 @@ export class LivePriceWebSocketClient {
360
445
  }
361
446
 
362
447
  if (this.connectPromise) {
363
- await this.connectPromise
448
+ await this.connectPromise;
364
449
  }
365
450
 
366
451
  if (this.ws.readyState !== WebSocket.OPEN) {
@@ -374,6 +459,7 @@ export class LivePriceWebSocketClient {
374
459
  JSON.stringify({
375
460
  type: "subscribe",
376
461
  symbols: symbols,
462
+ feed: feed,
377
463
  })
378
464
  );
379
465
  }
@@ -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=