laplace-api 1.0.0 → 1.0.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/jest.config.js +1 -0
- package/package.json +5 -1
- package/src/client/client.ts +85 -0
- package/src/client/financial_fundamentals.ts +81 -0
- package/src/client/live_price.ts +49 -0
- package/{test → src/test}/client_test_suite.ts +1 -1
- package/src/test/financial_fundamentals.test.ts +52 -0
- package/src/test/live_price.test.ts +58 -0
- package/tsconfig.json +7 -3
- package/client/client.ts +0 -41
- /package/{client → src/client}/collections.ts +0 -0
- /package/{client → src/client}/search.ts +0 -0
- /package/{client → src/client}/stocks.ts +0 -0
- /package/{jest.setup.ts → src/jest.setup.ts} +0 -0
- /package/{test → src/test}/client.test.ts +0 -0
- /package/{test → src/test}/collections.test.ts +0 -0
- /package/{test → src/test}/search.test.ts +0 -0
- /package/{test → src/test}/stocks.test.ts +0 -0
- /package/{utilities → src/utilities}/configuration.ts +0 -0
- /package/{utilities → src/utilities}/fs.ts +0 -0
- /package/{utilities → src/utilities}/test.env +0 -0
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "laplace-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.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": {
|
|
@@ -35,14 +35,18 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/Laplace-Analytics/laplace-api-js#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@types/uuid": "^10.0.0",
|
|
38
39
|
"axios": "^1.7.7",
|
|
39
40
|
"dotenv": "^16.4.5",
|
|
40
41
|
"envalid": "^8.0.0",
|
|
42
|
+
"event-source-polyfill": "^1.0.31",
|
|
41
43
|
"mongodb": "^6.8.0",
|
|
44
|
+
"uuid": "^10.0.0",
|
|
42
45
|
"winston": "^3.14.2"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@types/axios": "^0.14.0",
|
|
49
|
+
"@types/event-source-polyfill": "^1.0.5",
|
|
46
50
|
"@types/jest": "^29.5.12",
|
|
47
51
|
"@types/mongodb": "^4.0.7",
|
|
48
52
|
"@types/node": "^22.5.4",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { Logger } from 'winston';
|
|
3
|
+
import { LaplaceConfiguration } from '../utilities/configuration';
|
|
4
|
+
import { EventSourcePolyfill } from 'event-source-polyfill';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export class Client {
|
|
8
|
+
private cli: AxiosInstance;
|
|
9
|
+
private baseUrl: string;
|
|
10
|
+
private apiKey: string;
|
|
11
|
+
private logger: Logger;
|
|
12
|
+
|
|
13
|
+
constructor(cfg: LaplaceConfiguration, logger: Logger) {
|
|
14
|
+
this.cli = axios.create();
|
|
15
|
+
this.baseUrl = cfg.baseURL;
|
|
16
|
+
this.apiKey = cfg.apiKey;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async sendRequest<T>(config: AxiosRequestConfig): Promise<T> {
|
|
21
|
+
try {
|
|
22
|
+
const response = await this.cli.request<T>({
|
|
23
|
+
...config,
|
|
24
|
+
baseURL: this.baseUrl,
|
|
25
|
+
headers: {
|
|
26
|
+
...config.headers,
|
|
27
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return response.data;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
34
|
+
throw new Error(`Unexpected status code: ${error.response.status}, body: ${JSON.stringify(error.response.data)}`);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
sendSSERequest<T>(url: string): { events: AsyncIterable<T>, cancel: () => void } {
|
|
41
|
+
const eventSource = new EventSourcePolyfill(url, {
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: 'text/event-stream',
|
|
44
|
+
'Cache-Control': 'no-cache',
|
|
45
|
+
Connection: 'keep-alive',
|
|
46
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const events: AsyncIterable<T> = {
|
|
51
|
+
async *[Symbol.asyncIterator]() {
|
|
52
|
+
try {
|
|
53
|
+
while (true) {
|
|
54
|
+
yield await new Promise<T>((resolve, reject) => {
|
|
55
|
+
eventSource.onmessage = (event) => {
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(event.data);
|
|
58
|
+
resolve(data);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
reject(new Error(`Error parsing event data: ${error}`));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
eventSource.onerror = (event) => {
|
|
65
|
+
reject(new Error(`SSE error: ${JSON.stringify(event)}`));
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
eventSource.close();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const cancel = () => {
|
|
76
|
+
eventSource.close();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return { events, cancel };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createClient(cfg: LaplaceConfiguration, logger: Logger): Client {
|
|
84
|
+
return new Client(cfg, logger);
|
|
85
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Client } from './client';
|
|
2
|
+
import { Region } from './collections';
|
|
3
|
+
|
|
4
|
+
export interface StockDividend {
|
|
5
|
+
date: Date;
|
|
6
|
+
dividendAmount: number;
|
|
7
|
+
dividendRatio: number;
|
|
8
|
+
netDividendAmount: number;
|
|
9
|
+
netDividendRatio: number;
|
|
10
|
+
priceThen: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StockStats {
|
|
14
|
+
previousClose: number;
|
|
15
|
+
ytdReturn: number;
|
|
16
|
+
yearlyReturn: number;
|
|
17
|
+
marketCap: number;
|
|
18
|
+
peRatio: number;
|
|
19
|
+
pbRatio: number;
|
|
20
|
+
yearLow: number;
|
|
21
|
+
yearHigh: number;
|
|
22
|
+
threeYear: number;
|
|
23
|
+
fiveYear: number;
|
|
24
|
+
symbol: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export enum StockStatsKey {
|
|
28
|
+
PreviousClose = 'previous_close',
|
|
29
|
+
YtdReturn = 'ytd_return',
|
|
30
|
+
YearlyReturn = 'yearly_return',
|
|
31
|
+
MarketCap = 'market_cap',
|
|
32
|
+
FK = 'fk',
|
|
33
|
+
PDDD = 'pddd',
|
|
34
|
+
YearLow = 'year_low',
|
|
35
|
+
YearHigh = 'year_high',
|
|
36
|
+
ThreeYearReturn = '3_year_return',
|
|
37
|
+
FiveYearReturn = '5_year_return',
|
|
38
|
+
LatestPrice = 'latest_price'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TopMover {
|
|
42
|
+
symbol: string;
|
|
43
|
+
percentChange: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class FinancialFundamentalsClient {
|
|
47
|
+
constructor(private client: Client) {}
|
|
48
|
+
|
|
49
|
+
async getStockDividends(symbol: string, region: Region): Promise<StockDividend[]> {
|
|
50
|
+
const url = new URL(`${this.client['baseUrl']}/api/v1/stock/dividends`);
|
|
51
|
+
url.searchParams.append('symbol', symbol);
|
|
52
|
+
url.searchParams.append('region', region);
|
|
53
|
+
|
|
54
|
+
return this.client.sendRequest<StockDividend[]>({
|
|
55
|
+
method: 'GET',
|
|
56
|
+
url: url.toString(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getStockStats(symbols: string[], keys: StockStatsKey[], region: Region): Promise<StockStats[]> {
|
|
61
|
+
const url = new URL(`${this.client['baseUrl']}/api/v1/stock/stats`);
|
|
62
|
+
url.searchParams.append('symbols', symbols.join(','));
|
|
63
|
+
url.searchParams.append('region', region);
|
|
64
|
+
url.searchParams.append('keys', keys.join(','));
|
|
65
|
+
|
|
66
|
+
return this.client.sendRequest<StockStats[]>({
|
|
67
|
+
method: 'GET',
|
|
68
|
+
url: url.toString(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getTopMovers(region: Region): Promise<TopMover[]> {
|
|
73
|
+
const url = new URL(`${this.client['baseUrl']}/api/v1/stock/top-movers`);
|
|
74
|
+
url.searchParams.append('region', region);
|
|
75
|
+
|
|
76
|
+
return this.client.sendRequest<TopMover[]>({
|
|
77
|
+
method: 'GET',
|
|
78
|
+
url: url.toString(),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Client } from './client';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
export enum Region {
|
|
5
|
+
BIST = 'BIST',
|
|
6
|
+
US = 'US',
|
|
7
|
+
// Add other regions as needed
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function* getLivePrice<T>(
|
|
11
|
+
client: Client,
|
|
12
|
+
symbols: string[],
|
|
13
|
+
region: Region
|
|
14
|
+
): AsyncGenerator<T, void, undefined> {
|
|
15
|
+
const streamId = uuidv4();
|
|
16
|
+
const url = `${client['baseUrl']}/api/v1/stock/price/live?filter=${symbols.join(',')}®ion=${region}&stream=${streamId}`;
|
|
17
|
+
|
|
18
|
+
const { events, cancel } = client.sendSSERequest<T>(url);
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
yield* events;
|
|
22
|
+
} finally {
|
|
23
|
+
cancel();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BISTStockLiveData {
|
|
28
|
+
s: string; // Symbol
|
|
29
|
+
ch: number; // DailyPercentChange
|
|
30
|
+
p: number; // ClosePrice
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface USStockLiveData {
|
|
34
|
+
s: string; // Symbol
|
|
35
|
+
bp: number; // BidPrice
|
|
36
|
+
ap: number; // AskPrice
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class LivePriceClient {
|
|
40
|
+
constructor(private client: Client) {}
|
|
41
|
+
|
|
42
|
+
getLivePriceForBIST(symbols: string[], region: Region): AsyncGenerator<BISTStockLiveData, void, undefined> {
|
|
43
|
+
return getLivePrice<BISTStockLiveData>(this.client, symbols, region);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getLivePriceForUS(symbols: string[], region: Region): AsyncGenerator<USStockLiveData, void, undefined> {
|
|
47
|
+
return getLivePrice<USStockLiveData>(this.client, symbols, region);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -3,7 +3,7 @@ import * as dotenv from 'dotenv';
|
|
|
3
3
|
import { findModuleRoot } from '../utilities/fs';
|
|
4
4
|
import { loadGlobal, LaplaceConfiguration } from '../utilities/configuration';
|
|
5
5
|
|
|
6
|
-
const testConfig = './utilities/test.env';
|
|
6
|
+
const testConfig = './src/utilities/test.env';
|
|
7
7
|
|
|
8
8
|
class ClientTestSuite {
|
|
9
9
|
config: LaplaceConfiguration | null = null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Logger } from 'winston';
|
|
2
|
+
import { LaplaceConfiguration } from '../utilities/configuration';
|
|
3
|
+
import { Client, createClient } from '../client/client';
|
|
4
|
+
import { FinancialFundamentalsClient } from '../client/financial_fundamentals';
|
|
5
|
+
import { Region } from '../client/collections';
|
|
6
|
+
import { StockStatsKey } from '../client/financial_fundamentals';
|
|
7
|
+
import './client_test_suite';
|
|
8
|
+
|
|
9
|
+
describe('FinancialFundamentals', () => {
|
|
10
|
+
let client: Client;
|
|
11
|
+
let stockClient: FinancialFundamentalsClient;
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
const config = (global as any).testSuite.config as LaplaceConfiguration;
|
|
15
|
+
const logger: Logger = {
|
|
16
|
+
info: jest.fn(),
|
|
17
|
+
error: jest.fn(),
|
|
18
|
+
warn: jest.fn(),
|
|
19
|
+
debug: jest.fn(),
|
|
20
|
+
} as unknown as Logger;
|
|
21
|
+
|
|
22
|
+
client = createClient(config, logger);
|
|
23
|
+
stockClient = new FinancialFundamentalsClient(client);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('GetStockDividends', async () => {
|
|
27
|
+
const resp = await stockClient.getStockDividends('TUPRS', Region.Tr);
|
|
28
|
+
expect(resp).not.toBeEmpty();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('GetStockStats', async () => {
|
|
32
|
+
const resp = await stockClient.getStockStats(['TUPRS'], [
|
|
33
|
+
StockStatsKey.PreviousClose,
|
|
34
|
+
StockStatsKey.YtdReturn,
|
|
35
|
+
StockStatsKey.YearlyReturn,
|
|
36
|
+
StockStatsKey.MarketCap,
|
|
37
|
+
StockStatsKey.FK,
|
|
38
|
+
StockStatsKey.PDDD,
|
|
39
|
+
StockStatsKey.YearLow,
|
|
40
|
+
StockStatsKey.YearHigh,
|
|
41
|
+
StockStatsKey.ThreeYearReturn,
|
|
42
|
+
StockStatsKey.FiveYearReturn,
|
|
43
|
+
StockStatsKey.LatestPrice,
|
|
44
|
+
], Region.Tr);
|
|
45
|
+
expect(resp).not.toBeEmpty();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('GetTopMovers', async () => {
|
|
49
|
+
const resp = await stockClient.getTopMovers(Region.Tr);
|
|
50
|
+
expect(resp).not.toBeEmpty();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Logger } from 'winston';
|
|
2
|
+
import { LaplaceConfiguration } from '../utilities/configuration';
|
|
3
|
+
import { Client, createClient } from '../client/client';
|
|
4
|
+
import { LivePriceClient, Region, BISTStockLiveData, USStockLiveData } from '../client/live_price';
|
|
5
|
+
import './client_test_suite';
|
|
6
|
+
|
|
7
|
+
describe('LivePrice', () => {
|
|
8
|
+
let client: Client;
|
|
9
|
+
let livePriceClient: LivePriceClient;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
const config = (global as any).testSuite.config as LaplaceConfiguration;
|
|
13
|
+
const logger: Logger = {
|
|
14
|
+
info: jest.fn(),
|
|
15
|
+
error: jest.fn(),
|
|
16
|
+
warn: jest.fn(),
|
|
17
|
+
debug: jest.fn(),
|
|
18
|
+
} as unknown as Logger;
|
|
19
|
+
|
|
20
|
+
client = createClient(config, logger);
|
|
21
|
+
livePriceClient = new LivePriceClient(client);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const testLivePrice = async (
|
|
25
|
+
symbols: string[],
|
|
26
|
+
region: Region,
|
|
27
|
+
getLivePriceFunc: (symbols: string[], region: Region) => AsyncGenerator<BISTStockLiveData | USStockLiveData, void, unknown>
|
|
28
|
+
) => {
|
|
29
|
+
const livePriceGenerator = getLivePriceFunc.call(livePriceClient, symbols, region);
|
|
30
|
+
let livePriceCount = 0;
|
|
31
|
+
|
|
32
|
+
jest.setTimeout(10000); // 10 seconds timeout
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
for await (const livePrice of livePriceGenerator) {
|
|
36
|
+
expect(livePrice).not.toBeEmpty();
|
|
37
|
+
livePriceCount++;
|
|
38
|
+
if (livePriceCount > 3) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Error occurred during live price retrieval: ${error}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
expect(livePriceCount).toBeGreaterThan(0);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
test('BISTLivePrice', async () => {
|
|
50
|
+
const symbols = ['TUPRS', 'SASA', 'THYAO', 'GARAN', 'YKBN'];
|
|
51
|
+
await testLivePrice(symbols, Region.BIST, livePriceClient.getLivePriceForBIST);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('USLivePrice', async () => {
|
|
55
|
+
const symbols = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'META'];
|
|
56
|
+
await testLivePrice(symbols, Region.US, livePriceClient.getLivePriceForUS);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
"strict": true,
|
|
7
7
|
"esModuleInterop": true,
|
|
8
8
|
"types": ["jest", "node"],
|
|
9
|
-
"
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"typeRoots": ["./node_modules/@types", "./src/types"]
|
|
10
11
|
},
|
|
11
|
-
"include": ["src/**/*", "jest.setup.ts"],
|
|
12
|
-
"exclude": ["node_modules", "**/*.spec.ts"]
|
|
12
|
+
"include": ["src/**/*", "src/jest.setup.ts"],
|
|
13
|
+
"exclude": ["node_modules", "**/*.spec.ts"],
|
|
14
|
+
"paths": {
|
|
15
|
+
"@/*": ["src/*"]
|
|
16
|
+
}
|
|
13
17
|
}
|
package/client/client.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
2
|
-
import { Logger } from 'winston';
|
|
3
|
-
import { LaplaceConfiguration } from '../utilities/configuration';
|
|
4
|
-
|
|
5
|
-
export class Client {
|
|
6
|
-
private cli: AxiosInstance;
|
|
7
|
-
private baseUrl: string;
|
|
8
|
-
private apiKey: string;
|
|
9
|
-
private logger: Logger;
|
|
10
|
-
|
|
11
|
-
constructor(cfg: LaplaceConfiguration, logger: Logger) {
|
|
12
|
-
this.cli = axios.create();
|
|
13
|
-
this.baseUrl = cfg.baseURL;
|
|
14
|
-
this.apiKey = cfg.apiKey;
|
|
15
|
-
this.logger = logger;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async sendRequest<T>(config: AxiosRequestConfig): Promise<T> {
|
|
19
|
-
try {
|
|
20
|
-
const response = await this.cli.request<T>({
|
|
21
|
-
...config,
|
|
22
|
-
baseURL: this.baseUrl,
|
|
23
|
-
headers: {
|
|
24
|
-
...config.headers,
|
|
25
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
return response.data;
|
|
30
|
-
} catch (error) {
|
|
31
|
-
if (axios.isAxiosError(error) && error.response) {
|
|
32
|
-
throw new Error(`Unexpected status code: ${error.response.status}, body: ${JSON.stringify(error.response.data)}`);
|
|
33
|
-
}
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function createClient(cfg: LaplaceConfiguration, logger: Logger): Client {
|
|
40
|
-
return new Client(cfg, logger);
|
|
41
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|