diff --git a/README.md b/README.md index a141e2aa..b53ae58d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This [Trading 212](https://www.trading212.com/) API can be used with TypeScript ## Implemented Endpoints - [x] [Instruments Metadata](https://t212public-api-docs.redoc.ly/#tag/Instruments-Metadata) -- [ ] Pies +- [x] [Pies](https://t212public-api-docs.redoc.ly/#tag/Pies) - [x] [Equity Orders](https://t212public-api-docs.redoc.ly/#tag/Equity-Orders) - [x] [Account Data](https://t212public-api-docs.redoc.ly/#tag/Account-Data) - [x] [Personal Portfolio](https://t212public-api-docs.redoc.ly/#tag/Personal-Portfolio) diff --git a/package.json b/package.json index c16797ed..99c99881 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "demo:history": "cross-env TSIMP_DIAG=error node --import=tsimp/import ./src/demo/history.ts", "demo:metadata": "cross-env TSIMP_DIAG=error node --import=tsimp/import ./src/demo/metadata.ts", "demo:order": "cross-env TSIMP_DIAG=error node --import=tsimp/import ./src/demo/order.ts", + "demo:pie": "cross-env TSIMP_DIAG=error node --import=tsimp/import ./src/demo/pie.ts", "demo:portfolio": "cross-env TSIMP_DIAG=error node --import=tsimp/import ./src/demo/portfolio.ts", "deploy": "exit 0", "dev": "cross-env TSIMP_DIAG=error node --inspect --import=tsimp/import ./src/start.ts", diff --git a/src/RESTClient.ts b/src/RESTClient.ts index 719e61cb..7564e47b 100644 --- a/src/RESTClient.ts +++ b/src/RESTClient.ts @@ -13,6 +13,7 @@ import {MetadataAPI} from './api/metadata/MetadataAPI.js'; import {PortfolioAPI} from './api/portfolio/PortfolioAPI.js'; import {HistoryAPI} from './api/history/HistoryAPI.js'; import {OrderAPI} from './api/order/OrderAPI.js'; +import {PieAPI} from './api/pie/PieAPI.js'; /** * This class configures the HTTP Library (axios) so it uses the proper URL and reconnection states. It also exposes all available endpoints. @@ -34,6 +35,7 @@ export class RESTClient { readonly history: HistoryAPI; readonly metadata: MetadataAPI; readonly order: OrderAPI; + readonly pie: PieAPI; readonly portfolio: PortfolioAPI; private readonly httpClient: AxiosInstance; @@ -76,9 +78,16 @@ export class RESTClient { const url = error.config?.url; const method = error.config?.method; - // If a particular order is fetched we can use a lower timeout - if (method === 'get' && url?.includes(OrderAPI.URL.ORDERS + '/')) { - return 1_000; + if (method === 'get') { + // If a particular order ID is fetched we can use a lower timeout + if (url?.includes(OrderAPI.URL.ORDERS + '/')) { + return 1_000; + } + + // If a particular pie ID is fetched we can use a lower timeout + if (url?.includes(PieAPI.URL.PIES + '/')) { + return 5_000; + } } switch (url) { @@ -89,6 +98,7 @@ export class RESTClient { return 5_000; case AccountAPI.URL.INFO: case MetadataAPI.URL.EXCHANGES: + case PieAPI.URL.PIES: return 30_000; case MetadataAPI.URL.INSTRUMENTS: return 50_000; @@ -111,6 +121,7 @@ export class RESTClient { this.history = new HistoryAPI(this.httpClient); this.metadata = new MetadataAPI(this.httpClient); this.order = new OrderAPI(this.httpClient); + this.pie = new PieAPI(this.httpClient); this.portfolio = new PortfolioAPI(this.httpClient); } } diff --git a/src/api/pie/PieAPI.ts b/src/api/pie/PieAPI.ts new file mode 100644 index 00000000..64ed01a1 --- /dev/null +++ b/src/api/pie/PieAPI.ts @@ -0,0 +1,78 @@ +import type {AxiosInstance} from 'axios'; +import {z} from 'zod'; +import {DIVIDEND_CASH_ACTION, PIE_ICON, PIE_STATUS_GOAL} from '../union.js'; + +const PieSchema = z.object({ + cash: z.number(), + dividendDetails: z.object({ + gained: z.number(), + inCash: z.number(), + reinvested: z.number(), + }), + id: z.number(), + progress: z.number(), + result: z.object({ + investedValue: z.number(), + result: z.number(), + resultCoef: z.number(), + value: z.number(), + }), + status: PIE_STATUS_GOAL, +}); + +const DetailedPieSchema = z.object({ + instruments: z.array( + z.object({ + currentShare: z.number(), + expectedShare: z.number(), + issues: z.array(z.object({name: z.string(), severity: z.string()})), + ownedQuantity: z.number(), + result: z.object({ + investedValue: z.number(), + result: z.number(), + resultCoef: z.number(), + value: z.number(), + }), + ticker: z.string(), + }) + ), + settings: z.object({ + creationDate: z.string().datetime({offset: true}), + dividendCashAction: DIVIDEND_CASH_ACTION, + endDate: z.string().datetime({offset: true}), + goal: z.number(), + icon: PIE_ICON, + id: z.number(), + instrumentShares: z.union([z.null(), z.record(z.number())]), + initialInvestment: z.number(), + name: z.string(), + publicUrl: z.union([z.string(), z.null()]), + }), +}); + +export type Pie = z.infer; + +export type DetailedPie = z.infer; + +/** + * @see https://t212public-api-docs.redoc.ly/#tag/Pies + */ +export class PieAPI { + static readonly URL = { + PIES: '/api/v0/equity/pies', + }; + + constructor(private readonly apiClient: AxiosInstance) {} + + /** + * @see https://t212public-api-docs.redoc.ly/#operation/getAll + * @see https://t212public-api-docs.redoc.ly/#operation/getDetailed + */ + async getPie(): Promise; + async getPie(id: number): Promise; + async getPie(id?: number) { + const resource = id ? `${PieAPI.URL.PIES}/${id}` : PieAPI.URL.PIES; + const response = await this.apiClient.get(resource); + return id ? DetailedPieSchema.parse(response.data) : z.array(PieSchema).parse(response.data); + } +} diff --git a/src/api/union.ts b/src/api/union.ts index d0680b07..8733adbf 100644 --- a/src/api/union.ts +++ b/src/api/union.ts @@ -9,6 +9,8 @@ export const DEVICE = z.union([ z.literal('WEB'), ]); +export const DIVIDEND_CASH_ACTION = z.union([z.literal('REINVEST'), z.literal('TO_ACCOUNT_CASH')]); + export const DIVIDEND_TYPE = z.union([ z.literal('BONUS_MANUFACTURED_PAYMENT'), z.literal('BONUS'), @@ -108,6 +110,48 @@ export const ORDER_STATUS = z.union([ export const ORDER_STRATEGY = z.union([z.literal('QUANTITY'), z.literal('VALUE')]); +export const PIE_ICON = z.union([ + z.literal('Airplane'), + z.literal('Apartments'), + z.literal('Bills'), + z.literal('BillsAndCoins'), + z.literal('Briefcase'), + z.literal('Burger'), + z.literal('Bus'), + z.literal('Cabin'), + z.literal('Car'), + z.literal('Child'), + z.literal('Coins'), + z.literal('Convertable'), + z.literal('Education'), + z.literal('Energy'), + z.literal('Factory'), + z.literal('Family'), + z.literal('Global'), + z.literal('Home'), + z.literal('Iceberg'), + z.literal('Landscape'), + z.literal('Leaf'), + z.literal('Materials'), + z.literal('Medical'), + z.literal('PiggyBank'), + z.literal('Pill'), + z.literal('Ring'), + z.literal('RV'), + z.literal('Shipping'), + z.literal('Storefront'), + z.literal('Tech'), + z.literal('Travel'), + z.literal('Umbrella'), + z.literal('Unicorn'), + z.literal('Vault'), + z.literal('Water'), + z.literal('Whale'), + z.literal('Wind'), +]); + +export const PIE_STATUS_GOAL = z.union([z.literal('AHEAD'), z.literal('ON_TRACK'), z.literal('BEHIND')]); + export const TAX_NAME = z.union([ z.literal('COMMISSION_TURNOVER'), z.literal('CURRENCY_CONVERSION_FEE'), diff --git a/src/demo/pie.ts b/src/demo/pie.ts new file mode 100644 index 00000000..3662d746 --- /dev/null +++ b/src/demo/pie.ts @@ -0,0 +1,6 @@ +import {initClient} from './initClient.js'; + +const client = await initClient(); + +const pies = await client.rest.pie.getPie(); +console.info(new Date().toISOString(), pies); diff --git a/src/fixtures/api/v0/equity/pies/pie.json b/src/fixtures/api/v0/equity/pies/pie.json new file mode 100644 index 00000000..700e786b --- /dev/null +++ b/src/fixtures/api/v0/equity/pies/pie.json @@ -0,0 +1,29 @@ +{ + "instruments": [ + { + "currentShare": 1, + "expectedShare": 1, + "issues": [], + "ownedQuantity": 8.3444584, + "result": { + "investedValue": 1000, + "result": 24.37, + "resultCoef": 0.0244, + "value": 1024.37 + }, + "ticker": "VWCEd_EQ" + } + ], + "settings": { + "creationDate": "2023-12-01T23:59:59.999+03:00", + "dividendCashAction": "REINVEST", + "endDate": "2024-12-01T23:59:59.999+03:00", + "goal": 13159, + "icon": "Coins", + "id": 1337, + "initialInvestment": 1000, + "instrumentShares": null, + "name": "World ETF", + "publicUrl": null + } +}