Browse Source

Move computeSnapshot to processor

pull/3725/head
Thomas Kaul 12 months ago
parent
commit
926c42cbc4
  1. 30
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  2. 27
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
  3. 21
      apps/api/src/app/redis-cache/redis-cache.service.mock.ts
  4. 3
      apps/api/src/services/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts
  5. 2
      apps/api/src/services/portfolio-snapshot/portfolio-snapshot.module.ts
  6. 52
      apps/api/src/services/portfolio-snapshot/portfolio-snapshot.processor.ts
  7. 28
      apps/api/src/services/portfolio-snapshot/portfolio-snapshot.service.mock.ts

30
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -12,7 +12,6 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/portfolio-snapshot/portfolio-snapshot.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { CACHE_TTL_INFINITE } from '@ghostfolio/common/config';
import {
PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS
@ -39,7 +38,6 @@ import { Logger } from '@nestjs/common';
import { Big } from 'big.js';
import { plainToClass } from 'class-transformer';
import {
addMilliseconds,
differenceInDays,
eachDayOfInterval,
endOfDay,
@ -875,29 +873,6 @@ export abstract class PortfolioCalculator {
return chartDateMap;
}
private async computeAndCacheSnapshot() {
const snapshot = await this.computeSnapshot();
const expiration = addMilliseconds(
new Date(),
this.configurationService.get('CACHE_QUOTES_TTL')
);
this.redisCacheService.set(
this.redisCacheService.getPortfolioSnapshotKey({
filters: this.filters,
userId: this.userId
}),
JSON.stringify(<PortfolioSnapshotValue>(<unknown>{
expiration: expiration.getTime(),
portfolioSnapshot: snapshot
})),
CACHE_TTL_INFINITE
);
return snapshot;
}
@LogPerformance
private computeTransactionPoints() {
this.transactionPoints = [];
@ -1080,6 +1055,7 @@ export abstract class PortfolioCalculator {
// Compute in the background
this.portfolioSnapshotService.addJobToQueue({
data: {
filters: this.filters,
userId: this.userId
},
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
@ -1089,13 +1065,13 @@ export abstract class PortfolioCalculator {
// priority
}
});
this.computeAndCacheSnapshot();
}
} else {
// Wait for computation
// TODO
const job = await this.portfolioSnapshotService.addJobToQueue({
data: {
filters: this.filters,
userId: this.userId
},
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
@ -1108,7 +1084,7 @@ export abstract class PortfolioCalculator {
await job.finished();
this.snapshot = await this.computeAndCacheSnapshot();
await this.initialize();
}
}
}

27
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -14,6 +14,9 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotProcessor } from '@ghostfolio/api/services/portfolio-snapshot/portfolio-snapshot.processor';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
@ -28,6 +31,18 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
};
});
jest.mock(
'@ghostfolio/api/services/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -42,6 +57,7 @@ describe('PortfolioCalculator', () => {
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeEach(() => {
@ -56,12 +72,15 @@ describe('PortfolioCalculator', () => {
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null);
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
@ -110,7 +129,13 @@ describe('PortfolioCalculator', () => {
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.getSnapshot();
const portfolioSnapshotProcessor = new PortfolioSnapshotProcessor(
null,
null
);
const portfolioSnapshot =
await portfolioSnapshotProcessor.computeSnapshot();
const investments = portfolioCalculator.getInvestments();

21
apps/api/src/app/redis-cache/redis-cache.service.mock.ts

@ -1,13 +1,28 @@
import { Filter } from '@ghostfolio/common/interfaces';
import { Milliseconds } from 'cache-manager';
export const RedisCacheServiceMock = {
cache: new Map<string, string>(),
get: (key: string): Promise<string> => {
return Promise.resolve(null);
const value = RedisCacheServiceMock.cache.get(key) || null;
return Promise.resolve(value);
},
getPortfolioSnapshotKey: (userId: string): string => {
return `portfolio-snapshot-${userId}`;
getPortfolioSnapshotKey: ({
filters,
userId
}: {
filters?: Filter[];
userId: string;
}): string => {
const filtersHash = filters?.length;
return `portfolio-snapshot-${userId}-${filtersHash}`;
},
set: (key: string, value: string, ttl?: Milliseconds): Promise<string> => {
RedisCacheServiceMock.cache.set(key, value);
return Promise.resolve(value);
}
};

3
apps/api/src/services/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts

@ -1,3 +1,6 @@
import { Filter } from '@ghostfolio/common/interfaces';
export interface IPortfolioSnapshotQueueJob {
filters: Filter[];
userId: string;
}

2
apps/api/src/services/portfolio-snapshot/portfolio-snapshot.module.ts

@ -1,3 +1,4 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
@ -28,6 +29,7 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor';
MarketDataModule,
PrismaModule,
PropertyModule,
RedisCacheModule,
SymbolProfileModule
],
providers: [PortfolioSnapshotProcessor, PortfolioSnapshotService],

52
apps/api/src/services/portfolio-snapshot/portfolio-snapshot.processor.ts

@ -1,20 +1,28 @@
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
CACHE_TTL_INFINITE,
PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
PORTFOLIO_SNAPSHOT_QUEUE
} from '@ghostfolio/common/config';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { Process, Processor } from '@nestjs/bull';
import { Injectable, Logger } from '@nestjs/common';
import * as Big from 'big.js';
import { Job } from 'bull';
import ms from 'ms';
import { setTimeout } from 'timers/promises';
import { addMilliseconds } from 'date-fns';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
@Injectable()
@Processor(PORTFOLIO_SNAPSHOT_QUEUE)
export class PortfolioSnapshotProcessor {
public constructor() {}
public constructor(
private readonly configurationService: ConfigurationService,
private readonly redisCacheService: RedisCacheService
) {}
@Process({ concurrency: 1, name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME })
public async calculatePortfolioSnapshot(
@ -26,13 +34,31 @@ export class PortfolioSnapshotProcessor {
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
);
// TODO: Do something
await setTimeout(ms('1 second'));
const snapshot = await this.computeSnapshot();
Logger.log(
`Portfolio snapshot calculation of user ${job.data.userId} has been completed`,
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
);
const expiration = addMilliseconds(
new Date(),
this.configurationService.get('CACHE_QUOTES_TTL')
);
this.redisCacheService.set(
this.redisCacheService.getPortfolioSnapshotKey({
filters: job.data.filters,
userId: job.data.userId
}),
JSON.stringify(<PortfolioSnapshotValue>(<unknown>{
expiration: expiration.getTime(),
portfolioSnapshot: snapshot
})),
CACHE_TTL_INFINITE
);
return snapshot;
} catch (error) {
Logger.error(
error,
@ -42,4 +68,20 @@ export class PortfolioSnapshotProcessor {
throw new Error(error);
}
}
// TODO
public async computeSnapshot(): Promise<PortfolioSnapshot> {
return {
currentValueInBaseCurrency: new Big(0),
hasErrors: false,
historicalData: [],
positions: [],
totalFeesWithCurrencyEffect: new Big(0),
totalInterestWithCurrencyEffect: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0),
totalLiabilitiesWithCurrencyEffect: new Big(0),
totalValuablesWithCurrencyEffect: new Big(0)
};
}
}

28
apps/api/src/services/portfolio-snapshot/portfolio-snapshot.service.mock.ts

@ -0,0 +1,28 @@
import * as Bull from 'bull';
import { setTimeout } from 'timers/promises';
import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface';
export const PortfolioSnapshotServiceMock = {
addJobToQueue({
data,
name,
opts
}: {
data: IPortfolioSnapshotQueueJob;
name: string;
opts?: Bull.JobOptions;
}): Promise<Bull.Job<any>> {
// Mock the Job object with a finished method
const mockJob: Partial<Bull.Job<any>> = {
// Mock the finished method to return a resolved promise
finished: async () => {
await setTimeout(100);
return Promise.resolve('Mocked job finished result');
}
};
return Promise.resolve(mockJob as Bull.Job<any>);
}
};
Loading…
Cancel
Save