import { getYesterday } from '@ghostfolio/helper'; import { Injectable } from '@nestjs/common'; import * as bent from 'bent'; import * as cheerio from 'cheerio'; import { format } from 'date-fns'; import { DataProviderInterface } from '../../interfaces/data-provider.interface'; import { Granularity } from '../../interfaces/granularity.type'; import { IDataProviderHistoricalResponse, IDataProviderResponse, MarketState } from '../../interfaces/interfaces'; import { PrismaService } from '../../prisma.service'; @Injectable() export class GhostfolioScraperApiService implements DataProviderInterface { private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g; public constructor(private prisma: PrismaService) {} public async get( aSymbols: string[] ): Promise<{ [symbol: string]: IDataProviderResponse }> { if (aSymbols.length <= 0) { return {}; } try { const symbol = aSymbols[0]; const scraperConfig = await this.getScraperConfig(symbol); const { marketPrice } = await this.prisma.marketData.findFirst({ orderBy: { date: 'desc' }, where: { symbol } }); return { [symbol]: { marketPrice, currency: scraperConfig?.currency, marketState: MarketState.delayed, name: scraperConfig?.name } }; } catch (error) { console.error(error); } return {}; } public async getHistorical( aSymbols: string[], aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { if (aSymbols.length <= 0) { return {}; } try { const symbol = aSymbols[0]; const scraperConfig = await this.getScraperConfig(symbol); const get = bent(scraperConfig?.url, 'GET', 'string', 200, {}); const html = await get(); const $ = cheerio.load(html); const value = this.extractNumberFromString( $(scraperConfig?.selector).text() ); return { [symbol]: { [format(getYesterday(), 'yyyy-MM-dd')]: { marketPrice: value } } }; } catch (error) { console.error(error); } return {}; } private extractNumberFromString(aString: string): number { try { const [numberString] = aString.match( GhostfolioScraperApiService.NUMERIC_REGEXP ); return parseFloat(numberString.trim()); } catch { return undefined; } } private async getScraperConfig(aSymbol: string) { try { const { value: scraperConfigString } = await this.prisma.property.findFirst({ select: { value: true }, where: { key: 'SCRAPER_CONFIG' } }); return JSON.parse(scraperConfigString).find((item) => { return item.symbol === aSymbol; }); } catch {} return {}; } }