diff --git a/apps/api/src/app/admin/queue/queue.module.ts b/apps/api/src/app/admin/queue/queue.module.ts index 4cfb79492..049ce8ea6 100644 --- a/apps/api/src/app/admin/queue/queue.module.ts +++ b/apps/api/src/app/admin/queue/queue.module.ts @@ -1,6 +1,7 @@ import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { StatisticsGatheringQueueModule } from '@ghostfolio/api/services/queues/statistics-gathering/statistics-gathering.module'; +import { WatchlistComputationQueueModule } from '@ghostfolio/api/services/queues/watchlist/watchlist.module'; import { Module } from '@nestjs/common'; @@ -12,7 +13,8 @@ import { QueueService } from './queue.service'; imports: [ DataGatheringQueueModule, PortfolioSnapshotQueueModule, - StatisticsGatheringQueueModule + StatisticsGatheringQueueModule, + WatchlistComputationQueueModule ], providers: [QueueService] }) diff --git a/apps/api/src/app/admin/queue/queue.service.ts b/apps/api/src/app/admin/queue/queue.service.ts index f47b3d3a1..6dc52becb 100644 --- a/apps/api/src/app/admin/queue/queue.service.ts +++ b/apps/api/src/app/admin/queue/queue.service.ts @@ -2,7 +2,8 @@ import { DATA_GATHERING_QUEUE, PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, QUEUE_JOB_STATUS_LIST, - STATISTICS_GATHERING_QUEUE + STATISTICS_GATHERING_QUEUE, + WATCHLIST_COMPUTATION_QUEUE } from '@ghostfolio/common/config'; import { AdminJobs } from '@ghostfolio/common/interfaces'; @@ -18,7 +19,9 @@ export class QueueService { @InjectQueue(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) private readonly portfolioSnapshotQueue: Queue, @InjectQueue(STATISTICS_GATHERING_QUEUE) - private readonly statisticsGatheringQueue: Queue + private readonly statisticsGatheringQueue: Queue, + @InjectQueue(WATCHLIST_COMPUTATION_QUEUE) + private readonly watchlistComputationQueue: Queue ) {} public async deleteJob(aId: string) { @@ -28,6 +31,10 @@ export class QueueService { job = await this.portfolioSnapshotQueue.getJob(aId); } + if (!job) { + job = await this.watchlistComputationQueue.getJob(aId); + } + return job?.remove(); } @@ -42,6 +49,7 @@ export class QueueService { await this.dataGatheringQueue.clean(300, queueStatus); await this.portfolioSnapshotQueue.clean(300, queueStatus); await this.statisticsGatheringQueue.clean(300, queueStatus); + await this.watchlistComputationQueue.clean(300, queueStatus); } } @@ -52,6 +60,10 @@ export class QueueService { job = await this.portfolioSnapshotQueue.getJob(aId); } + if (!job) { + job = await this.watchlistComputationQueue.getJob(aId); + } + return job?.promote(); } @@ -62,18 +74,24 @@ export class QueueService { limit?: number; status?: JobStatus[]; }): Promise { - const [dataGatheringJobs, portfolioSnapshotJobs, statisticsGatheringJobs] = - await Promise.all([ - this.dataGatheringQueue.getJobs(status), - this.portfolioSnapshotQueue.getJobs(status), - this.statisticsGatheringQueue.getJobs(status) - ]); + const [ + dataGatheringJobs, + portfolioSnapshotJobs, + statisticsGatheringJobs, + watchlistComputationJobs + ] = await Promise.all([ + this.dataGatheringQueue.getJobs(status), + this.portfolioSnapshotQueue.getJobs(status), + this.statisticsGatheringQueue.getJobs(status), + this.watchlistComputationQueue.getJobs(status) + ]); const jobsWithState = await Promise.all( [ ...dataGatheringJobs, ...portfolioSnapshotJobs, - ...statisticsGatheringJobs + ...statisticsGatheringJobs, + ...watchlistComputationJobs ] .filter((job) => { return job; diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 0a27faa64..201183904 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -10,6 +10,7 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; +import { WatchlistComputationQueueModule } from '@ghostfolio/api/services/queues/watchlist/watchlist.module'; import { BULL_BOARD_ROUTE, DEFAULT_LANGUAGE_CODE, @@ -167,6 +168,7 @@ import { UserModule } from './user/user.module'; SymbolModule, TagsModule, UserModule, + WatchlistComputationQueueModule, WatchlistModule ], providers: [I18nService] diff --git a/apps/api/src/app/endpoints/watchlist/interfaces/watchlist-value.interface.ts b/apps/api/src/app/endpoints/watchlist/interfaces/watchlist-value.interface.ts new file mode 100644 index 000000000..36f2aa201 --- /dev/null +++ b/apps/api/src/app/endpoints/watchlist/interfaces/watchlist-value.interface.ts @@ -0,0 +1,6 @@ +import { WatchlistResponse } from '@ghostfolio/common/interfaces'; + +export interface WatchlistValue { + expiration: number; + watchlist: WatchlistResponse['watchlist']; +} diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.module.ts b/apps/api/src/app/endpoints/watchlist/watchlist.module.ts index 9b4b960a0..8c824e7fc 100644 --- a/apps/api/src/app/endpoints/watchlist/watchlist.module.ts +++ b/apps/api/src/app/endpoints/watchlist/watchlist.module.ts @@ -1,11 +1,11 @@ +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; -import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; -import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; +import { WatchlistComputationQueueModule } from '@ghostfolio/api/services/queues/watchlist/watchlist.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { Module } from '@nestjs/common'; @@ -16,15 +16,15 @@ import { WatchlistService } from './watchlist.service'; @Module({ controllers: [WatchlistController], imports: [ - BenchmarkModule, DataGatheringQueueModule, DataProviderModule, ImpersonationModule, - MarketDataModule, PrismaModule, + RedisCacheModule, SymbolProfileModule, TransformDataSourceInRequestModule, - TransformDataSourceInResponseModule + TransformDataSourceInResponseModule, + WatchlistComputationQueueModule ], providers: [WatchlistService] }) diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts index 666023dbf..285a352d7 100644 --- a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts +++ b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts @@ -1,23 +1,34 @@ -import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; -import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; +import { WatchlistComputationService } from '@ghostfolio/api/services/queues/watchlist/watchlist-computation.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { + WATCHLIST_COMPUTATION_QUEUE_PRIORITY_HIGH, + WATCHLIST_COMPUTATION_QUEUE_PRIORITY_LOW, + WATCHLIST_PROCESS_JOB_NAME, + WATCHLIST_PROCESS_JOB_OPTIONS +} from '@ghostfolio/common/config'; import { WatchlistResponse } from '@ghostfolio/common/interfaces'; -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { DataSource, Prisma } from '@prisma/client'; +import { isAfter } from 'date-fns'; + +import { WatchlistValue } from './interfaces/watchlist-value.interface'; @Injectable() export class WatchlistService { + private readonly logger = new Logger(WatchlistService.name); + public constructor( - private readonly benchmarkService: BenchmarkService, private readonly dataGatheringService: DataGatheringService, private readonly dataProviderService: DataProviderService, - private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, - private readonly symbolProfileService: SymbolProfileService + private readonly redisCacheService: RedisCacheService, + private readonly symbolProfileService: SymbolProfileService, + private readonly watchlistComputationService: WatchlistComputationService ) {} public async createWatchlistItem({ @@ -66,6 +77,10 @@ export class WatchlistService { }, where: { id: userId } }); + + await this.redisCacheService.remove( + this.redisCacheService.getWatchlistKey({ userId }) + ); } public async deleteWatchlistItem({ @@ -87,69 +102,69 @@ export class WatchlistService { }, where: { id: userId } }); + + await this.redisCacheService.remove( + this.redisCacheService.getWatchlistKey({ userId }) + ); } public async getWatchlistItems( userId: string ): Promise { - const user = await this.prismaService.user.findUnique({ - select: { - watchlist: { - select: { dataSource: true, symbol: true } - } - }, - where: { id: userId } - }); + let cachedWatchlist: WatchlistResponse['watchlist']; + let isCachedWatchlistExpired = false; + + try { + const cachedWatchlistValue = await this.redisCacheService.get( + this.redisCacheService.getWatchlistKey({ userId }) + ); - const [assetProfiles, quotes] = await Promise.all([ - this.symbolProfileService.getSymbolProfiles(user.watchlist), - this.dataProviderService.getQuotes({ - items: user.watchlist.map(({ dataSource, symbol }) => { - return { dataSource, symbol }; - }) - }) - ]); - - const watchlist = await Promise.all( - user.watchlist.map(async ({ dataSource, symbol }) => { - const assetProfile = assetProfiles.find((profile) => { - return profile.dataSource === dataSource && profile.symbol === symbol; + const { expiration, watchlist }: WatchlistValue = + JSON.parse(cachedWatchlistValue); + + cachedWatchlist = watchlist; + + if (isAfter(new Date(), new Date(expiration))) { + isCachedWatchlistExpired = true; + } + } catch {} + + if (cachedWatchlist) { + this.logger.debug(`Fetched watchlist of user '${userId}' from cache`); + + if (isCachedWatchlistExpired) { + // Compute in the background + this.watchlistComputationService.addJobToQueue({ + data: { userId }, + name: WATCHLIST_PROCESS_JOB_NAME, + opts: { + ...WATCHLIST_PROCESS_JOB_OPTIONS, + jobId: userId, + priority: WATCHLIST_COMPUTATION_QUEUE_PRIORITY_LOW + } }); + } - const [allTimeHigh, trends] = await Promise.all([ - this.marketDataService.getMax({ - dataSource, - symbol - }), - this.benchmarkService.getBenchmarkTrends({ dataSource, symbol }) - ]); - - const performancePercent = - this.benchmarkService.calculateChangeInPercentage( - allTimeHigh?.marketPrice, - quotes[symbol]?.marketPrice - ); - - return { - dataSource, - symbol, - marketCondition: - this.benchmarkService.getMarketCondition(performancePercent), - name: assetProfile?.name, - performances: { - allTimeHigh: { - performancePercent, - date: allTimeHigh?.date - } - }, - trend50d: trends.trend50d, - trend200d: trends.trend200d - }; - }) - ); + return cachedWatchlist; + } - return watchlist.sort((a, b) => { - return a.name.localeCompare(b.name); + // Wait for computation + await this.watchlistComputationService.addJobToQueue({ + data: { userId }, + name: WATCHLIST_PROCESS_JOB_NAME, + opts: { + ...WATCHLIST_PROCESS_JOB_OPTIONS, + jobId: userId, + priority: WATCHLIST_COMPUTATION_QUEUE_PRIORITY_HIGH + } }); + + const job = await this.watchlistComputationService.getJob(userId); + + if (job) { + await job.finished(); + } + + return this.getWatchlistItems(userId); } } diff --git a/apps/api/src/app/redis-cache/redis-cache.service.ts b/apps/api/src/app/redis-cache/redis-cache.service.ts index b87740f8c..8dc8dbc96 100644 --- a/apps/api/src/app/redis-cache/redis-cache.service.ts +++ b/apps/api/src/app/redis-cache/redis-cache.service.ts @@ -76,6 +76,10 @@ export class RedisCacheService { return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`; } + public getWatchlistKey({ userId }: { userId: string }) { + return `watchlist-${userId}`; + } + public async isHealthy() { const HEALTH_CHECK_TIMEOUT = ms('5 seconds'); diff --git a/apps/api/src/services/queues/watchlist/interfaces/watchlist-queue-job.interface.ts b/apps/api/src/services/queues/watchlist/interfaces/watchlist-queue-job.interface.ts new file mode 100644 index 000000000..947724dc0 --- /dev/null +++ b/apps/api/src/services/queues/watchlist/interfaces/watchlist-queue-job.interface.ts @@ -0,0 +1,3 @@ +export interface WatchlistQueueJob { + userId: string; +} diff --git a/apps/api/src/services/queues/watchlist/watchlist-computation.service.ts b/apps/api/src/services/queues/watchlist/watchlist-computation.service.ts new file mode 100644 index 000000000..8201fcebd --- /dev/null +++ b/apps/api/src/services/queues/watchlist/watchlist-computation.service.ts @@ -0,0 +1,31 @@ +import { WATCHLIST_COMPUTATION_QUEUE } from '@ghostfolio/common/config'; + +import { InjectQueue } from '@nestjs/bull'; +import { Injectable } from '@nestjs/common'; +import { JobOptions, Queue } from 'bull'; + +import { WatchlistQueueJob } from './interfaces/watchlist-queue-job.interface'; + +@Injectable() +export class WatchlistComputationService { + public constructor( + @InjectQueue(WATCHLIST_COMPUTATION_QUEUE) + private readonly watchlistComputationQueue: Queue + ) {} + + public async addJobToQueue({ + data, + name, + opts + }: { + data: WatchlistQueueJob; + name: string; + opts?: JobOptions; + }) { + return this.watchlistComputationQueue.add(name, data, opts); + } + + public async getJob(jobId: string) { + return this.watchlistComputationQueue.getJob(jobId); + } +} diff --git a/apps/api/src/services/queues/watchlist/watchlist.module.ts b/apps/api/src/services/queues/watchlist/watchlist.module.ts new file mode 100644 index 000000000..50245854a --- /dev/null +++ b/apps/api/src/services/queues/watchlist/watchlist.module.ts @@ -0,0 +1,52 @@ +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; +import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { WatchlistComputationService } from '@ghostfolio/api/services/queues/watchlist/watchlist-computation.service'; +import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; +import { + DEFAULT_PROCESSOR_WATCHLIST_COMPUTATION_TIMEOUT, + WATCHLIST_COMPUTATION_QUEUE +} from '@ghostfolio/common/config'; + +import { BullAdapter } from '@bull-board/api/bullAdapter'; +import { BullBoardModule } from '@bull-board/nestjs'; +import { BullModule } from '@nestjs/bull'; +import { Module } from '@nestjs/common'; + +import { WatchlistProcessor } from './watchlist.processor'; + +@Module({ + exports: [BullModule, WatchlistComputationService], + imports: [ + BenchmarkModule, + BullBoardModule.forFeature({ + adapter: BullAdapter, + name: WATCHLIST_COMPUTATION_QUEUE, + options: { + displayName: 'Watchlist Computation', + readOnlyMode: process.env.BULL_BOARD_IS_READ_ONLY !== 'false' + } + }), + BullModule.registerQueue({ + name: WATCHLIST_COMPUTATION_QUEUE, + settings: { + lockDuration: parseInt( + process.env.PROCESSOR_WATCHLIST_COMPUTATION_TIMEOUT ?? + DEFAULT_PROCESSOR_WATCHLIST_COMPUTATION_TIMEOUT.toString(), + 10 + ) + } + }), + ConfigurationModule, + DataProviderModule, + MarketDataModule, + PrismaModule, + RedisCacheModule, + SymbolProfileModule + ], + providers: [WatchlistComputationService, WatchlistProcessor] +}) +export class WatchlistComputationQueueModule {} diff --git a/apps/api/src/services/queues/watchlist/watchlist.processor.ts b/apps/api/src/services/queues/watchlist/watchlist.processor.ts new file mode 100644 index 000000000..b5c5cf815 --- /dev/null +++ b/apps/api/src/services/queues/watchlist/watchlist.processor.ts @@ -0,0 +1,152 @@ +import { WatchlistValue } from '@ghostfolio/api/app/endpoints/watchlist/interfaces/watchlist-value.interface'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; +import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { + CACHE_TTL_INFINITE, + DEFAULT_PROCESSOR_WATCHLIST_COMPUTATION_CONCURRENCY, + WATCHLIST_COMPUTATION_QUEUE, + WATCHLIST_PROCESS_JOB_NAME +} from '@ghostfolio/common/config'; +import { WatchlistResponse } from '@ghostfolio/common/interfaces'; + +import { Process, Processor } from '@nestjs/bull'; +import { Injectable, Logger } from '@nestjs/common'; +import { Job } from 'bull'; +import { addMilliseconds } from 'date-fns'; + +import { WatchlistQueueJob } from './interfaces/watchlist-queue-job.interface'; + +@Injectable() +@Processor(WATCHLIST_COMPUTATION_QUEUE) +export class WatchlistProcessor { + private readonly logger = new Logger(WatchlistProcessor.name); + + public constructor( + private readonly benchmarkService: BenchmarkService, + private readonly configurationService: ConfigurationService, + private readonly dataProviderService: DataProviderService, + private readonly marketDataService: MarketDataService, + private readonly prismaService: PrismaService, + private readonly redisCacheService: RedisCacheService, + private readonly symbolProfileService: SymbolProfileService + ) {} + + @Process({ + concurrency: parseInt( + process.env.PROCESSOR_WATCHLIST_COMPUTATION_CONCURRENCY ?? + DEFAULT_PROCESSOR_WATCHLIST_COMPUTATION_CONCURRENCY.toString(), + 10 + ), + name: WATCHLIST_PROCESS_JOB_NAME + }) + public async calculateWatchlist(job: Job) { + try { + const startTime = performance.now(); + const { userId } = job.data; + + this.logger.log( + `Watchlist calculation of user '${userId}' has been started` + ); + + const user = await this.prismaService.user.findUnique({ + select: { + watchlist: { + select: { dataSource: true, symbol: true } + } + }, + where: { id: userId } + }); + + const [assetProfiles, quotes] = await Promise.all([ + this.symbolProfileService.getSymbolProfiles(user.watchlist), + this.dataProviderService.getQuotes({ + items: user.watchlist.map(({ dataSource, symbol }) => { + return { dataSource, symbol }; + }) + }) + ]); + + let isComplete = user.watchlist.length > 0; + + const watchlist: WatchlistResponse['watchlist'] = await Promise.all( + user.watchlist.map(async ({ dataSource, symbol }) => { + const assetProfile = assetProfiles.find((profile) => { + return ( + profile.dataSource === dataSource && profile.symbol === symbol + ); + }); + + const [allTimeHigh, trends] = await Promise.all([ + this.marketDataService.getMax({ + dataSource, + symbol + }), + this.benchmarkService.getBenchmarkTrends({ dataSource, symbol }) + ]); + + if (!allTimeHigh?.marketPrice || !quotes[symbol]?.marketPrice) { + isComplete = false; + } + + const performancePercent = + this.benchmarkService.calculateChangeInPercentage( + allTimeHigh?.marketPrice, + quotes[symbol]?.marketPrice + ); + + return { + dataSource, + symbol, + marketCondition: + this.benchmarkService.getMarketCondition(performancePercent), + name: assetProfile?.name, + performances: { + allTimeHigh: { + performancePercent, + date: allTimeHigh?.date + } + }, + trend50d: trends.trend50d, + trend200d: trends.trend200d + }; + }) + ); + + const sortedWatchlist = watchlist.sort((a, b) => { + return a.name.localeCompare(b.name); + }); + + this.logger.log( + `Watchlist calculation of user '${userId}' has been completed in ${( + (performance.now() - startTime) / + 1000 + ).toFixed(3)} seconds` + ); + + const expiration = addMilliseconds( + new Date(), + isComplete ? this.configurationService.get('CACHE_QUOTES_TTL') : 0 + ); + + await this.redisCacheService.set( + this.redisCacheService.getWatchlistKey({ userId }), + JSON.stringify({ + expiration: expiration.getTime(), + watchlist: sortedWatchlist + } as WatchlistValue), + CACHE_TTL_INFINITE + ); + + return sortedWatchlist; + } catch (error) { + this.logger.error(error); + + throw new Error(error); + } + } +} diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 7e7cd2ba5..3745b8681 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -79,6 +79,10 @@ export const PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW = export const STATISTICS_GATHERING_QUEUE = 'STATISTICS_GATHERING_QUEUE'; +export const WATCHLIST_COMPUTATION_QUEUE = 'WATCHLIST_COMPUTATION_QUEUE'; +export const WATCHLIST_COMPUTATION_QUEUE_PRIORITY_HIGH = 1; +export const WATCHLIST_COMPUTATION_QUEUE_PRIORITY_LOW = Number.MAX_SAFE_INTEGER; + export const DEFAULT_COLOR_SCHEME: ColorScheme = 'LIGHT'; export const DEFAULT_CURRENCY = 'USD'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; @@ -91,6 +95,8 @@ export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; +export const DEFAULT_PROCESSOR_WATCHLIST_COMPUTATION_CONCURRENCY = 1; +export const DEFAULT_PROCESSOR_WATCHLIST_COMPUTATION_TIMEOUT = 30000; export const DEFAULT_REDACTED_PATHS = [ 'accounts[*].balance', @@ -221,6 +227,11 @@ export const PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS: JobOptions = { removeOnComplete: true }; +export const WATCHLIST_PROCESS_JOB_NAME = 'WATCHLIST'; +export const WATCHLIST_PROCESS_JOB_OPTIONS: JobOptions = { + removeOnComplete: true +}; + export const HEADER_KEY_IMPERSONATION = 'Impersonation-Id'; export const HEADER_KEY_TIMEZONE = 'Timezone'; export const HEADER_KEY_TOKEN = 'Authorization';