Compare commits
49 Commits
c499da44e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d2010f5d3a | |||
| 730353baa7 | |||
| 5a851a15ae | |||
| e6f7b2270b | |||
| 405a959691 | |||
| 1ce14628de | |||
| 6234f33ebb | |||
| eab6aa4e25 | |||
| b0504e88f6 | |||
| ef264dff2a | |||
| f81c39153a | |||
| dfe18a62dd | |||
| 7841804cd1 | |||
| dd626dc272 | |||
| ee4487f914 | |||
| 0744ccdf6a | |||
| e4a7143f75 | |||
| f7382487d3 | |||
| fe4c54ebcb | |||
| be8ff08f94 | |||
| 1e7dc2baca | |||
| 199d729802 | |||
| a9122ecab1 | |||
| 1feab44ee4 | |||
| 4006719950 | |||
| 00e61c3fea | |||
| 211d18e606 | |||
| 552a75f13d | |||
| 554bc62199 | |||
| 438f19812b | |||
| dbd7f0f9e7 | |||
| 3594e33786 | |||
| bb777f002b | |||
| 6603df380c | |||
| cdbadd5c92 | |||
| b2d20fac5c | |||
| 4ddcfd33cf | |||
| 59f6f90664 | |||
| b4c37b218a | |||
| d1761800c0 | |||
| 9716c25377 | |||
| 46ebcc041f | |||
| fe7e197480 | |||
| f27c03dbaf | |||
| cfe7eb0454 | |||
| 06400d276e | |||
| 13d55bc800 | |||
| fdf746c067 | |||
| 81b898a8f1 |
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
coverage
|
||||||
|
.eslintcache
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Optional: Port to run the server on (defaults to 3000)
|
||||||
|
PORT=3000
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Build and publish
|
||||||
|
run-name: Build docker image and publish to registry
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: https://github.com/docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
config-inline: |
|
||||||
|
[registry."gitea.lino.cooking"]
|
||||||
|
http = true
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
uses: https://github.com/docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea.lino.cooking
|
||||||
|
username: lino
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: https://github.com/docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: gitea.lino.cooking/${{ gitea.repository }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=sha,prefix={{branch}}-
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: https://github.com/docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=gitea.lino.cooking/${{ gitea.repository }}:buildcache
|
||||||
|
cache-to: type=registry,ref=gitea.lino.cooking/${{ gitea.repository }}:buildcache,mode=max
|
||||||
+1
-1
@@ -129,4 +129,4 @@ dist
|
|||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
# Use Node.js LTS with Debian (needed for Puppeteer dependencies)
|
||||||
|
FROM node:20-bookworm-slim
|
||||||
|
|
||||||
|
# Install Chromium dependencies for Puppeteer
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
chromium \
|
||||||
|
fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf \
|
||||||
|
libxss1 \
|
||||||
|
--no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set Puppeteer to use installed Chromium
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
@@ -1,3 +1,97 @@
|
|||||||
# bpi-stock-price-scraper
|
# bpi-stock-price-scraper
|
||||||
|
|
||||||
Extrair preço de PPRs para introduzir no Ghostfolio
|
Web server API to fetch latest BPI PPR stock prices on demand.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- REST API endpoints to get latest prices for BPI Destino PPR funds
|
||||||
|
- Returns price data as JSON
|
||||||
|
- Dockerized for easy deployment
|
||||||
|
- Health check endpoint included
|
||||||
|
|
||||||
|
## Available Symbols
|
||||||
|
|
||||||
|
- `BPIDEST2040` - BPI Destino PPR 2040
|
||||||
|
- `BPIDEST2050` - BPI Destino PPR 2050
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- `GET /` - API information and usage examples
|
||||||
|
- `GET /health` - Health check with available symbols
|
||||||
|
- `GET /price/:symbol` - Get latest price for a specific symbol
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"symbol": "BPIDEST2040",
|
||||||
|
"name": "BPI Destino PPR 2040",
|
||||||
|
"price": 7.73313,
|
||||||
|
"date": "2026-04-09",
|
||||||
|
"timestamp": "2026-04-10T16:11:25.850Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Docker (Recommended)
|
||||||
|
|
||||||
|
Build and run with docker-compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Or build and run manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t bpi-stock-price-scraper .
|
||||||
|
docker run -p 3000:3000 bpi-stock-price-scraper
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
|
||||||
|
Install dependencies and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
For development with auto-reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The server runs on port 3000 by default. You can change it via the `PORT` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=8080 npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in docker-compose.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- PORT=8080
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test the API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get API info
|
||||||
|
curl http://localhost:3000/
|
||||||
|
|
||||||
|
# Check health
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
|
||||||
|
# Get latest price
|
||||||
|
curl http://localhost:3000/price/BPIDEST2040
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { AvailableSymbol, ProviderType } from "./interfaces";
|
||||||
|
|
||||||
|
export const availableSymbols: Record<string, AvailableSymbol> = {
|
||||||
|
// DECO Proteste
|
||||||
|
"0P0001P80D": {
|
||||||
|
mappedSymbol: "bpi-impacto-clima-moderado",
|
||||||
|
provider: ProviderType.DECO,
|
||||||
|
},
|
||||||
|
IE00BFMXXD54: {
|
||||||
|
mappedSymbol: "vanguard-sp-500-ucits-etf-usd-acc",
|
||||||
|
provider: ProviderType.DECO,
|
||||||
|
},
|
||||||
|
IE00BK5BQT80: {
|
||||||
|
mappedSymbol: "vanguard-ftse-all-world-ucits-etf-usd-acc",
|
||||||
|
provider: ProviderType.DECO,
|
||||||
|
},
|
||||||
|
|
||||||
|
// BPI
|
||||||
|
BPIDEST2040: {
|
||||||
|
mappedSymbol: "bpi-destino-ppr-2040",
|
||||||
|
provider: ProviderType.BPI,
|
||||||
|
},
|
||||||
|
BPIDEST2050: {
|
||||||
|
mappedSymbol: "bpi-destino-ppr-2050",
|
||||||
|
provider: ProviderType.BPI,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Financial times
|
||||||
|
"0P0001P80D-FI": {
|
||||||
|
mappedSymbol: "PTBG2EHM0006",
|
||||||
|
provider: ProviderType.FT,
|
||||||
|
},
|
||||||
|
LU0203975437: {
|
||||||
|
mappedSymbol: "lu0203975437",
|
||||||
|
provider: ProviderType.FT,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
bpi-scraper:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- PORT=3000
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{languageOptions: { globals: globals.browser }},
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
];
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import puppeteer, { Page } from "puppeteer";
|
||||||
|
import express from "express";
|
||||||
|
import "dotenv/config";
|
||||||
|
import { Provider, ProviderType } from "./interfaces";
|
||||||
|
import { availableSymbols } from "./availableSymbols";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
const providers: Record<ProviderType, Provider> = {
|
||||||
|
[ProviderType.BPI]: {
|
||||||
|
type: ProviderType.BPI,
|
||||||
|
baseUrl:
|
||||||
|
"https://www.bancobpi.pt/particulares/poupar-investir/ppr/${symbol}",
|
||||||
|
},
|
||||||
|
[ProviderType.DECO]: {
|
||||||
|
type: ProviderType.DECO,
|
||||||
|
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 {
|
||||||
|
name: string;
|
||||||
|
type: ProviderType;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigForSymbol(symbol: string): SymbolConfig {
|
||||||
|
if (!availableSymbols[symbol]) {
|
||||||
|
throw new Error(`Symbol ${symbol} is not supported`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: symbol,
|
||||||
|
type: availableSymbols[symbol].provider,
|
||||||
|
url: providers[availableSymbols[symbol].provider].baseUrl.replace(
|
||||||
|
"${symbol}",
|
||||||
|
availableSymbols[symbol].mappedSymbol,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to scrape price for a given symbol
|
||||||
|
async function getLatestPrice(symbol: string) {
|
||||||
|
const config: SymbolConfig = getConfigForSymbol(symbol);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Scraping ${config.url} for symbol ${symbol} using provider ${config.type}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Launch the browser
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
args: ["--no-sandbox"],
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Open a new tab
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// Visit the page and wait until network connections are completed
|
||||||
|
await page.goto(config.url, { waitUntil: "networkidle2" });
|
||||||
|
|
||||||
|
switch (config.type) {
|
||||||
|
case ProviderType.BPI:
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Always close the browser
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrapeDeco(page: Page, symbol: string, config: SymbolConfig) {
|
||||||
|
// Interact with the DOM to retrieve the price and date
|
||||||
|
const [price, date] = await page.evaluate(() => {
|
||||||
|
const priceElement = document.querySelector(
|
||||||
|
".product-points-data__current-value",
|
||||||
|
);
|
||||||
|
const dateElement = document.querySelector(
|
||||||
|
".product-points-data__current-value + span",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!priceElement || !dateElement) {
|
||||||
|
throw new Error("Could not find price or date elements");
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = priceElement.textContent
|
||||||
|
.replace("€", "")
|
||||||
|
.replace("EUR", "")
|
||||||
|
.replace(",", ".")
|
||||||
|
.trim();
|
||||||
|
const date = dateElement.textContent.trim();
|
||||||
|
|
||||||
|
return [price, date];
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketPrice = parseFloat(price);
|
||||||
|
return {
|
||||||
|
symbol,
|
||||||
|
name: config.name,
|
||||||
|
currency: "EUR",
|
||||||
|
price: marketPrice,
|
||||||
|
date,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrapeBPI(page: Page, symbol: string, config: SymbolConfig) {
|
||||||
|
// Interact with the DOM to retrieve the price and date
|
||||||
|
const [price, date] = await page.evaluate(() => {
|
||||||
|
const spanTags = document.getElementsByTagName("span");
|
||||||
|
const priceSearchText = "ÚLTIMA COTAÇÃO:";
|
||||||
|
const dateSearchText = "DATA COTAÇÃO:";
|
||||||
|
let priceElementFound;
|
||||||
|
let dateElementFound;
|
||||||
|
|
||||||
|
for (let i = 0; i < spanTags.length; i++) {
|
||||||
|
if (spanTags[i].textContent.trim() == priceSearchText) {
|
||||||
|
priceElementFound = spanTags[i];
|
||||||
|
if (priceElementFound && dateElementFound) break;
|
||||||
|
}
|
||||||
|
if (spanTags[i].textContent.trim() == dateSearchText) {
|
||||||
|
dateElementFound = spanTags[i];
|
||||||
|
if (priceElementFound && dateElementFound) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priceElementFound || !dateElementFound) {
|
||||||
|
throw new Error("Could not find price or date elements");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
priceElementFound?.nextSibling?.textContent,
|
||||||
|
dateElementFound?.nextSibling?.textContent,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketPrice = parseFloat(
|
||||||
|
price?.replace("€", "").trim().replace(",", ".") || "0",
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
symbol,
|
||||||
|
name: config.name,
|
||||||
|
currency: "EUR",
|
||||||
|
price: marketPrice,
|
||||||
|
date: date?.trim() || "",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Fetching price for symbol: ${symbol}`);
|
||||||
|
const priceData = await getLatestPrice(symbol);
|
||||||
|
console.log(`Success: ${symbol} - ${priceData.price}`);
|
||||||
|
res.json(priceData);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error fetching price for ${symbol}:`, error.message);
|
||||||
|
res.status(400).json({
|
||||||
|
error: error.message,
|
||||||
|
symbol,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get("/health", (req, res) => {
|
||||||
|
res.json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Root endpoint with usage instructions
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.json({
|
||||||
|
message: "BPI Stock Price Scraper API",
|
||||||
|
endpoints: {
|
||||||
|
health: "/health",
|
||||||
|
"price-bpi": "/price/bpi/:symbol",
|
||||||
|
"price-deco": "/price/deco/:symbol",
|
||||||
|
"price-ft": "/price/ft/:symbol",
|
||||||
|
},
|
||||||
|
example: `/price/bpi/BPIDEST2040`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export enum ProviderType {
|
||||||
|
BPI = "bpi",
|
||||||
|
DECO = "deco",
|
||||||
|
FT = "ft",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Provider {
|
||||||
|
type: ProviderType;
|
||||||
|
baseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableSymbol {
|
||||||
|
mappedSymbol: string;
|
||||||
|
provider: ProviderType;
|
||||||
|
}
|
||||||
Generated
+2786
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "bpi-stock-price-scraper",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"repository": "ssh://git@10.0.2.28:222/lino-authelia/bpi-stock-price-scraper.git",
|
||||||
|
"author": "Lino Silva <me@lino.cooking>",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsx index.ts",
|
||||||
|
"dev": "tsx watch index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"puppeteer": "^23.5.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.12.0",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/node": "^25.7.0",
|
||||||
|
"eslint": "^9.12.0",
|
||||||
|
"globals": "^15.11.0",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2385
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user