laplace-api 4.5.0 → 4.6.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": "4.5.0",
3
+ "version": "4.6.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": {
@@ -46,6 +46,8 @@ type StockLiveDataType<T extends LivePriceFeed> = T extends
46
46
  ? OrderbookLiveData
47
47
  : USStockLiveData;
48
48
 
49
+ type LastDataKey = `${string}:${LivePriceFeed}`;
50
+
49
51
  export enum LogLevel {
50
52
  Info = "info",
51
53
  Warn = "warn",
@@ -101,6 +103,13 @@ export class LivePriceWebSocketClient {
101
103
  feed: LivePriceFeed;
102
104
  }
103
105
  >();
106
+ private symbolLastData = new Map<
107
+ LastDataKey,
108
+ BISTStockLiveData | USStockLiveData | OrderbookLiveData
109
+ >();
110
+ private getLastDataKey(symbol: string, feed: LivePriceFeed): LastDataKey {
111
+ return `${symbol}:${feed}`;
112
+ }
104
113
  private reconnectAttempts = 0;
105
114
  private reconnectTimeout: NodeJS.Timeout | null = null;
106
115
  private isClosed: boolean = false;
@@ -306,6 +315,8 @@ export class LivePriceWebSocketClient {
306
315
  } as USStockLiveData;
307
316
  }
308
317
  if (priceData.symbol) {
318
+ const lastDataKey = this.getLastDataKey(priceData.symbol, feed);
319
+ this.symbolLastData.set(lastDataKey, priceData);
309
320
  const handlers = this.getHandlersForSymbol(
310
321
  priceData.symbol,
311
322
  feed
@@ -426,6 +437,17 @@ export class LivePriceWebSocketClient {
426
437
  const symbolHandlers = this.getHandlersForSymbol(symbol, feed);
427
438
  if (symbolHandlers.length === 1) {
428
439
  symbolsToAdd.push(symbol);
440
+ } else if (symbolHandlers.length > 1) {
441
+ const lastDataKey = this.getLastDataKey(symbol, feed);
442
+ const lastData:
443
+ | BISTStockLiveData
444
+ | USStockLiveData
445
+ | OrderbookLiveData
446
+ | undefined = this.symbolLastData.get(lastDataKey);
447
+
448
+ if (lastData) {
449
+ typedHandler(lastData);
450
+ }
429
451
  }
430
452
  }
431
453
  this.addSymbols(symbolsToAdd, feed);
@@ -476,6 +498,11 @@ export class LivePriceWebSocketClient {
476
498
  feed: feed,
477
499
  })
478
500
  );
501
+
502
+ for (const symbol of symbols) {
503
+ const key = this.getLastDataKey(symbol, feed);
504
+ this.symbolLastData.delete(key);
505
+ }
479
506
  }
480
507
 
481
508
  private async addSymbols(symbols: string[], feed: LivePriceFeed) {
@@ -0,0 +1,137 @@
1
+ import {Client} from "./client";
2
+ import {Locale, Region} from "./collections";
3
+ import {PaginatedResponse} from "./capital_increase";
4
+ import {SortDirection} from "./broker";
5
+
6
+ export interface NewsHighlights {
7
+ consumer: string[];
8
+ energyAndUtilities: string[];
9
+ finance: string[];
10
+ healthcare: string[];
11
+ industrialsAndMaterials: string[];
12
+ tech: string[];
13
+ other: string[];
14
+ }
15
+
16
+ export enum NewsType {
17
+ BRIEFS = "briefs",
18
+ BLOOMBERG = "bloomberg",
19
+ FDA = "fda",
20
+ REUTERS = "reuters",
21
+ }
22
+
23
+ export enum NewsOrderBy {
24
+ TIMESTAMP = "timestamp",
25
+ }
26
+
27
+ export interface News {
28
+ url: string;
29
+ imageUrl: string;
30
+ timestamp: string;
31
+ publisherUrl: string;
32
+ publisher: NewsPublisher;
33
+ relatedTickers: NewsTicker[];
34
+ qualityScore: number;
35
+ createdAt: string;
36
+ tickers?: NewsTicker[];
37
+ categories?: NewsCategories;
38
+ sectors?: NewsSector;
39
+ content?: NewsContent;
40
+ industries?: NewsIndustry;
41
+ }
42
+
43
+ export interface NewsPublisher {
44
+ name: string;
45
+ logoUrl?: string;
46
+ }
47
+
48
+ export interface NewsTicker {
49
+ id: string;
50
+ name: string;
51
+ symbol?: string;
52
+ }
53
+
54
+ export interface NewsCategories {
55
+ name: string;
56
+ newsCount: number;
57
+ categoryType?: string;
58
+ meanType?: number;
59
+ }
60
+
61
+ export interface NewsSector {
62
+ name: string;
63
+ newsCount: number;
64
+ categoryType?: string;
65
+ meanType?: number;
66
+ }
67
+
68
+ export interface NewsContent {
69
+ title: string;
70
+ description: string;
71
+ content: string[];
72
+ summary: string[];
73
+ investorInsight: string;
74
+ }
75
+
76
+ export interface NewsIndustry {
77
+ name: string;
78
+ meanType: number;
79
+ }
80
+
81
+ export class NewsClient extends Client {
82
+ async getHighlights(region: Region, locale: Locale): Promise<NewsHighlights> {
83
+ const url = new URL(
84
+ `${this["baseUrl"]}/api/v1/news/highlights`,
85
+ );
86
+ url.searchParams.append("region", region);
87
+ url.searchParams.append("locale", locale);
88
+
89
+ return this.sendRequest<NewsHighlights>({
90
+ method: "GET",
91
+ url: url.toString(),
92
+ })
93
+ }
94
+
95
+ async getNews(
96
+ region: Region,
97
+ locale: Locale,
98
+ newsType: NewsType,
99
+ page: number | null,
100
+ size: number | null,
101
+ orderBy: NewsOrderBy | null,
102
+ orderByDirection: SortDirection | null,
103
+ extraFilters: string | null,
104
+ ): Promise<PaginatedResponse<News>> {
105
+ const url = new URL(
106
+ `${this["baseUrl"]}/api/v1/news`,
107
+ );
108
+ url.searchParams.append("region", region);
109
+ url.searchParams.append("locale", locale);
110
+ url.searchParams.append("newsType", newsType);
111
+
112
+ if (page) {
113
+ url.searchParams.append("page", page.toString());
114
+ }
115
+
116
+ if (size) {
117
+ url.searchParams.append("size", size.toString());
118
+ }
119
+
120
+ if (orderBy) {
121
+ url.searchParams.append("orderBy", orderBy);
122
+ }
123
+
124
+ if (orderByDirection) {
125
+ url.searchParams.append("orderByDirection", orderByDirection);
126
+ }
127
+
128
+ if (extraFilters) {
129
+ url.searchParams.append("extraFilters", extraFilters);
130
+ }
131
+
132
+ return this.sendRequest<PaginatedResponse<News>>({
133
+ method: "GET",
134
+ url: url.toString(),
135
+ });
136
+ }
137
+ }
@@ -0,0 +1,204 @@
1
+ import { Logger } from "winston";
2
+ import { LaplaceConfiguration } from "../utilities/configuration";
3
+ import {
4
+ NewsClient,
5
+ NewsHighlights,
6
+ News,
7
+ NewsType,
8
+ NewsOrderBy,
9
+ } from "../client/news";
10
+ import "./client_test_suite";
11
+ import { Region, Locale } from "../client/collections";
12
+ import { SortDirection } from "../client/broker";
13
+ import { PaginatedResponse } from "../client/capital_increase";
14
+
15
+ const mockNewsHighlightsResponse: NewsHighlights = {
16
+ consumer: ["news1", "news2"],
17
+ energyAndUtilities: ["news3"],
18
+ finance: ["news4", "news5"],
19
+ healthcare: ["news6"],
20
+ industrialsAndMaterials: ["news7"],
21
+ tech: ["news8"],
22
+ other: ["news9"]
23
+ };
24
+
25
+ const mockNewsResponse: News[] = [
26
+ {
27
+ url: "https://example.com/news1",
28
+ imageUrl: "https://example.com/image1.jpg",
29
+ timestamp: "2024-03-14T10:00:00Z",
30
+ publisherUrl: "https://example.com",
31
+ publisher: {
32
+ name: "Example Publisher",
33
+ logoUrl: "https://example.com/logo.png"
34
+ },
35
+ relatedTickers: [
36
+ {
37
+ id: "1",
38
+ name: "Ticker 1",
39
+ symbol: "TCK1"
40
+ }
41
+ ],
42
+ qualityScore: 85,
43
+ createdAt: "2024-03-14T09:00:00Z"
44
+ },
45
+ {
46
+ url: "https://example.com/news2",
47
+ imageUrl: "https://example.com/image2.jpg",
48
+ timestamp: "2024-03-14T11:00:00Z",
49
+ publisherUrl: "https://example.com",
50
+ publisher: {
51
+ name: "Example Publisher 2"
52
+ },
53
+ relatedTickers: [],
54
+ qualityScore: 90,
55
+ createdAt: "2024-03-14T10:00:00Z"
56
+ }
57
+ ];
58
+
59
+ const mockPaginatedNewsResponse: PaginatedResponse<News> = {
60
+ recordCount: 2,
61
+ items: mockNewsResponse
62
+ };
63
+
64
+ describe("News Client", () => {
65
+ let client: NewsClient;
66
+
67
+ beforeAll(() => {
68
+ const config = (global as any).testSuite.config as LaplaceConfiguration;
69
+ const logger: Logger = {
70
+ info: jest.fn(),
71
+ error: jest.fn(),
72
+ warn: jest.fn(),
73
+ debug: jest.fn(),
74
+ } as unknown as Logger;
75
+
76
+ client = new NewsClient(config, logger);
77
+ });
78
+
79
+ describe("Integration Tests", () => {
80
+ describe("getHighlights", () => {
81
+ test("should return news highlights for region and locale", async () => {
82
+ const resp = await client.getHighlights(Region.Tr, Locale.Tr);
83
+
84
+ expect(resp).toBeDefined();
85
+ expect(Array.isArray(resp.consumer)).toBe(true);
86
+ expect(Array.isArray(resp.energyAndUtilities)).toBe(true);
87
+ expect(Array.isArray(resp.finance)).toBe(true);
88
+ expect(Array.isArray(resp.healthcare)).toBe(true);
89
+ expect(Array.isArray(resp.industrialsAndMaterials)).toBe(true);
90
+ expect(Array.isArray(resp.tech)).toBe(true);
91
+ expect(Array.isArray(resp.other)).toBe(true);
92
+ });
93
+ });
94
+
95
+ describe("getNews", () => {
96
+ test("should return paginated news list", async () => {
97
+ const resp = await client.getNews(
98
+ Region.Tr,
99
+ Locale.Tr,
100
+ NewsType.BRIEFS,
101
+ 1,
102
+ 10,
103
+ NewsOrderBy.TIMESTAMP,
104
+ SortDirection.Desc,
105
+ null
106
+ );
107
+
108
+ expect(resp).toBeDefined();
109
+ expect(typeof resp.recordCount).toBe("number");
110
+ expect(Array.isArray(resp.items)).toBe(true);
111
+
112
+ if (resp.items.length > 0) {
113
+ const firstNews = resp.items[0];
114
+ expect(typeof firstNews.url).toBe("string");
115
+ expect(typeof firstNews.timestamp).toBe("string");
116
+ expect(typeof firstNews.publisher).toBe("object");
117
+ expect(Array.isArray(firstNews.relatedTickers)).toBe(true);
118
+ }
119
+ });
120
+ });
121
+ });
122
+
123
+ describe("Mock Tests", () => {
124
+ beforeEach(() => {
125
+ jest.clearAllMocks();
126
+ });
127
+
128
+ describe("getHighlights", () => {
129
+ test("should return news highlights with mock data", async () => {
130
+ jest.spyOn(client, 'getHighlights').mockResolvedValue(mockNewsHighlightsResponse);
131
+
132
+ const resp = await client.getHighlights(Region.Tr, Locale.Tr);
133
+
134
+ expect(resp).toBeDefined();
135
+ expect(resp.consumer).toHaveLength(2);
136
+ expect(resp.energyAndUtilities).toHaveLength(1);
137
+ expect(resp.finance).toHaveLength(2);
138
+ expect(resp.tech).toHaveLength(1);
139
+
140
+ expect(client.getHighlights).toHaveBeenCalledWith(Region.Tr, Locale.Tr);
141
+ });
142
+
143
+ test("should handle API errors for highlights", async () => {
144
+ jest.spyOn(client, 'getHighlights').mockRejectedValue(new Error("Failed to fetch highlights"));
145
+
146
+ await expect(client.getHighlights(Region.Tr, Locale.Tr))
147
+ .rejects.toThrow("Failed to fetch highlights");
148
+ });
149
+ });
150
+
151
+ describe("getNews", () => {
152
+ test("should return paginated news with mock data", async () => {
153
+ jest.spyOn(client, 'getNews').mockResolvedValue(mockPaginatedNewsResponse);
154
+
155
+ const resp = await client.getNews(
156
+ Region.Tr,
157
+ Locale.Tr,
158
+ NewsType.BRIEFS,
159
+ 1,
160
+ 10,
161
+ NewsOrderBy.TIMESTAMP,
162
+ SortDirection.Desc,
163
+ null
164
+ );
165
+
166
+ expect(resp.items).toHaveLength(2);
167
+ expect(resp.recordCount).toBe(2);
168
+
169
+ const firstNews = resp.items[0];
170
+ expect(firstNews.url).toBe("https://example.com/news1");
171
+ expect(firstNews.publisher.name).toBe("Example Publisher");
172
+ expect(firstNews.relatedTickers).toHaveLength(1);
173
+ expect(firstNews.qualityScore).toBe(85);
174
+
175
+ expect(client.getNews).toHaveBeenCalledWith(
176
+ Region.Tr,
177
+ Locale.Tr,
178
+ NewsType.BRIEFS,
179
+ 1,
180
+ 10,
181
+ NewsOrderBy.TIMESTAMP,
182
+ SortDirection.Desc,
183
+ null
184
+ );
185
+ });
186
+
187
+ test("should handle API errors for news", async () => {
188
+ jest.spyOn(client, 'getNews').mockRejectedValue(new Error("Failed to fetch news"));
189
+
190
+ await expect(client.getNews(
191
+ Region.Tr,
192
+ Locale.Tr,
193
+ NewsType.BRIEFS,
194
+ 1,
195
+ 10,
196
+ NewsOrderBy.TIMESTAMP,
197
+ SortDirection.Desc,
198
+ null
199
+ )).rejects.toThrow("Failed to fetch news");
200
+ });
201
+ });
202
+ });
203
+ });
204
+