laplace-api 3.1.0 → 4.1.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.
@@ -1,3 +1,30 @@
1
+ import { Logger } from "winston";
2
+ import { LaplaceConfiguration } from "../utilities/configuration";
3
+ import { Client } from "./client";
4
+
5
+ interface WebSocketUrlResponse {
6
+ url: string;
7
+ }
8
+
9
+ interface WebSocketUrlParams {
10
+ externalUserId: string;
11
+ feeds: LivePriceFeed[];
12
+ }
13
+
14
+ export enum AccessorType {
15
+ User = "user",
16
+ }
17
+
18
+ interface UpdateUserDetailsParams {
19
+ externalUserID: string;
20
+ firstName?: string;
21
+ lastName?: string;
22
+ address?: string;
23
+ city?: string;
24
+ countryCode?: string;
25
+ accessorType?: AccessorType;
26
+ active: boolean;
27
+ }
1
28
 
2
29
  interface RawBISTStockLiveData {
3
30
  _id: number;
@@ -88,7 +115,7 @@ export class WebSocketError extends Error {
88
115
  }
89
116
  }
90
117
 
91
- export class LivePriceWebSocketClient {
118
+ export class LivePriceWebSocketClient extends Client {
92
119
  private ws: WebSocket | null = null;
93
120
  private subscriptionCounter = 0;
94
121
  private subscriptions = new Map<
@@ -99,7 +126,10 @@ export class LivePriceWebSocketClient {
99
126
  feed: LivePriceFeed;
100
127
  }
101
128
  >();
102
- private symbolLastData = new Map<string, BISTStockLiveData | USStockLiveData>();
129
+ private symbolLastData = new Map<
130
+ string,
131
+ BISTStockLiveData | USStockLiveData
132
+ >();
103
133
  private reconnectAttempts = 0;
104
134
  private reconnectTimeout: NodeJS.Timeout | null = null;
105
135
  private isClosed: boolean = false;
@@ -110,8 +140,19 @@ export class LivePriceWebSocketClient {
110
140
  private lastMessageTimestamp: number = 0;
111
141
  private inactivityCheckInterval: NodeJS.Timeout | null = null;
112
142
  private readonly INACTIVITY_TIMEOUT = 15000;
143
+ private externalUserId: string | null = null;
144
+ private feeds: LivePriceFeed[] | null = null;
113
145
 
114
- constructor(options: WebSocketOptions = {}) {
146
+ constructor(
147
+ feeds: LivePriceFeed[],
148
+ externalUserId: string,
149
+ cfg: LaplaceConfiguration,
150
+ logger: Logger,
151
+ options: WebSocketOptions = {}
152
+ ) {
153
+ super(cfg, logger);
154
+ this.feeds = feeds;
155
+ this.externalUserId = externalUserId;
115
156
  this.options = {
116
157
  enableLogging: true,
117
158
  logLevel: LogLevel.Error,
@@ -128,17 +169,17 @@ export class LivePriceWebSocketClient {
128
169
  clearInterval(this.inactivityCheckInterval);
129
170
  this.inactivityCheckInterval = null;
130
171
  }
131
-
132
- this.inactivityCheckInterval = setInterval(async ()=> {
172
+
173
+ this.inactivityCheckInterval = setInterval(async () => {
133
174
  if (Date.now() - this.lastMessageTimestamp >= this.INACTIVITY_TIMEOUT) {
134
175
  this.stopInactivityInterval();
135
176
  try {
136
177
  this.attemptReconnect();
137
- } catch(error) {
178
+ } catch (error) {
138
179
  this.log(`Failed to reconnect: ${error}`, "error");
139
180
  }
140
181
  }
141
- }, this.INACTIVITY_TIMEOUT)
182
+ }, this.INACTIVITY_TIMEOUT);
142
183
  }
143
184
 
144
185
  private stopInactivityInterval() {
@@ -175,12 +216,43 @@ export class LivePriceWebSocketClient {
175
216
  }
176
217
  }
177
218
 
178
- async connect(url: string): Promise<WebSocket> {
219
+ async updateUserDetails(params: UpdateUserDetailsParams): Promise<void> {
220
+ const url = new URL(`${this["baseUrl"]}/api/v1/ws/user`);
221
+
222
+ await this.sendRequest<void>({
223
+ method: "PUT",
224
+ url: url.toString(),
225
+ data: params,
226
+ });
227
+ }
228
+
229
+ private async getWebSocketUrl(): Promise<string> {
230
+ const url = new URL(`${this["baseUrl"]}/api/v2/ws/url`);
231
+
232
+ const params: WebSocketUrlParams = {
233
+ externalUserId: this.externalUserId!,
234
+ feeds: this.feeds!,
235
+ };
236
+
237
+ const response = await this.sendRequest<WebSocketUrlResponse>({
238
+ method: "POST",
239
+ url: url.toString(),
240
+ data: params,
241
+ });
242
+
243
+ return response.url;
244
+ }
245
+
246
+ async connect(url?: string): Promise<WebSocket> {
179
247
  this.log("Connecting to WebSocket...");
180
- this.wsUrl = url;
248
+ if (!this.externalUserId || !this.feeds) {
249
+ throw new Error("External user ID and feeds are required");
250
+ }
251
+
252
+ this.wsUrl = url || (await this.getWebSocketUrl());
181
253
 
182
254
  if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
183
- this.ws = new WebSocket(url);
255
+ this.ws = new WebSocket(this.wsUrl);
184
256
  this.connectPromise = this.setupWebSocket();
185
257
 
186
258
  await this.connectPromise;
@@ -419,7 +491,8 @@ export class LivePriceWebSocketClient {
419
491
  if (symbolHandlers.length === 1) {
420
492
  symbolsToAdd.push(symbol);
421
493
  } else if (symbolHandlers.length > 1) {
422
- const lastData: BISTStockLiveData | USStockLiveData | undefined = this.symbolLastData.get(symbol);
494
+ const lastData: BISTStockLiveData | USStockLiveData | undefined =
495
+ this.symbolLastData.get(symbol);
423
496
  if (lastData) {
424
497
  typedHandler(lastData);
425
498
  }
@@ -1,83 +1,235 @@
1
1
  import { Client } from "./client";
2
2
  import { Region } from "./collections";
3
- import { v4 as uuidv4 } from 'uuid';
4
- import { LivePriceFeed } from "./live-price-web-socket";
3
+ import { v4 as uuidv4 } from "uuid";
5
4
 
6
- interface WebSocketUrlResponse {
7
- url: string;
8
- }
5
+ export type MessageType = "pr" | "state_change" | "heartbeat" | "ob";
9
6
 
10
- interface WebSocketUrlParams {
11
- externalUserId: string;
12
- feeds: LivePriceFeed[];
7
+ // Stream Message Wrapper - v2 formatter wraps all messages in this structure
8
+ export interface StreamMessage<T> {
9
+ t: MessageType;
10
+ d: T;
13
11
  }
14
12
 
15
- export interface BISTStockLiveData {
16
- s: string; // Symbol
13
+ export interface BISTStockPriceData {
14
+ s: string; // Symbol
17
15
  ch: number; // DailyPercentChange
18
16
  p: number; // ClosePrice
17
+ d: number; // Date
18
+ }
19
+
20
+ export interface USStockPriceData {
21
+ s: string; // Symbol
22
+ p: number; // Price
23
+ d: number; // Date
24
+ }
25
+
26
+ export type BISTStockStreamData = StreamMessage<BISTStockPriceData>;
27
+ export type USStockStreamData = StreamMessage<USStockPriceData>;
28
+
29
+ export enum OrderbookLevelSide {
30
+ Bid = "bid",
31
+ Ask = "ask"
32
+ }
33
+
34
+ export interface OrderbookLevel {
35
+ level: number;
36
+ vol: number;
37
+ orders: number;
38
+ p: number;
39
+ side: OrderbookLevelSide;
40
+ }
41
+
42
+ export interface OrderbookDeletedLevel {
43
+ level: number;
44
+ side: OrderbookLevelSide;
45
+ }
46
+
47
+ export interface OrderbookLiveData {
48
+ updated?: OrderbookLevel[];
49
+ deleted?: OrderbookDeletedLevel[];
50
+ symbol: string;
51
+ }
52
+
53
+ export enum PriceDataType {
54
+ Live = "live",
55
+ Delayed = "delayed",
56
+ Orderbook = "orderbook",
57
+ }
58
+
59
+ export interface ILivePriceClient<T> {
60
+ close(): void;
61
+ receive(): AsyncIterable<T>;
62
+ subscribe(symbols: string[]): Promise<void>;
63
+ }
64
+
65
+ class LivePriceClientImpl<T> implements ILivePriceClient<T> {
66
+ private client: Client;
67
+ private region: Region;
68
+ private dataType: PriceDataType;
69
+ private symbols: string[] = [];
70
+ private closed = false;
71
+ private currentStream: AsyncIterable<T> | null = null;
72
+ private cancelFn: (() => void) | null = null;
73
+
74
+ constructor(client: Client, region: Region, dataType: PriceDataType) {
75
+ this.client = client;
76
+ this.region = region;
77
+ this.dataType = dataType;
78
+ }
79
+
80
+ close(): void {
81
+ if (this.closed) return;
82
+
83
+ this.closed = true;
84
+ if (this.cancelFn) {
85
+ this.cancelFn();
86
+ }
87
+ }
88
+
89
+ receive(): AsyncIterable<T> {
90
+ if (!this.currentStream) {
91
+ throw new Error("Not subscribed. Call subscribe() first.");
92
+ }
93
+ return this.currentStream;
94
+ }
95
+
96
+ async subscribe(symbols: string[]): Promise<void> {
97
+ // Cancel existing connection
98
+ if (this.cancelFn) {
99
+ this.cancelFn();
100
+ }
101
+
102
+ const streamId = uuidv4();
103
+ let url: string;
104
+
105
+ switch (this.dataType) {
106
+ case PriceDataType.Live:
107
+ url = `${
108
+ this.client["baseUrl"]
109
+ }/api/v2/stock/price/live?filter=${symbols.join(",")}&region=${
110
+ this.region
111
+ }&stream=${streamId}`;
112
+ break;
113
+ case PriceDataType.Delayed:
114
+ url = `${
115
+ this.client["baseUrl"]
116
+ }/api/v1/stock/price/delayed?filter=${symbols.join(",")}&region=${
117
+ this.region
118
+ }&stream=${streamId}`;
119
+ break;
120
+ case PriceDataType.Orderbook:
121
+ url = `${
122
+ this.client["baseUrl"]
123
+ }/api/v1/stock/orderbook/live?filter=${symbols.join(",")}&region=${
124
+ this.region
125
+ }&stream=${streamId}`;
126
+ break;
127
+ }
128
+
129
+ const { events, cancel } = this.client.sendSSERequest<T>(url);
130
+
131
+ this.currentStream = events;
132
+ this.cancelFn = cancel;
133
+ this.symbols = symbols;
134
+ this.closed = false;
135
+ }
19
136
  }
20
137
 
21
- export interface USStockLiveData {
22
- s: string; // Symbol
23
- bp: number; // BidPrice
24
- ap: number; // AskPrice
138
+ export function getLivePrice<T>(
139
+ client: Client,
140
+ symbols: string[],
141
+ region: Region
142
+ ): ILivePriceClient<T> {
143
+ if (!client) {
144
+ throw new Error("Client cannot be null");
145
+ }
146
+
147
+ const livePriceClient = new LivePriceClientImpl<T>(client, region, PriceDataType.Live);
148
+ livePriceClient.subscribe(symbols).catch((error) => {
149
+ console.error("Failed to initialize live price client", error);
150
+ });
151
+
152
+ return livePriceClient;
25
153
  }
26
154
 
27
- function getSSELivePrice<T>(
155
+ export function getDelayedPrice<T>(
28
156
  client: Client,
29
157
  symbols: string[],
30
158
  region: Region,
31
- streamId: string = uuidv4(),
32
- ): {
33
- events: AsyncIterable<T>,
34
- cancel: () => void
35
- } {
36
- const url = `${client["baseUrl"]}/api/v1/stock/price/live?filter=${symbols.join(',')}&region=${region}&stream=${streamId}`;
159
+ ): ILivePriceClient<T> {
160
+ if (!client) {
161
+ throw new Error("Client cannot be null");
162
+ }
163
+
164
+ const livePriceClient = new LivePriceClientImpl<T>(client, region, PriceDataType.Delayed);
165
+ livePriceClient.subscribe(symbols).catch((error) => {
166
+ console.error("Failed to initialize live price client", error);
167
+ });
168
+
169
+ return livePriceClient;
170
+ }
171
+
172
+ function getOrderbook<T>(
173
+ client: Client,
174
+ symbols: string[],
175
+ region: Region,
176
+ ): ILivePriceClient<T> {
177
+ if (!client) {
178
+ throw new Error("Client cannot be null");
179
+ }
37
180
 
38
- return client.sendSSERequest<T>(url);
181
+ const orderbookClient = new LivePriceClientImpl<T>(client, region, PriceDataType.Orderbook);
182
+ orderbookClient.subscribe(symbols).catch((error) => {
183
+ console.error("Failed to initialize orderbook client", error);
184
+ });
185
+
186
+ return orderbookClient;
187
+ }
188
+
189
+ export function getLivePriceForBIST(
190
+ client: Client,
191
+ symbols: string[]
192
+ ): ILivePriceClient<BISTStockStreamData> {
193
+ return getLivePrice<BISTStockStreamData>(client, symbols, Region.Tr);
194
+ }
195
+
196
+ export function getLivePriceForUS(
197
+ client: Client,
198
+ symbols: string[]
199
+ ): ILivePriceClient<USStockStreamData> {
200
+ return getLivePrice<USStockStreamData>(client, symbols, Region.Us);
201
+ }
202
+
203
+ export function getDelayedPriceForBIST(
204
+ client: Client,
205
+ symbols: string[]
206
+ ): ILivePriceClient<BISTStockStreamData> {
207
+ return getDelayedPrice<BISTStockStreamData>(client, symbols, Region.Tr);
208
+ }
209
+
210
+ export function getOrderbookForBIST(
211
+ client: Client,
212
+ symbols: string[]
213
+ ): ILivePriceClient<OrderbookLiveData> {
214
+ return getOrderbook<OrderbookLiveData>(client, symbols, Region.Tr);
39
215
  }
40
216
 
41
217
  export class LivePriceClient extends Client {
42
- async getWebSocketUrl(
43
- externalUserId: string,
44
- feeds: LivePriceFeed[]
45
- ): Promise<string> {
46
- const url = new URL(`${this["baseUrl"]}/api/v2/ws/url`);
47
-
48
- const params: WebSocketUrlParams = {
49
- externalUserId,
50
- feeds
51
- };
52
-
53
- const response = await this.sendRequest<WebSocketUrlResponse>({
54
- method: "POST",
55
- url: url.toString(),
56
- data: params,
57
- });
58
-
59
- return response.url;
218
+ getLivePriceForBIST(symbols: string[]): ILivePriceClient<BISTStockStreamData> {
219
+ return getLivePriceForBIST(this, symbols);
220
+ }
221
+
222
+ getLivePriceForUS(symbols: string[]): ILivePriceClient<USStockStreamData> {
223
+ return getLivePriceForUS(this, symbols);
60
224
  }
61
225
 
62
- getLivePriceForBIST(
63
- symbols: string[],
64
- region: Region,
65
- streamId?: string,
66
- ): {
67
- events: AsyncIterable<BISTStockLiveData>,
68
- cancel: () => void
69
- } {
70
- return getSSELivePrice<BISTStockLiveData>(this, symbols, region, streamId);
226
+ getDelayedPriceForBIST(
227
+ symbols: string[],
228
+ ): ILivePriceClient<BISTStockStreamData> {
229
+ return getDelayedPriceForBIST(this, symbols);
71
230
  }
72
231
 
73
- getLivePriceForUS(
74
- symbols: string[],
75
- region: Region,
76
- streamId?: string,
77
- ): {
78
- events: AsyncIterable<USStockLiveData>,
79
- cancel: () => void
80
- } {
81
- return getSSELivePrice<USStockLiveData>(this, symbols, region, streamId);
232
+ getOrderbookForBIST(symbols: string[]): ILivePriceClient<OrderbookLiveData> {
233
+ return getOrderbookForBIST(this, symbols);
82
234
  }
83
235
  }
@@ -0,0 +1,75 @@
1
+ import { Client } from "./client";
2
+
3
+ export interface Politician {
4
+ id: number;
5
+ politicianName: string;
6
+ totalHoldings: number;
7
+ lastUpdated: Date;
8
+ }
9
+
10
+ export interface Holding {
11
+ politicianName: string;
12
+ symbol: string;
13
+ company: string;
14
+ holding: string;
15
+ allocation: string;
16
+ lastUpdated: Date;
17
+ }
18
+
19
+ export interface HoldingShort {
20
+ symbol: string;
21
+ company: string;
22
+ holding: string;
23
+ allocation: string;
24
+ }
25
+
26
+ export interface TopHolding {
27
+ symbol: string;
28
+ company: string;
29
+ politicians: TopHoldingPolitician[];
30
+ count: number;
31
+ }
32
+
33
+ export interface TopHoldingPolitician {
34
+ name: string;
35
+ holding: string;
36
+ allocation: string;
37
+ }
38
+
39
+ export interface PoliticianDetail {
40
+ id: number;
41
+ name: string;
42
+ holdings: HoldingShort[];
43
+ totalHoldings: number;
44
+ lastUpdated: Date;
45
+ }
46
+
47
+ export class PoliticianClient extends Client {
48
+ async getAllPolitician(): Promise<Politician[]> {
49
+ return await this.sendRequest<Politician[]>({
50
+ method: 'GET',
51
+ url: `/api/v1/politician`,
52
+ });
53
+ }
54
+
55
+ async getPoliticianHoldingBySymbol(symbol: string): Promise<Holding[]> {
56
+ return await this.sendRequest<Holding[]>({
57
+ method: 'GET',
58
+ url: `/api/v1/holding/${symbol}`
59
+ })
60
+ }
61
+
62
+ async getAllTopHoldings(): Promise<TopHolding[]> {
63
+ return await this.sendRequest<TopHolding[]>({
64
+ method: 'GET',
65
+ url: `/api/v1/top-holding`
66
+ })
67
+ }
68
+
69
+ async getPoliticianDetail(id: number): Promise<PoliticianDetail> {
70
+ return await this.sendRequest<PoliticianDetail>({
71
+ method: 'GET',
72
+ url: `/api/v1/politician/${id}`
73
+ })
74
+ }
75
+ }
@@ -1,3 +1,4 @@
1
+ import { PaginatedResponse } from './capital_increase';
1
2
  import { Client } from './client';
2
3
  import { Region, Locale } from './collections';
3
4
  import { LaplaceHTTPError } from './errors';
@@ -64,7 +65,7 @@ export interface StockDetail extends Stock {
64
65
  shortDescription: string;
65
66
  localizedShortDescription: LocaleString;
66
67
  region: string;
67
- markets: Market[];
68
+ markets?: Market[];
68
69
  }
69
70
 
70
71
  export enum Market {
@@ -102,8 +103,10 @@ export interface StockRestriction {
102
103
  id: number;
103
104
  title: string;
104
105
  description: string;
106
+ symbol?: string;
105
107
  startDate: string;
106
108
  endDate: string;
109
+ market?: string;
107
110
  }
108
111
 
109
112
  export interface TickRule {
@@ -111,7 +114,7 @@ export interface TickRule {
111
114
  additionalPrice: number;
112
115
  lowerPriceLimit: number;
113
116
  upperPriceLimit: number;
114
- rules: TickSizeRule[];
117
+ rules: TickSizeRule[] | null;
115
118
  }
116
119
 
117
120
  export interface TickSizeRule {
@@ -120,6 +123,32 @@ export interface TickSizeRule {
120
123
  tickSize: number;
121
124
  }
122
125
 
126
+ export interface EarningsTranscriptListItem {
127
+ symbol: string;
128
+ year: number;
129
+ quarter: number;
130
+ date: string;
131
+ fiscal_year: number;
132
+ }
133
+
134
+ export interface EarningsTranscriptWithSummary {
135
+ symbol: string;
136
+ year: number;
137
+ quarter: number;
138
+ date: string;
139
+ content: string;
140
+ summary?: string;
141
+ has_summary: boolean;
142
+ }
143
+
144
+ export interface MarketState {
145
+ id: number;
146
+ marketSymbol?: string | null;
147
+ state: string;
148
+ lastTimestamp: string;
149
+ stockSymbol?: string | null;
150
+ }
151
+
123
152
  export class StockClient extends Client {
124
153
  async getAllStocks(region: Region, page: number|null = null, pageSize: number|null = null): Promise<Stock[]> {
125
154
  return this.sendRequest<Stock[]>({
@@ -188,6 +217,14 @@ export class StockClient extends Client {
188
217
  });
189
218
  }
190
219
 
220
+ async getAllStockRestrictions(region: Region): Promise<StockRestriction[]> {
221
+ return this.sendRequest<StockRestriction[]>({
222
+ method: 'GET',
223
+ url: '/api/v1/stock/restrictions/all',
224
+ params: { region },
225
+ });
226
+ }
227
+
191
228
  async getTickRules(symbol: string, region: Region): Promise<TickRule> {
192
229
  return this.sendRequest<TickRule>({
193
230
  method: 'GET',
@@ -195,4 +232,50 @@ export class StockClient extends Client {
195
232
  params: { symbol, region },
196
233
  });
197
234
  }
235
+
236
+ async getEarningsTranscripts(symbol: string, region: Region): Promise<EarningsTranscriptListItem[]> {
237
+ return this.sendRequest<EarningsTranscriptListItem[]>({
238
+ method: 'GET',
239
+ url: '/api/v1/earnings/transcripts',
240
+ params: { symbol, region },
241
+ });
242
+ }
243
+
244
+ async getEarningsTranscript(symbol: string, year: number, quarter: number): Promise<EarningsTranscriptWithSummary> {
245
+ return this.sendRequest<EarningsTranscriptWithSummary>({
246
+ method: 'GET',
247
+ url: '/api/v1/earnings/transcript',
248
+ params: { symbol, year, quarter },
249
+ });
250
+ }
251
+
252
+ async getStockStateAll(page: number, size: number, region: Region): Promise<PaginatedResponse<MarketState>> {
253
+ return this.sendRequest<PaginatedResponse<MarketState>>({
254
+ method: 'GET',
255
+ url: '/api/v1/state/stock/all',
256
+ params: { page, size, region },
257
+ });
258
+ }
259
+
260
+ async getStockState(symbol: string): Promise<MarketState> {
261
+ return this.sendRequest<MarketState>({
262
+ method: 'GET',
263
+ url: `/api/v1/state/stock/${symbol}`,
264
+ });
265
+ }
266
+
267
+ async getStateAll(page: number, size: number, region: Region): Promise<PaginatedResponse<MarketState>> {
268
+ return this.sendRequest<PaginatedResponse<MarketState>>({
269
+ method: 'GET',
270
+ url: '/api/v1/state/all',
271
+ params: { page, size, region },
272
+ });
273
+ }
274
+
275
+ async getState(symbol: string): Promise<MarketState> {
276
+ return this.sendRequest<MarketState>({
277
+ method: 'GET',
278
+ url: `/api/v1/state/${symbol}`,
279
+ });
280
+ }
198
281
  }