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,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
|
-
|
|
4
|
-
|
|
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" | "
|
|
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 "
|
|
196
|
-
const
|
|
197
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/client/live-price.ts
CHANGED
|
@@ -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/
|
|
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 =
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
});
|
package/src/utilities/test.env
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
BASE_URL=
|
|
2
|
-
API_KEY=
|
|
1
|
+
BASE_URL=
|
|
2
|
+
API_KEY=
|