From 4451514ec5ea77ab8508aaa57f492a04147c4ef5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 14 Apr 2023 07:01:34 +0200 Subject: [PATCH 1/6] Release 1.253.0 (#1854) --- CHANGELOG.md | 6 +++++- apps/api/src/services/cron.service.ts | 3 ++- package.json | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20eb4e6a8..5402edd4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 1.253.0 - 2023-04-14 + +### Changed + +- Reduced the execution interval of the data gathering to every 12 hours ### Fixed diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index 8b036c35e..3b592d176 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -21,12 +21,13 @@ export class CronService { @Cron(CronExpression.EVERY_4_HOURS) public async runEveryFourHours() { - await this.dataGatheringService.gather7Days(); + // await this.dataGatheringService.gather7Days(); } @Cron(CronExpression.EVERY_12_HOURS) public async runEveryTwelveHours() { await this.exchangeRateDataService.loadCurrencies(); + await this.dataGatheringService.gather7Days(); } @Cron(CronExpression.EVERY_DAY_AT_5PM) diff --git a/package.json b/package.json index 0bbab1ec5..457bbb63f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.252.2", + "version": "1.253.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From 1ed5690b33e5d55cb6f87bb92c221c5506e4c069 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 14 Apr 2023 19:57:23 +0200 Subject: [PATCH 2/6] Feature/improve queue jobs implementation (#1855) * Improve queue jobs implementation * Update changelog --- CHANGELOG.md | 7 +++ apps/api/src/app/admin/admin.controller.ts | 15 ++++- apps/api/src/app/order/order.service.ts | 5 +- apps/api/src/services/cron.service.ts | 8 ++- .../src/services/data-gathering.service.ts | 61 ++++++++----------- 5 files changed, 52 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5402edd4b..75e464622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Improved the queue jobs implementation by adding in bulk +- Improved the queue jobs implementation by introducing unique job ids + ## 1.253.0 - 2023-04-14 ### Changed diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 7f3f17791..6d34f8cdb 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -107,7 +107,10 @@ export class AdminController { dataSource, symbol }, - GATHER_ASSET_PROFILE_PROCESS_OPTIONS + { + ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, + jobId: `${dataSource}-${symbol}}` + } ); } @@ -138,7 +141,10 @@ export class AdminController { dataSource, symbol }, - GATHER_ASSET_PROFILE_PROCESS_OPTIONS + { + ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, + jobId: `${dataSource}-${symbol}}` + } ); } } @@ -167,7 +173,10 @@ export class AdminController { dataSource, symbol }, - GATHER_ASSET_PROFILE_PROCESS_OPTIONS + { + ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, + jobId: `${dataSource}-${symbol}}` + } ); } diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 2d0cb7376..50cc3bf71 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -118,7 +118,10 @@ export class OrderService { dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, symbol: data.SymbolProfile.connectOrCreate.create.symbol }, - GATHER_ASSET_PROFILE_PROCESS_OPTIONS + { + ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, + jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}}` + } ); const isDraft = isAfter(data.date as Date, endOfToday()); diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index 3b592d176..69906dfa9 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -21,13 +21,12 @@ export class CronService { @Cron(CronExpression.EVERY_4_HOURS) public async runEveryFourHours() { - // await this.dataGatheringService.gather7Days(); + await this.dataGatheringService.gather7Days(); } @Cron(CronExpression.EVERY_12_HOURS) public async runEveryTwelveHours() { await this.exchangeRateDataService.loadCurrencies(); - await this.dataGatheringService.gather7Days(); } @Cron(CronExpression.EVERY_DAY_AT_5PM) @@ -46,7 +45,10 @@ export class CronService { dataSource, symbol }, - GATHER_ASSET_PROFILE_PROCESS_OPTIONS + { + ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, + jobId: `${dataSource}-${symbol}}` + } ); } } diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 62209846c..4015cf114 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -2,8 +2,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se import { DATA_GATHERING_QUEUE, GATHER_HISTORICAL_MARKET_DATA_PROCESS, - GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS, - QUEUE_JOB_STATUS_LIST + GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS } from '@ghostfolio/common/config'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { UniqueAsset } from '@ghostfolio/common/interfaces'; @@ -34,17 +33,14 @@ export class DataGatheringService { private readonly symbolProfileService: SymbolProfileService ) {} - public async addJobToQueue(name: string, data: any, options?: JobOptions) { - const hasJob = await this.hasJob(name, data); + public async addJobToQueue(name: string, data: any, opts?: JobOptions) { + return this.dataGatheringQueue.add(name, data, opts); + } - if (hasJob) { - Logger.log( - `Job ${name} with data ${JSON.stringify(data)} already exists.`, - 'DataGatheringService' - ); - } else { - return this.dataGatheringQueue.add(name, data, options); - } + public async addJobsToQueue( + jobs: { data: any; name: string; opts?: JobOptions }[] + ) { + return this.dataGatheringQueue.addBulk(jobs); } public async gather7Days() { @@ -209,17 +205,22 @@ export class DataGatheringService { } public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) { - for (const { dataSource, date, symbol } of aSymbolsWithStartDate) { - await this.addJobToQueue( - GATHER_HISTORICAL_MARKET_DATA_PROCESS, - { - dataSource, - date, - symbol - }, - GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS - ); - } + await this.addJobsToQueue( + aSymbolsWithStartDate.map(({ dataSource, date, symbol }) => { + return { + data: { + dataSource, + date, + symbol + }, + name: GATHER_HISTORICAL_MARKET_DATA_PROCESS, + opts: { + ...GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS, + jobId: `${dataSource}-${symbol}-${format(date, DATE_FORMAT)}` + } + }; + }) + ); } public async getSymbolsMax(): Promise { @@ -341,18 +342,4 @@ export class DataGatheringService { return [...currencyPairsToGather, ...symbolProfilesToGather]; } - - private async hasJob(name: string, data: any) { - const jobs = await this.dataGatheringQueue.getJobs( - QUEUE_JOB_STATUS_LIST.filter((status) => { - return status !== 'completed'; - }) - ); - - return jobs.some((job) => { - return ( - job.name === name && JSON.stringify(job.data) === JSON.stringify(data) - ); - }); - } } From f2d206262e9416a4eab29d7b2aa73917069a52ad Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 14 Apr 2023 19:58:49 +0200 Subject: [PATCH 3/6] Release 1.254.0 (#1856) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e464622..90b46c51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 1.254.0 - 2023-04-14 ### Changed diff --git a/package.json b/package.json index 457bbb63f..9306fd41f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.253.0", + "version": "1.254.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { From e23ff33e6f2ab1c613b84b3a84b28a0d88df1feb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 15 Apr 2023 11:54:47 +0200 Subject: [PATCH 4/6] Feature/skip job creation for manual data source without scraper configuration (#1857) * Skip job creation for MANUAL data source without scraper configuration * Update changelog --- CHANGELOG.md | 8 ++ apps/api/src/app/admin/admin.controller.ts | 66 +++++----- apps/api/src/app/order/order.service.ts | 10 +- apps/api/src/services/cron.service.ts | 32 ++--- .../src/services/data-gathering.service.ts | 120 +++++++++++------- 5 files changed, 138 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90b46c51f..a0d860290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Skipped creating queue jobs for asset profiles with `MANUAL` data source not having a scraper configuration +- Reduced the execution interval of the data gathering to every hour + ## 1.254.0 - 2023-04-14 ### Changed - Improved the queue jobs implementation by adding in bulk - Improved the queue jobs implementation by introducing unique job ids +- Reverted the execution interval of the data gathering from every 12 hours to every 4 hours ## 1.253.0 - 2023-04-14 diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 6d34f8cdb..c32ecf03b 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -100,19 +100,21 @@ export class AdminController { const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); - for (const { dataSource, symbol } of uniqueAssets) { - await this.dataGatheringService.addJobToQueue( - GATHER_ASSET_PROFILE_PROCESS, - { - dataSource, - symbol - }, - { - ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, - jobId: `${dataSource}-${symbol}}` - } - ); - } + await this.dataGatheringService.addJobsToQueue( + uniqueAssets.map(({ dataSource, symbol }) => { + return { + data: { + dataSource, + symbol + }, + name: GATHER_ASSET_PROFILE_PROCESS, + opts: { + ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, + jobId: `${dataSource}-${symbol}}` + } + }; + }) + ); this.dataGatheringService.gatherMax(); } @@ -134,19 +136,21 @@ export class AdminController { const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); - for (const { dataSource, symbol } of uniqueAssets) { - await this.dataGatheringService.addJobToQueue( - GATHER_ASSET_PROFILE_PROCESS, - { - dataSource, - symbol - }, - { - ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, - jobId: `${dataSource}-${symbol}}` - } - ); - } + await this.dataGatheringService.addJobsToQueue( + uniqueAssets.map(({ dataSource, symbol }) => { + return { + data: { + dataSource, + symbol + }, + name: GATHER_ASSET_PROFILE_PROCESS, + opts: { + ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, + jobId: `${dataSource}-${symbol}}` + } + }; + }) + ); } @Post('gather/profile-data/:dataSource/:symbol') @@ -167,17 +171,17 @@ export class AdminController { ); } - await this.dataGatheringService.addJobToQueue( - GATHER_ASSET_PROFILE_PROCESS, - { + await this.dataGatheringService.addJobToQueue({ + data: { dataSource, symbol }, - { + name: GATHER_ASSET_PROFILE_PROCESS, + opts: { ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, jobId: `${dataSource}-${symbol}}` } - ); + }); } @Post('gather/:dataSource/:symbol') diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 50cc3bf71..4080c5c34 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -112,17 +112,17 @@ export class OrderService { }; } - await this.dataGatheringService.addJobToQueue( - GATHER_ASSET_PROFILE_PROCESS, - { + await this.dataGatheringService.addJobToQueue({ + data: { dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, symbol: data.SymbolProfile.connectOrCreate.create.symbol }, - { + name: GATHER_ASSET_PROFILE_PROCESS, + opts: { ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}}` } - ); + }); const isDraft = isAfter(data.date as Date, endOfToday()); diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index 69906dfa9..bf186b32f 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -19,8 +19,8 @@ export class CronService { private readonly twitterBotService: TwitterBotService ) {} - @Cron(CronExpression.EVERY_4_HOURS) - public async runEveryFourHours() { + @Cron(CronExpression.EVERY_HOUR) + public async runEveryHour() { await this.dataGatheringService.gather7Days(); } @@ -38,18 +38,20 @@ export class CronService { public async runEverySundayAtTwelvePm() { const uniqueAssets = await this.dataGatheringService.getUniqueAssets(); - for (const { dataSource, symbol } of uniqueAssets) { - await this.dataGatheringService.addJobToQueue( - GATHER_ASSET_PROFILE_PROCESS, - { - dataSource, - symbol - }, - { - ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, - jobId: `${dataSource}-${symbol}}` - } - ); - } + await this.dataGatheringService.addJobsToQueue( + uniqueAssets.map(({ dataSource, symbol }) => { + return { + data: { + dataSource, + symbol + }, + name: GATHER_ASSET_PROFILE_PROCESS, + opts: { + ...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, + jobId: `${dataSource}-${symbol}}` + } + }; + }) + ); } } diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 4015cf114..5de70c925 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -11,6 +11,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { DataSource } from '@prisma/client'; import { JobOptions, Queue } from 'bull'; import { format, min, subDays, subYears } from 'date-fns'; +import { isEmpty } from 'lodash'; import { DataProviderService } from './data-provider/data-provider.service'; import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface'; @@ -33,7 +34,15 @@ export class DataGatheringService { private readonly symbolProfileService: SymbolProfileService ) {} - public async addJobToQueue(name: string, data: any, opts?: JobOptions) { + public async addJobToQueue({ + data, + name, + opts + }: { + data: any; + name: string; + opts?: JobOptions; + }) { return this.dataGatheringQueue.add(name, data, opts); } @@ -223,48 +232,6 @@ export class DataGatheringService { ); } - public async getSymbolsMax(): Promise { - const startDate = - ( - await this.prismaService.order.findFirst({ - orderBy: [{ date: 'asc' }] - }) - )?.date ?? new Date(); - - const currencyPairsToGather = this.exchangeRateDataService - .getCurrencyPairs() - .map(({ dataSource, symbol }) => { - return { - dataSource, - symbol, - date: min([startDate, subYears(new Date(), 10)]) - }; - }); - - const symbolProfilesToGather = ( - await this.prismaService.symbolProfile.findMany({ - orderBy: [{ symbol: 'asc' }], - select: { - dataSource: true, - Order: { - orderBy: [{ date: 'asc' }], - select: { date: true }, - take: 1 - }, - scraperConfiguration: true, - symbol: true - } - }) - ).map((symbolProfile) => { - return { - ...symbolProfile, - date: symbolProfile.Order?.[0]?.date ?? startDate - }; - }); - - return [...currencyPairsToGather, ...symbolProfilesToGather]; - } - public async getUniqueAssets(): Promise { const symbolProfiles = await this.prismaService.symbolProfile.findMany({ orderBy: [{ symbol: 'asc' }] @@ -299,7 +266,7 @@ export class DataGatheringService { // Only consider symbols with incomplete market data for the last // 7 days - const symbolsNotToGather = ( + const symbolsWithCompleteMarketData = ( await this.prismaService.marketData.groupBy({ _count: true, by: ['symbol'], @@ -317,8 +284,14 @@ export class DataGatheringService { }); const symbolProfilesToGather = symbolProfiles - .filter(({ symbol }) => { - return !symbolsNotToGather.includes(symbol); + .filter(({ dataSource, scraperConfiguration, symbol }) => { + const manualDataSourceWithScraperConfiguration = + dataSource === 'MANUAL' && !isEmpty(scraperConfiguration); + + return ( + !symbolsWithCompleteMarketData.includes(symbol) && + (dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration) + ); }) .map((symbolProfile) => { return { @@ -330,7 +303,7 @@ export class DataGatheringService { const currencyPairsToGather = this.exchangeRateDataService .getCurrencyPairs() .filter(({ symbol }) => { - return !symbolsNotToGather.includes(symbol); + return !symbolsWithCompleteMarketData.includes(symbol); }) .map(({ dataSource, symbol }) => { return { @@ -342,4 +315,57 @@ export class DataGatheringService { return [...currencyPairsToGather, ...symbolProfilesToGather]; } + + private async getSymbolsMax(): Promise { + const startDate = + ( + await this.prismaService.order.findFirst({ + orderBy: [{ date: 'asc' }] + }) + )?.date ?? new Date(); + + const currencyPairsToGather = this.exchangeRateDataService + .getCurrencyPairs() + .map(({ dataSource, symbol }) => { + return { + dataSource, + symbol, + date: min([startDate, subYears(new Date(), 10)]) + }; + }); + + const symbolProfilesToGather = ( + await this.prismaService.symbolProfile.findMany({ + orderBy: [{ symbol: 'asc' }], + select: { + dataSource: true, + Order: { + orderBy: [{ date: 'asc' }], + select: { date: true }, + take: 1 + }, + scraperConfiguration: true, + symbol: true + } + }) + ) + .filter((symbolProfile) => { + const manualDataSourceWithScraperConfiguration = + symbolProfile.dataSource === 'MANUAL' && + !isEmpty(symbolProfile.scraperConfiguration); + + return ( + symbolProfile.dataSource !== 'MANUAL' || + manualDataSourceWithScraperConfiguration + ); + }) + .map((symbolProfile) => { + return { + ...symbolProfile, + date: symbolProfile.Order?.[0]?.date ?? startDate + }; + }); + + return [...currencyPairsToGather, ...symbolProfilesToGather]; + } } From 56b437ca74dca2db685896e44e04861980a8cc31 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 15 Apr 2023 15:08:56 +0200 Subject: [PATCH 5/6] Bugfix/improve info message style (#1858) * Improve info message style * Update changelog --- CHANGELOG.md | 4 ++++ apps/client/src/styles.scss | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d860290..c98c8e7a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Skipped creating queue jobs for asset profiles with `MANUAL` data source not having a scraper configuration - Reduced the execution interval of the data gathering to every hour +### Fixed + +- Improved the style of the system message + ## 1.254.0 - 2023-04-14 ### Changed diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index 73d7d4ded..942e75294 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -464,7 +464,7 @@ ngx-skeleton-loader { } .with-info-message { - height: calc(100vh - 5rem - 3.5rem) !important; + height: calc(100vh - 5rem - 3.5rem + 0.5rem) !important; } .with-placeholder-as-option { From 4c907d56f068f6be554bac10d071ccfdd94e5bcd Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 15 Apr 2023 15:09:15 +0200 Subject: [PATCH 6/6] Improve message (#1859) --- apps/api/src/app/portfolio/portfolio-calculator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 57835298c..952b81677 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -722,7 +722,7 @@ export class PortfolioCalculator { ); } else if (!currentPosition.quantity.eq(0)) { Logger.warn( - `Missing initial value for symbol ${currentPosition.symbol} at ${currentPosition.firstBuyDate}`, + `Missing historical market data for symbol ${currentPosition.symbol}`, 'PortfolioCalculator' ); hasErrors = true;