feat: Financial Times tracking
Build and publish / build (push) Successful in 44s

This commit is contained in:
Lino Silva
2026-05-13 00:10:00 +01:00
parent 5a851a15ae
commit 730353baa7
3 changed files with 56 additions and 0 deletions
+10
View File
@@ -24,4 +24,14 @@ export const availableSymbols: Record<string, AvailableSymbol> = {
mappedSymbol: "bpi-destino-ppr-2050", mappedSymbol: "bpi-destino-ppr-2050",
provider: ProviderType.BPI, provider: ProviderType.BPI,
}, },
// Financial times
"0P0001P80D-FI": {
mappedSymbol: "PTBG2EHM0006",
provider: ProviderType.FT,
},
lu0203975437: {
mappedSymbol: "lu0203975437",
provider: ProviderType.FT,
},
}; };
+45
View File
@@ -18,6 +18,10 @@ const providers: Record<ProviderType, Provider> = {
baseUrl: baseUrl:
"https://www.deco.proteste.pt/investe/investimentos/fundos/${symbol}", "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 { interface SymbolConfig {
@@ -67,6 +71,8 @@ async function getLatestPrice(symbol: string) {
return await scrapeBPI(page, symbol, config); return await scrapeBPI(page, symbol, config);
case ProviderType.DECO: case ProviderType.DECO:
return await scrapeDeco(page, symbol, config); return await scrapeDeco(page, symbol, config);
case ProviderType.FT:
return await scrapeFT(page, symbol, config);
default: default:
throw new Error(`Unsupported type: ${config.type}`); 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 // API endpoint to get latest price for a symbol
app.get("/price/:symbol", async (req, res) => { app.get("/price/:symbol", async (req, res) => {
const { symbol } = req.params; const { symbol } = req.params;
@@ -186,6 +229,7 @@ app.get("/", (req, res) => {
health: "/health", health: "/health",
"price-bpi": "/price/bpi/:symbol", "price-bpi": "/price/bpi/:symbol",
"price-deco": "/price/deco/:symbol", "price-deco": "/price/deco/:symbol",
"price-ft": "/price/ft/:symbol",
}, },
example: `/price/bpi/BPIDEST2040`, example: `/price/bpi/BPIDEST2040`,
}); });
@@ -194,5 +238,6 @@ app.get("/", (req, res) => {
// Start the server // Start the server
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server running on http://localhost:${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`); console.log(`Example: http://localhost:${PORT}/price/bpi/BPIDEST2040`);
}); });
+1
View File
@@ -1,6 +1,7 @@
export enum ProviderType { export enum ProviderType {
BPI = "bpi", BPI = "bpi",
DECO = "deco", DECO = "deco",
FT = "ft",
} }
export interface Provider { export interface Provider {