import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { IDataProviderHistoricalResponse, IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config'; import { DATE_FORMAT, extractNumberFromString, getYesterday } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import * as cheerio from 'cheerio'; import { isUUID } from 'class-validator'; import { addDays, format, isBefore } from 'date-fns'; import got from 'got'; @Injectable() export class ManualService implements DataProviderInterface { public constructor( private readonly prismaService: PrismaService, private readonly symbolProfileService: SymbolProfileService ) {} public canHandle(symbol: string) { return true; } public async getAssetProfile( aSymbol: string ): Promise> { return { dataSource: this.getName(), symbol: aSymbol }; } public async getDividends({ from, granularity = 'day', symbol, to }: { from: Date; granularity: Granularity; symbol: string; to: Date; }) { return {}; } public async getHistorical( aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { try { const symbol = aSymbol; const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( [{ symbol, dataSource: this.getName() }] ); const { defaultMarketPrice, headers = {}, selector, url } = symbolProfile.scraperConfiguration ?? {}; if (defaultMarketPrice) { const historical: { [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; } = { [symbol]: {} }; let date = from; while (isBefore(date, to)) { historical[symbol][format(date, DATE_FORMAT)] = { marketPrice: defaultMarketPrice }; date = addDays(date, 1); } return historical; } else if (selector === undefined || url === undefined) { return {}; } const abortController = new AbortController(); setTimeout(() => { abortController.abort(); }, DEFAULT_REQUEST_TIMEOUT); const { body } = await got(url, { headers, // @ts-ignore signal: abortController.signal }); const $ = cheerio.load(body); const value = extractNumberFromString($(selector).text()); return { [symbol]: { [format(getYesterday(), DATE_FORMAT)]: { marketPrice: value } } }; } catch (error) { throw new Error( `Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format( from, DATE_FORMAT )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}` ); } } public getName(): DataSource { return DataSource.MANUAL; } public async getQuotes( aSymbols: string[] ): Promise<{ [symbol: string]: IDataProviderResponse }> { const response: { [symbol: string]: IDataProviderResponse } = {}; if (aSymbols.length <= 0) { return response; } try { const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( aSymbols.map((symbol) => { return { symbol, dataSource: this.getName() }; }) ); const marketData = await this.prismaService.marketData.findMany({ distinct: ['symbol'], orderBy: { date: 'desc' }, take: aSymbols.length, where: { symbol: { in: aSymbols } } }); for (const symbolProfile of symbolProfiles) { response[symbolProfile.symbol] = { currency: symbolProfile.currency, dataSource: this.getName(), marketPrice: marketData.find((marketDataItem) => { return marketDataItem.symbol === symbolProfile.symbol; })?.marketPrice, marketState: 'delayed' }; } return response; } catch (error) { Logger.error(error, 'ManualService'); } return {}; } public getTestSymbol() { return undefined; } public async search({ includeIndices = false, query }: { includeIndices?: boolean; query: string; }): Promise<{ items: LookupItem[] }> { let items = await this.prismaService.symbolProfile.findMany({ select: { assetClass: true, assetSubClass: true, currency: true, dataSource: true, name: true, symbol: true }, where: { OR: [ { dataSource: this.getName(), name: { mode: 'insensitive', startsWith: query } }, { dataSource: this.getName(), symbol: { mode: 'insensitive', startsWith: query } } ] } }); items = items.filter(({ symbol }) => { // Remove UUID symbols (activities of type ITEM) return !isUUID(symbol); }); return { items }; } }