Browse Source

Feature/set up caching in portfolio calculator (#3335)

* Set up caching

* Update changelog
pull/3338/head
Thomas Kaul 9 months ago
committed by GitHub
parent
commit
4f41bac328
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 8
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  3. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  4. 26
      apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts
  5. 108
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  6. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  7. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts
  8. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts
  9. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  10. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts
  11. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts
  12. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts
  13. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts
  14. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
  15. 26
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts
  16. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  17. 28
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
  18. 12
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts
  19. 8
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  20. 24
      apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts
  21. 2
      apps/api/src/app/portfolio/portfolio.module.ts
  22. 34
      apps/api/src/app/portfolio/portfolio.service.ts
  23. 13
      apps/api/src/app/redis-cache/redis-cache.service.mock.ts
  24. 4
      apps/api/src/app/redis-cache/redis-cache.service.ts
  25. 3
      apps/api/src/events/events.module.ts
  26. 8
      apps/api/src/events/portfolio-changed.listener.ts
  27. 3
      apps/api/src/models/rule.ts
  28. 3
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  29. 3
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  30. 13
      apps/api/src/services/data-provider/data-provider.service.ts
  31. 9
      libs/common/src/lib/class-transformer.ts
  32. 2
      libs/common/src/lib/interfaces/index.ts
  33. 31
      libs/common/src/lib/interfaces/timeline-position.interface.ts
  34. 4
      libs/common/src/lib/models/index.ts
  35. 82
      libs/common/src/lib/models/portfolio-snapshot.ts
  36. 92
      libs/common/src/lib/models/timeline-position.ts

2
CHANGELOG.md

@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Extended the content of the _Self-Hosting_ section by the custom asset instructions on the Frequently Asked Questions (FAQ) page - Extended the content of the _Self-Hosting_ section by the custom asset instructions on the Frequently Asked Questions (FAQ) page
- Set up an event system to follow portfolio changes - Added the caching to the portfolio calculator (experimental)
### Changed ### Changed

8
apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts

@ -1,10 +1,6 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface'; import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces';
import { import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
SymbolMetrics,
TimelinePosition,
UniqueAsset
} from '@ghostfolio/common/interfaces';
export class MWRPortfolioCalculator extends PortfolioCalculator { export class MWRPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance( protected calculateOverallPerformance(

4
apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts

@ -24,3 +24,7 @@ export const symbolProfileDummyData = {
sectors: [], sectors: [],
updatedAt: undefined updatedAt: undefined
}; };
export const userDummyData = {
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
};

26
apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts

@ -1,8 +1,10 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange, UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -18,8 +20,10 @@ export enum PerformanceCalculationType {
@Injectable() @Injectable()
export class PortfolioCalculatorFactory { export class PortfolioCalculatorFactory {
public constructor( public constructor(
private readonly configurationService: ConfigurationService,
private readonly currentRateService: CurrentRateService, private readonly currentRateService: CurrentRateService,
private readonly exchangeRateDataService: ExchangeRateDataService private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly redisCacheService: RedisCacheService
) {} ) {}
public createCalculator({ public createCalculator({
@ -27,13 +31,17 @@ export class PortfolioCalculatorFactory {
activities, activities,
calculationType, calculationType,
currency, currency,
dateRange = 'max' dateRange = 'max',
isExperimentalFeatures = false,
userId
}: { }: {
accountBalanceItems?: HistoricalDataItem[]; accountBalanceItems?: HistoricalDataItem[];
activities: Activity[]; activities: Activity[];
calculationType: PerformanceCalculationType; calculationType: PerformanceCalculationType;
currency: string; currency: string;
dateRange?: DateRange; dateRange?: DateRange;
isExperimentalFeatures?: boolean;
userId: string;
}): PortfolioCalculator { }): PortfolioCalculator {
switch (calculationType) { switch (calculationType) {
case PerformanceCalculationType.MWR: case PerformanceCalculationType.MWR:
@ -42,8 +50,12 @@ export class PortfolioCalculatorFactory {
activities, activities,
currency, currency,
dateRange, dateRange,
isExperimentalFeatures,
userId,
configurationService: this.configurationService,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService exchangeRateDataService: this.exchangeRateDataService,
redisCacheService: this.redisCacheService
}); });
case PerformanceCalculationType.TWR: case PerformanceCalculationType.TWR:
return new TWRPortfolioCalculator({ return new TWRPortfolioCalculator({
@ -52,7 +64,11 @@ export class PortfolioCalculatorFactory {
currency, currency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
dateRange, dateRange,
exchangeRateDataService: this.exchangeRateDataService isExperimentalFeatures,
userId,
configurationService: this.configurationService,
exchangeRateDataService: this.exchangeRateDataService,
redisCacheService: this.redisCacheService
}); });
default: default:
throw new Error('Invalid calculation type'); throw new Error('Invalid calculation type');

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

@ -1,13 +1,14 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface';
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { import {
getFactor, getFactor,
getInterval getInterval
} from '@ghostfolio/api/helper/portfolio.helper'; } from '@ghostfolio/api/helper/portfolio.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MAX_CHART_ITEMS } from '@ghostfolio/common/config'; import { MAX_CHART_ITEMS } from '@ghostfolio/common/config';
@ -23,12 +24,14 @@ import {
InvestmentItem, InvestmentItem,
ResponseError, ResponseError,
SymbolMetrics, SymbolMetrics,
TimelinePosition,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
import { DateRange, GroupBy } from '@ghostfolio/common/types'; import { DateRange, GroupBy } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { plainToClass } from 'class-transformer';
import { import {
differenceInDays, differenceInDays,
eachDayOfInterval, eachDayOfInterval,
@ -41,6 +44,7 @@ import {
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { first, last, uniq, uniqBy } from 'lodash'; import { first, last, uniq, uniqBy } from 'lodash';
import ms from 'ms';
export abstract class PortfolioCalculator { export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false; protected static readonly ENABLE_LOGGING = false;
@ -48,52 +52,78 @@ export abstract class PortfolioCalculator {
protected accountBalanceItems: HistoricalDataItem[]; protected accountBalanceItems: HistoricalDataItem[];
protected orders: PortfolioOrder[]; protected orders: PortfolioOrder[];
private configurationService: ConfigurationService;
private currency: string; private currency: string;
private currentRateService: CurrentRateService; private currentRateService: CurrentRateService;
private dataProviderInfos: DataProviderInfo[]; private dataProviderInfos: DataProviderInfo[];
private endDate: Date; private endDate: Date;
private exchangeRateDataService: ExchangeRateDataService; private exchangeRateDataService: ExchangeRateDataService;
private isExperimentalFeatures: boolean;
private redisCacheService: RedisCacheService;
private snapshot: PortfolioSnapshot; private snapshot: PortfolioSnapshot;
private snapshotPromise: Promise<void>; private snapshotPromise: Promise<void>;
private startDate: Date; private startDate: Date;
private transactionPoints: TransactionPoint[]; private transactionPoints: TransactionPoint[];
private userId: string;
public constructor({ public constructor({
accountBalanceItems, accountBalanceItems,
activities, activities,
configurationService,
currency, currency,
currentRateService, currentRateService,
dateRange, dateRange,
exchangeRateDataService exchangeRateDataService,
isExperimentalFeatures,
redisCacheService,
userId
}: { }: {
accountBalanceItems: HistoricalDataItem[]; accountBalanceItems: HistoricalDataItem[];
activities: Activity[]; activities: Activity[];
configurationService: ConfigurationService;
currency: string; currency: string;
currentRateService: CurrentRateService; currentRateService: CurrentRateService;
dateRange: DateRange; dateRange: DateRange;
exchangeRateDataService: ExchangeRateDataService; exchangeRateDataService: ExchangeRateDataService;
isExperimentalFeatures: boolean;
redisCacheService: RedisCacheService;
userId: string;
}) { }) {
this.accountBalanceItems = accountBalanceItems; this.accountBalanceItems = accountBalanceItems;
this.configurationService = configurationService;
this.currency = currency; this.currency = currency;
this.currentRateService = currentRateService; this.currentRateService = currentRateService;
this.exchangeRateDataService = exchangeRateDataService; this.exchangeRateDataService = exchangeRateDataService;
this.orders = activities.map( this.isExperimentalFeatures = isExperimentalFeatures;
({ date, fee, quantity, SymbolProfile, tags = [], type, unitPrice }) => {
return { this.orders = activities
.map(
({
date,
fee,
quantity,
SymbolProfile, SymbolProfile,
tags, tags = [],
type, type,
date: format(date, DATE_FORMAT), unitPrice
fee: new Big(fee), }) => {
quantity: new Big(quantity), return {
unitPrice: new Big(unitPrice) SymbolProfile,
}; tags,
} type,
); date: format(date, DATE_FORMAT),
fee: new Big(fee),
quantity: new Big(quantity),
unitPrice: new Big(unitPrice)
};
}
)
.sort((a, b) => {
return a.date?.localeCompare(b.date);
});
this.orders.sort((a, b) => { this.redisCacheService = redisCacheService;
return a.date?.localeCompare(b.date); this.userId = userId;
});
const { endDate, startDate } = getInterval(dateRange); const { endDate, startDate } = getInterval(dateRange);
@ -1011,6 +1041,48 @@ export abstract class PortfolioCalculator {
} }
private async initialize() { private async initialize() {
this.snapshot = await this.computeSnapshot(this.startDate, this.endDate); if (this.isExperimentalFeatures) {
const startTimeTotal = performance.now();
const cachedSnapshot = await this.redisCacheService.get(
this.redisCacheService.getPortfolioSnapshotKey(this.userId)
);
if (cachedSnapshot) {
this.snapshot = plainToClass(
PortfolioSnapshot,
JSON.parse(cachedSnapshot)
);
Logger.debug(
`Fetched portfolio snapshot from cache in ${(
(performance.now() - startTimeTotal) /
1000
).toFixed(3)} seconds`,
'PortfolioCalculator'
);
} else {
this.snapshot = await this.computeSnapshot(
this.startDate,
this.endDate
);
this.redisCacheService.set(
this.redisCacheService.getPortfolioSnapshotKey(this.userId),
JSON.stringify(this.snapshot),
this.configurationService.get('CACHE_QUOTES_TTL')
);
Logger.debug(
`Computed portfolio snapshot in ${(
(performance.now() - startTimeTotal) /
1000
).toFixed(3)} seconds`,
'PortfolioCalculator'
);
}
} else {
this.snapshot = await this.computeSnapshot(this.startDate, this.endDate);
}
} }
} }

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PortfolioCalculatorFactory, PortfolioCalculatorFactory,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -101,7 +122,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'CHF' currency: 'CHF',
userId: userDummyData.id
}); });
const chartData = await portfolioCalculator.getChartData({ const chartData = await portfolioCalculator.getChartData({

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PerformanceCalculationType, PerformanceCalculationType,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -86,7 +107,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'CHF' currency: 'CHF',
userId: userDummyData.id
}); });
const chartData = await portfolioCalculator.getChartData({ const chartData = await portfolioCalculator.getChartData({

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PortfolioCalculatorFactory, PortfolioCalculatorFactory,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -71,7 +92,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'CHF' currency: 'CHF',
userId: userDummyData.id
}); });
const chartData = await portfolioCalculator.getChartData({ const chartData = await portfolioCalculator.getChartData({

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PortfolioCalculatorFactory, PortfolioCalculatorFactory,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
jest.mock( jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => { () => {
@ -37,11 +50,15 @@ jest.mock(
); );
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -51,9 +68,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -99,7 +120,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'CHF' currency: 'CHF',
userId: userDummyData.id
}); });
const chartData = await portfolioCalculator.getChartData({ const chartData = await portfolioCalculator.getChartData({

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PortfolioCalculatorFactory, PortfolioCalculatorFactory,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -71,7 +92,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'USD' currency: 'USD',
userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot( const portfolioSnapshot = await portfolioCalculator.computeSnapshot(

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PortfolioCalculatorFactory, PortfolioCalculatorFactory,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
jest.mock( jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => { () => {
@ -37,11 +50,15 @@ jest.mock(
); );
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -51,9 +68,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -84,7 +105,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'CHF' currency: 'CHF',
userId: userDummyData.id
}); });
const chartData = await portfolioCalculator.getChartData({ const chartData = await portfolioCalculator.getChartData({

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PortfolioCalculatorFactory, PortfolioCalculatorFactory,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -71,7 +92,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'USD' currency: 'USD',
userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot( const portfolioSnapshot = await portfolioCalculator.computeSnapshot(

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PortfolioCalculatorFactory, PortfolioCalculatorFactory,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -71,7 +92,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'USD' currency: 'USD',
userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot( const portfolioSnapshot = await portfolioCalculator.computeSnapshot(

28
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PerformanceCalculationType, PerformanceCalculationType,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
jest.mock( jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => { () => {
@ -37,11 +50,15 @@ jest.mock(
); );
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -51,9 +68,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -99,7 +120,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'USD' currency: 'USD',
userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot( const portfolioSnapshot = await portfolioCalculator.computeSnapshot(

26
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts

@ -1,9 +1,13 @@
import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PerformanceCalculationType, PerformanceCalculationType,
PortfolioCalculatorFactory PortfolioCalculatorFactory
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -19,12 +23,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -34,9 +51,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -49,7 +70,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities: [], activities: [],
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'CHF' currency: 'CHF',
userId: userDummyData.id
}); });
const start = subDays(new Date(Date.now()), 10); const start = subDays(new Date(Date.now()), 10);

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

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PerformanceCalculationType, PerformanceCalculationType,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -86,7 +107,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'CHF' currency: 'CHF',
userId: userDummyData.id
}); });
const chartData = await portfolioCalculator.getChartData({ const chartData = await portfolioCalculator.getChartData({

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

@ -1,7 +1,8 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { import {
PerformanceCalculationType, PerformanceCalculationType,
@ -9,6 +10,9 @@ import {
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
}; };
}); });
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });
@ -86,7 +107,8 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = factory.createCalculator({ const portfolioCalculator = factory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: 'CHF' currency: 'CHF',
userId: userDummyData.id
}); });
const chartData = await portfolioCalculator.getChartData({ const chartData = await portfolioCalculator.getChartData({

12
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts

@ -1,13 +1,19 @@
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let factory: PortfolioCalculatorFactory; let factory: PortfolioCalculatorFactory;
let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
@ -17,9 +23,13 @@ describe('PortfolioCalculator', () => {
null null
); );
redisCacheService = new RedisCacheService(null, null);
factory = new PortfolioCalculatorFactory( factory = new PortfolioCalculatorFactory(
configurationService,
currentRateService, currentRateService,
exchangeRateDataService exchangeRateDataService,
redisCacheService
); );
}); });

8
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -1,13 +1,9 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces';
SymbolMetrics, import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
TimelinePosition,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Big } from 'big.js'; import { Big } from 'big.js';

24
apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts

@ -1,24 +0,0 @@
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
import { Big } from 'big.js';
export interface PortfolioSnapshot extends ResponseError {
currentValueInBaseCurrency: Big;
grossPerformance: Big;
grossPerformanceWithCurrencyEffect: Big;
grossPerformancePercentage: Big;
grossPerformancePercentageWithCurrencyEffect: Big;
netAnnualizedPerformance?: Big;
netAnnualizedPerformanceWithCurrencyEffect?: Big;
netPerformance: Big;
netPerformanceWithCurrencyEffect: Big;
netPerformancePercentage: Big;
netPerformancePercentageWithCurrencyEffect: Big;
positions: TimelinePosition[];
totalFeesWithCurrencyEffect: Big;
totalInterestWithCurrencyEffect: Big;
totalInvestment: Big;
totalInvestmentWithCurrencyEffect: Big;
totalLiabilitiesWithCurrencyEffect: Big;
totalValuablesWithCurrencyEffect: Big;
}

2
apps/api/src/app/portfolio/portfolio.module.ts

@ -2,6 +2,7 @@ import { AccessModule } from '@ghostfolio/api/app/access/access.module';
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
@ -35,6 +36,7 @@ import { RulesService } from './rules.service';
MarketDataModule, MarketDataModule,
OrderModule, OrderModule,
PrismaModule, PrismaModule,
RedisCacheModule,
SymbolProfileModule, SymbolProfileModule,
UserModule UserModule
], ],

34
apps/api/src/app/portfolio/portfolio.service.ts

@ -29,6 +29,7 @@ import {
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter, Filter,
HistoricalDataItem, HistoricalDataItem,
InvestmentItem,
PortfolioDetails, PortfolioDetails,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
@ -36,10 +37,9 @@ import {
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
Position, Position,
TimelinePosition,
UserSettings UserSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { TimelinePosition } from '@ghostfolio/common/models';
import type { import type {
AccountWithValue, AccountWithValue,
DateRange, DateRange,
@ -277,8 +277,11 @@ export class PortfolioService {
const portfolioCalculator = this.calculatorFactory.createCalculator({ const portfolioCalculator = this.calculatorFactory.createCalculator({
activities, activities,
userId,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: this.request.user.Settings.settings.baseCurrency currency: this.request.user.Settings.settings.baseCurrency,
isExperimentalFeatures:
this.request.user.Settings.settings.isExperimentalFeatures
}); });
const items = await portfolioCalculator.getChart({ const items = await portfolioCalculator.getChart({
@ -352,8 +355,11 @@ export class PortfolioService {
const portfolioCalculator = this.calculatorFactory.createCalculator({ const portfolioCalculator = this.calculatorFactory.createCalculator({
activities, activities,
dateRange, dateRange,
userId,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: userCurrency currency: userCurrency,
isExperimentalFeatures:
this.request.user.Settings.settings.isExperimentalFeatures
}); });
const { currentValueInBaseCurrency, hasErrors, positions } = const { currentValueInBaseCurrency, hasErrors, positions } =
@ -648,11 +654,14 @@ export class PortfolioService {
]); ]);
const portfolioCalculator = this.calculatorFactory.createCalculator({ const portfolioCalculator = this.calculatorFactory.createCalculator({
userId,
activities: orders.filter((order) => { activities: orders.filter((order) => {
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
}), }),
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: userCurrency currency: userCurrency,
isExperimentalFeatures:
this.request.user.Settings.settings.isExperimentalFeatures
}); });
const portfolioStart = portfolioCalculator.getStartDate(); const portfolioStart = portfolioCalculator.getStartDate();
@ -919,8 +928,11 @@ export class PortfolioService {
const portfolioCalculator = this.calculatorFactory.createCalculator({ const portfolioCalculator = this.calculatorFactory.createCalculator({
activities, activities,
dateRange, dateRange,
userId,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: this.request.user.Settings.settings.baseCurrency currency: this.request.user.Settings.settings.baseCurrency,
isExperimentalFeatures:
this.request.user.Settings.settings.isExperimentalFeatures
}); });
let { hasErrors, positions } = await portfolioCalculator.getSnapshot(); let { hasErrors, positions } = await portfolioCalculator.getSnapshot();
@ -1108,8 +1120,11 @@ export class PortfolioService {
accountBalanceItems, accountBalanceItems,
activities, activities,
dateRange, dateRange,
userId,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: userCurrency currency: userCurrency,
isExperimentalFeatures:
this.request.user.Settings.settings.isExperimentalFeatures
}); });
const { const {
@ -1202,8 +1217,11 @@ export class PortfolioService {
const portfolioCalculator = this.calculatorFactory.createCalculator({ const portfolioCalculator = this.calculatorFactory.createCalculator({
activities, activities,
userId,
calculationType: PerformanceCalculationType.TWR, calculationType: PerformanceCalculationType.TWR,
currency: this.request.user.Settings.settings.baseCurrency currency: this.request.user.Settings.settings.baseCurrency,
isExperimentalFeatures:
this.request.user.Settings.settings.isExperimentalFeatures
}); });
let { totalFeesWithCurrencyEffect, positions, totalInvestment } = let { totalFeesWithCurrencyEffect, positions, totalInvestment } =

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

@ -0,0 +1,13 @@
import { RedisCacheService } from './redis-cache.service';
export const RedisCacheServiceMock = {
get: (key: string): Promise<string> => {
return Promise.resolve(null);
},
getPortfolioSnapshotKey: (userId: string): string => {
return `portfolio-snapshot-${userId}`;
},
set: (key: string, value: string, ttlInSeconds?: number): Promise<string> => {
return Promise.resolve(value);
}
};

4
apps/api/src/app/redis-cache/redis-cache.service.ts

@ -24,6 +24,10 @@ export class RedisCacheService {
return this.cache.get(key); return this.cache.get(key);
} }
public getPortfolioSnapshotKey(userId: string) {
return `portfolio-snapshot-${userId}`;
}
public getQuoteKey({ dataSource, symbol }: UniqueAsset) { public getQuoteKey({ dataSource, symbol }: UniqueAsset) {
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`; return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
} }

3
apps/api/src/events/events.module.ts

@ -1,8 +1,11 @@
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PortfolioChangedListener } from './portfolio-changed.listener'; import { PortfolioChangedListener } from './portfolio-changed.listener';
@Module({ @Module({
imports: [RedisCacheModule],
providers: [PortfolioChangedListener] providers: [PortfolioChangedListener]
}) })
export class EventsModule {} export class EventsModule {}

8
apps/api/src/events/portfolio-changed.listener.ts

@ -1,3 +1,5 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
@ -5,11 +7,17 @@ import { PortfolioChangedEvent } from './portfolio-changed.event';
@Injectable() @Injectable()
export class PortfolioChangedListener { export class PortfolioChangedListener {
public constructor(private readonly redisCacheService: RedisCacheService) {}
@OnEvent(PortfolioChangedEvent.getName()) @OnEvent(PortfolioChangedEvent.getName())
handlePortfolioChangedEvent(event: PortfolioChangedEvent) { handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
Logger.log( Logger.log(
`Portfolio of user with id ${event.getUserId()} has changed`, `Portfolio of user with id ${event.getUserId()} has changed`,
'PortfolioChangedListener' 'PortfolioChangedListener'
); );
this.redisCacheService.remove(
this.redisCacheService.getPortfolioSnapshotKey(event.getUserId())
);
} }
} }

3
apps/api/src/models/rule.ts

@ -1,7 +1,8 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { groupBy } from '@ghostfolio/common/helper'; import { groupBy } from '@ghostfolio/common/helper';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { EvaluationResult } from './interfaces/evaluation-result.interface';
import { RuleInterface } from './interfaces/rule.interface'; import { RuleInterface } from './interfaces/rule.interface';

3
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -1,7 +1,8 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule'; import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> { export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[]; private positions: TimelinePosition[];

3
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -1,7 +1,8 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule'; import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> { export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[]; private positions: TimelinePosition[];

13
apps/api/src/services/data-provider/data-provider.service.ts

@ -399,7 +399,8 @@ export class DataProviderService {
numberOfItemsInCache > 1 ? 's' : '' numberOfItemsInCache > 1 ? 's' : ''
} from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed( } from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed(
3 3
)} seconds` )} seconds`,
'DataProviderService'
); );
} }
@ -505,7 +506,8 @@ export class DataProviderService {
} from ${dataSource} in ${( } from ${dataSource} in ${(
(performance.now() - startTimeDataSource) / (performance.now() - startTimeDataSource) /
1000 1000
).toFixed(3)} seconds` ).toFixed(3)} seconds`,
'DataProviderService'
); );
try { try {
@ -535,14 +537,15 @@ export class DataProviderService {
await Promise.all(promises); await Promise.all(promises);
Logger.debug('------------------------------------------------'); Logger.debug('--------------------------------------------------------');
Logger.debug( Logger.debug(
`Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${( `Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${(
(performance.now() - startTimeTotal) / (performance.now() - startTimeTotal) /
1000 1000
).toFixed(3)} seconds` ).toFixed(3)} seconds`,
'DataProviderService'
); );
Logger.debug('================================================'); Logger.debug('========================================================');
return response; return response;
} }

9
libs/common/src/lib/class-transformer.ts

@ -0,0 +1,9 @@
import { Big } from 'big.js';
export function transformToBig({ value }: { value: string }): Big {
if (value === null) {
return null;
}
return new Big(value);
}

2
libs/common/src/lib/interfaces/index.ts

@ -48,7 +48,6 @@ import type { Subscription } from './subscription.interface';
import type { SymbolMetrics } from './symbol-metrics.interface'; import type { SymbolMetrics } from './symbol-metrics.interface';
import type { SystemMessage } from './system-message.interface'; import type { SystemMessage } from './system-message.interface';
import type { TabConfiguration } from './tab-configuration.interface'; import type { TabConfiguration } from './tab-configuration.interface';
import type { TimelinePosition } from './timeline-position.interface';
import type { UniqueAsset } from './unique-asset.interface'; import type { UniqueAsset } from './unique-asset.interface';
import type { UserSettings } from './user-settings.interface'; import type { UserSettings } from './user-settings.interface';
import type { User } from './user.interface'; import type { User } from './user.interface';
@ -102,7 +101,6 @@ export {
Subscription, Subscription,
SymbolMetrics, SymbolMetrics,
TabConfiguration, TabConfiguration,
TimelinePosition,
UniqueAsset, UniqueAsset,
User, User,
UserSettings UserSettings

31
libs/common/src/lib/interfaces/timeline-position.interface.ts

@ -1,31 +0,0 @@
import { DataSource, Tag } from '@prisma/client';
import { Big } from 'big.js';
export interface TimelinePosition {
averagePrice: Big;
currency: string;
dataSource: DataSource;
dividend: Big;
dividendInBaseCurrency: Big;
fee: Big;
firstBuyDate: string;
grossPerformance: Big;
grossPerformancePercentage: Big;
grossPerformancePercentageWithCurrencyEffect: Big;
grossPerformanceWithCurrencyEffect: Big;
investment: Big;
investmentWithCurrencyEffect: Big;
marketPrice: number;
marketPriceInBaseCurrency: number;
netPerformance: Big;
netPerformancePercentage: Big;
netPerformancePercentageWithCurrencyEffect: Big;
netPerformanceWithCurrencyEffect: Big;
quantity: Big;
symbol: string;
tags?: Tag[];
timeWeightedInvestment: Big;
timeWeightedInvestmentWithCurrencyEffect: Big;
transactionCount: number;
valueInBaseCurrency: Big;
}

4
libs/common/src/lib/models/index.ts

@ -0,0 +1,4 @@
import { PortfolioSnapshot } from './portfolio-snapshot';
import { TimelinePosition } from './timeline-position';
export { PortfolioSnapshot, TimelinePosition };

82
libs/common/src/lib/models/portfolio-snapshot.ts

@ -0,0 +1,82 @@
import { transformToBig } from '@ghostfolio/common/class-transformer';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
import { Big } from 'big.js';
import { Transform, Type } from 'class-transformer';
export class PortfolioSnapshot {
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
currentValueInBaseCurrency: Big;
errors?: UniqueAsset[];
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
grossPerformance: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
grossPerformanceWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
grossPerformancePercentage: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
grossPerformancePercentageWithCurrencyEffect: Big;
hasErrors: boolean;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netAnnualizedPerformance?: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netAnnualizedPerformanceWithCurrencyEffect?: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netPerformance: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netPerformanceWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netPerformancePercentage: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netPerformancePercentageWithCurrencyEffect: Big;
@Type(() => TimelinePosition)
positions: TimelinePosition[];
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
totalFeesWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
totalInterestWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
totalInvestment: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
totalInvestmentWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
totalLiabilitiesWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
totalValuablesWithCurrencyEffect: Big;
}

92
libs/common/src/lib/models/timeline-position.ts

@ -0,0 +1,92 @@
import { transformToBig } from '@ghostfolio/common/class-transformer';
import { DataSource, Tag } from '@prisma/client';
import { Big } from 'big.js';
import { Transform, Type } from 'class-transformer';
export class TimelinePosition {
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
averagePrice: Big;
currency: string;
dataSource: DataSource;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
dividend: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
dividendInBaseCurrency: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
fee: Big;
firstBuyDate: string;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
grossPerformance: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
grossPerformancePercentage: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
grossPerformancePercentageWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
grossPerformanceWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
investment: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
investmentWithCurrencyEffect: Big;
marketPrice: number;
marketPriceInBaseCurrency: number;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netPerformance: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netPerformancePercentage: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netPerformancePercentageWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
netPerformanceWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
quantity: Big;
symbol: string;
tags?: Tag[];
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
timeWeightedInvestment: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
timeWeightedInvestmentWithCurrencyEffect: Big;
transactionCount: number;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
valueInBaseCurrency: Big;
}
Loading…
Cancel
Save