laplace-api 4.5.0 → 4.7.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 +1 -1
- package/src/client/live-price-web-socket.ts +27 -0
- package/src/client/news.ts +152 -0
- package/src/test/news.test.ts +204 -0
package/package.json
CHANGED
|
@@ -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,152 @@
|
|
|
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
|
+
translations?: NewsTranslation;
|
|
38
|
+
categories?: NewsCategories;
|
|
39
|
+
sectors?: NewsSector;
|
|
40
|
+
content?: NewsContent;
|
|
41
|
+
industries?: NewsIndustry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NewsTranslation {
|
|
45
|
+
title: string;
|
|
46
|
+
description: string;
|
|
47
|
+
content: string;
|
|
48
|
+
summary: string;
|
|
49
|
+
summaryParsed: string[];
|
|
50
|
+
investorInsight: string;
|
|
51
|
+
language: string;
|
|
52
|
+
originalLanguage: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface NewsPublisher {
|
|
56
|
+
name: string;
|
|
57
|
+
logoUrl?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface NewsTicker {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
symbol?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface NewsCategories {
|
|
67
|
+
name: string;
|
|
68
|
+
newsCount: number;
|
|
69
|
+
categoryType?: string;
|
|
70
|
+
meanType?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface NewsSector {
|
|
74
|
+
name: string;
|
|
75
|
+
newsCount: number;
|
|
76
|
+
categoryType?: string;
|
|
77
|
+
meanType?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface NewsContent {
|
|
81
|
+
title: string;
|
|
82
|
+
description: string;
|
|
83
|
+
content: string[];
|
|
84
|
+
summary: string[];
|
|
85
|
+
investorInsight: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface NewsIndustry {
|
|
89
|
+
name: string;
|
|
90
|
+
meanType: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class NewsClient extends Client {
|
|
94
|
+
async getHighlights(region: Region, locale: Locale): Promise<NewsHighlights> {
|
|
95
|
+
const url = new URL(
|
|
96
|
+
`${this["baseUrl"]}/api/v1/news/highlights`,
|
|
97
|
+
);
|
|
98
|
+
url.searchParams.append("region", region);
|
|
99
|
+
url.searchParams.append("locale", locale);
|
|
100
|
+
|
|
101
|
+
return this.sendRequest<NewsHighlights>({
|
|
102
|
+
method: "GET",
|
|
103
|
+
url: url.toString(),
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getNews(
|
|
108
|
+
region: Region,
|
|
109
|
+
locale: Locale,
|
|
110
|
+
newsType: NewsType | null,
|
|
111
|
+
page: number | null,
|
|
112
|
+
size: number | null,
|
|
113
|
+
orderBy: NewsOrderBy | null,
|
|
114
|
+
orderByDirection: SortDirection | null,
|
|
115
|
+
extraFilters: string | null,
|
|
116
|
+
): Promise<PaginatedResponse<News>> {
|
|
117
|
+
const url = new URL(
|
|
118
|
+
`${this["baseUrl"]}/api/v1/news`,
|
|
119
|
+
);
|
|
120
|
+
url.searchParams.append("region", region);
|
|
121
|
+
url.searchParams.append("locale", locale);
|
|
122
|
+
|
|
123
|
+
if (newsType) {
|
|
124
|
+
url.searchParams.append("newsType", newsType);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (page) {
|
|
128
|
+
url.searchParams.append("page", page.toString());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (size) {
|
|
132
|
+
url.searchParams.append("size", size.toString());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (orderBy) {
|
|
136
|
+
url.searchParams.append("orderBy", orderBy);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (orderByDirection) {
|
|
140
|
+
url.searchParams.append("orderByDirection", orderByDirection);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (extraFilters) {
|
|
144
|
+
url.searchParams.append("extraFilters", extraFilters);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return this.sendRequest<PaginatedResponse<News>>({
|
|
148
|
+
method: "GET",
|
|
149
|
+
url: url.toString(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -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
|
+
|