From 730353baa767dcd15727e78f3a500f7e8632c1fe Mon Sep 17 00:00:00 2001 From: Lino Silva Date: Wed, 13 May 2026 00:10:00 +0100 Subject: [PATCH] feat: Financial Times tracking --- availableSymbols.ts | 10 ++++++++++ index.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ interfaces.ts | 1 + 3 files changed, 56 insertions(+) diff --git a/availableSymbols.ts b/availableSymbols.ts index 93957c5..6d48f43 100644 --- a/availableSymbols.ts +++ b/availableSymbols.ts @@ -24,4 +24,14 @@ export const availableSymbols: Record = { mappedSymbol: "bpi-destino-ppr-2050", provider: ProviderType.BPI, }, + + // Financial times + "0P0001P80D-FI": { + mappedSymbol: "PTBG2EHM0006", + provider: ProviderType.FT, + }, + lu0203975437: { + mappedSymbol: "lu0203975437", + provider: ProviderType.FT, + }, }; diff --git a/index.ts b/index.ts index 3dc9056..6ec99b5 100644 --- a/index.ts +++ b/index.ts @@ -18,6 +18,10 @@ const providers: Record = { baseUrl: "https://www.deco.proteste.pt/investe/investimentos/fundos/${symbol}", }, + [ProviderType.FT]: { + type: ProviderType.FT, + baseUrl: "https://markets.ft.com/data/funds/tearsheet/summary?s=${symbol}", + }, }; interface SymbolConfig { @@ -67,6 +71,8 @@ async function getLatestPrice(symbol: string) { return await scrapeBPI(page, symbol, config); case ProviderType.DECO: return await scrapeDeco(page, symbol, config); + case ProviderType.FT: + return await scrapeFT(page, symbol, config); default: throw new Error(`Unsupported type: ${config.type}`); } @@ -155,6 +161,43 @@ async function scrapeBPI(page: Page, symbol: string, config: SymbolConfig) { }; } +async function scrapeFT(page: Page, symbol: string, config: SymbolConfig) { + // Interact with the DOM to retrieve the price and date + const price = await page.evaluate(() => { + const spanTags = document.getElementsByTagName("span"); + const priceSearchText = "Price (EUR)"; + let priceElementFound; + + for (let i = 0; i < spanTags.length; i++) { + if (spanTags[i].textContent.trim() == priceSearchText) { + priceElementFound = spanTags[i]; + if (priceElementFound) break; + } + } + + if (!priceElementFound) { + throw new Error("Could not find price element"); + } + + return priceElementFound?.nextSibling?.textContent; + }); + + const marketPrice = parseFloat( + price?.replace("€", "").trim().replace(",", ".") || "0", + ); + + return { + symbol, + name: config.name, + currency: "EUR", + price: marketPrice, + // date in FT is not available, so we use the current date as a placeholder + // yyyy-mm-dd format + date: new Date().toISOString().split("T")[0], + timestamp: new Date().toISOString(), + }; +} + // API endpoint to get latest price for a symbol app.get("/price/:symbol", async (req, res) => { const { symbol } = req.params; @@ -186,6 +229,7 @@ app.get("/", (req, res) => { health: "/health", "price-bpi": "/price/bpi/:symbol", "price-deco": "/price/deco/:symbol", + "price-ft": "/price/ft/:symbol", }, example: `/price/bpi/BPIDEST2040`, }); @@ -194,5 +238,6 @@ app.get("/", (req, res) => { // Start the server app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); + console.log("Available symbols:", Object.keys(availableSymbols).join(", ")); console.log(`Example: http://localhost:${PORT}/price/bpi/BPIDEST2040`); }); diff --git a/interfaces.ts b/interfaces.ts index 18c3a3d..77a5436 100644 --- a/interfaces.ts +++ b/interfaces.ts @@ -1,6 +1,7 @@ export enum ProviderType { BPI = "bpi", DECO = "deco", + FT = "ft", } export interface Provider {