Browse Source

Task/deprecate SymbolProfile in favor of assetProfile in activity interface (#7315)

* Deprecate SymbolProfile in favor of assetProfile

* Update changelog
pull/7321/head^2
Thomas Kaul 23 hours ago
committed by GitHub
parent
commit
d5fb6311c1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 18
      apps/api/src/app/activities/activities.service.ts
  3. 7
      apps/api/src/app/endpoints/public/public.controller.ts
  4. 12
      apps/api/src/app/export/export.service.ts
  5. 38
      apps/api/src/app/import/import.service.ts
  6. 2
      apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts
  7. 4
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  8. 26
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  9. 462
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  10. 26
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
  11. 38
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
  12. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts
  13. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  14. 26
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  15. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  16. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  17. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
  18. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
  19. 380
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts
  20. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
  21. 38
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts
  22. 26
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  23. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  24. 528
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  25. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts
  26. 2
      apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts
  27. 6
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  28. 18
      apps/api/src/app/portfolio/portfolio.service.ts
  29. 7
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  30. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  31. 30
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  32. 4
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts
  33. 8
      apps/client/src/app/services/import-activities.service.ts
  34. 12
      libs/common/src/lib/config.ts
  35. 8
      libs/common/src/lib/interfaces/activities.interface.ts
  36. 6
      libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts
  37. 16
      libs/ui/src/lib/activities-table/activities-table.component.html
  38. 10
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  39. 4
      libs/ui/src/lib/activities-table/activities-table.component.ts

1
CHANGELOG.md

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Hardened the validation of the sectors in the asset profile endpoints - Hardened the validation of the sectors in the asset profile endpoints
- Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service - Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service
- Set the change detection strategy to `OnPush` in the _X-ray_ page - Set the change detection strategy to `OnPush` in the _X-ray_ page
- Deprecated `SymbolProfile` in favor of `assetProfile` in the activity interface
## 3.25.0 - 2026-07-12 ## 3.25.0 - 2026-07-12

18
apps/api/src/app/activities/activities.service.ts

@ -455,6 +455,23 @@ export class ActivitiesService {
userId, userId,
accountId: account.id, accountId: account.id,
accountUserId: account.userId, accountUserId: account.userId,
assetProfile: {
activitiesCount: 0,
assetClass: AssetClass.LIQUIDITY,
assetSubClass: AssetSubClass.CASH,
countries: [],
createdAt: new Date(balanceItem.date),
currency: account.currency,
dataSource:
this.dataProviderService.getDataSourceForExchangeRates(),
holdings: [],
id: account.currency,
isActive: true,
name: account.currency,
sectors: [],
symbol: account.currency,
updatedAt: new Date(balanceItem.date)
},
comment: account.name, comment: account.name,
createdAt: new Date(balanceItem.date), createdAt: new Date(balanceItem.date),
currency: account.currency, currency: account.currency,
@ -823,6 +840,7 @@ export class ActivitiesService {
return { return {
...order, ...order,
assetProfile,
feeInAssetProfileCurrency, feeInAssetProfileCurrency,
feeInBaseCurrency, feeInBaseCurrency,
unitPriceInAssetProfileCurrency, unitPriceInAssetProfileCurrency,

7
apps/api/src/app/endpoints/public/public.controller.ts

@ -110,26 +110,27 @@ export class PublicController {
? [] ? []
: activities.map( : activities.map(
({ ({
assetProfile,
currency, currency,
date, date,
fee, fee,
quantity, quantity,
SymbolProfile,
type, type,
unitPrice, unitPrice,
value, value,
valueInBaseCurrency valueInBaseCurrency
}) => { }) => {
return { return {
assetProfile,
currency, currency,
date, date,
fee, fee,
quantity, quantity,
SymbolProfile,
type, type,
unitPrice, unitPrice,
value, value,
valueInBaseCurrency valueInBaseCurrency,
SymbolProfile: assetProfile
}; };
} }
); );

12
apps/api/src/app/export/export.service.ts

@ -124,8 +124,8 @@ export class ExportService {
const customAssetProfiles = uniqBy( const customAssetProfiles = uniqBy(
activities activities
.map(({ SymbolProfile }) => { .map(({ assetProfile }) => {
return SymbolProfile; return assetProfile;
}) })
.filter(({ userId: assetProfileUserId }) => { .filter(({ userId: assetProfileUserId }) => {
return assetProfileUserId === userId; return assetProfileUserId === userId;
@ -224,13 +224,13 @@ export class ExportService {
activities: activities.map( activities: activities.map(
({ ({
accountId, accountId,
assetProfile,
comment, comment,
currency, currency,
date, date,
fee, fee,
id, id,
quantity, quantity,
SymbolProfile,
tags: currentTags, tags: currentTags,
type, type,
unitPrice unitPrice
@ -243,10 +243,10 @@ export class ExportService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
currency: currency ?? SymbolProfile.currency, currency: currency ?? assetProfile.currency,
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
date: date.toISOString(), date: date.toISOString(),
symbol: SymbolProfile.symbol, symbol: assetProfile.symbol,
tags: currentTags.map(({ id: tagId }) => { tags: currentTags.map(({ id: tagId }) => {
return tagId; return tagId;
}) })

38
apps/api/src/app/import/import.service.ts

@ -122,11 +122,11 @@ export class ImportService {
const isDuplicate = activities.some((activity) => { const isDuplicate = activities.some((activity) => {
return ( return (
activity.accountId === account?.id && activity.accountId === account?.id &&
activity.SymbolProfile.currency === assetProfile.currency && activity.assetProfile.currency === assetProfile.currency &&
activity.SymbolProfile.dataSource === assetProfile.dataSource && activity.assetProfile.dataSource === assetProfile.dataSource &&
isSameSecond(activity.date, date) && isSameSecond(activity.date, date) &&
activity.quantity === quantity && activity.quantity === quantity &&
activity.SymbolProfile.symbol === assetProfile.symbol && activity.assetProfile.symbol === assetProfile.symbol &&
activity.type === 'DIVIDEND' && activity.type === 'DIVIDEND' &&
activity.unitPrice === marketPrice activity.unitPrice === marketPrice
); );
@ -138,6 +138,7 @@ export class ImportService {
return { return {
account, account,
assetProfile,
date, date,
error, error,
quantity, quantity,
@ -463,19 +464,18 @@ export class ImportService {
const error = activity.error; const error = activity.error;
const fee = activity.fee; const fee = activity.fee;
const quantity = activity.quantity; const quantity = activity.quantity;
const SymbolProfile = activity.SymbolProfile;
const tagIds = activity.tagIds ?? []; const tagIds = activity.tagIds ?? [];
const type = activity.type; const type = activity.type;
const unitPrice = activity.unitPrice; const unitPrice = activity.unitPrice;
const assetProfile = assetProfiles[ const assetProfile = assetProfiles[
getAssetProfileIdentifier({ getAssetProfileIdentifier({
dataSource: SymbolProfile.dataSource, dataSource: activity.assetProfile.dataSource,
symbol: SymbolProfile.symbol symbol: activity.assetProfile.symbol
}) })
] ?? { ] ?? {
dataSource: SymbolProfile.dataSource, dataSource: activity.assetProfile.dataSource,
symbol: SymbolProfile.symbol symbol: activity.assetProfile.symbol
}; };
const { const {
assetClass, assetClass,
@ -619,6 +619,8 @@ export class ImportService {
activities.push({ activities.push({
...order, ...order,
// @ts-ignore
assetProfile,
error, error,
value, value,
valueInBaseCurrency, valueInBaseCurrency,
@ -633,19 +635,19 @@ export class ImportService {
if (!isDryRun) { if (!isDryRun) {
// Gather symbol data in the background, if not dry run // Gather symbol data in the background, if not dry run
const uniqueActivities = uniqBy(activities, ({ SymbolProfile }) => { const uniqueActivities = uniqBy(activities, ({ assetProfile }) => {
return getAssetProfileIdentifier({ return getAssetProfileIdentifier({
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
symbol: SymbolProfile.symbol symbol: assetProfile.symbol
}); });
}); });
this.dataGatheringService.gatherSymbols({ this.dataGatheringService.gatherSymbols({
dataGatheringItems: uniqueActivities.map(({ date, SymbolProfile }) => { dataGatheringItems: uniqueActivities.map(({ assetProfile, date }) => {
return { return {
date, date,
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
symbol: SymbolProfile.symbol symbol: assetProfile.symbol
}; };
}), }),
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
@ -692,12 +694,12 @@ export class ImportService {
activity.accountId === accountId && activity.accountId === accountId &&
activity.comment === comment && activity.comment === comment &&
(activity.currency === currency || (activity.currency === currency ||
activity.SymbolProfile.currency === currency) && activity.assetProfile.currency === currency) &&
activity.SymbolProfile.dataSource === dataSource && activity.assetProfile.dataSource === dataSource &&
isSameSecond(activity.date, date) && isSameSecond(activity.date, date) &&
activity.fee === fee && activity.fee === fee &&
activity.quantity === quantity && activity.quantity === quantity &&
activity.SymbolProfile.symbol === symbol && activity.assetProfile.symbol === symbol &&
activity.type === type && activity.type === type &&
activity.unitPrice === unitPrice activity.unitPrice === unitPrice
); );
@ -717,7 +719,7 @@ export class ImportService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
SymbolProfile: { assetProfile: {
dataSource, dataSource,
symbol, symbol,
activitiesCount: undefined, activitiesCount: undefined,

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

@ -22,7 +22,7 @@ export const activityDummyData = {
valueInBaseCurrency: undefined valueInBaseCurrency: undefined
}; };
export const symbolProfileDummyData = { export const assetProfileDummyData = {
activitiesCount: undefined, activitiesCount: undefined,
assetClass: undefined, assetClass: undefined,
assetSubClass: undefined, assetSubClass: undefined,

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

@ -121,11 +121,11 @@ export abstract class PortfolioCalculator {
this.activities = activities this.activities = activities
.map( .map(
({ ({
assetProfile,
date, date,
feeInAssetProfileCurrency, feeInAssetProfileCurrency,
feeInBaseCurrency, feeInBaseCurrency,
quantity, quantity,
SymbolProfile,
tags = [], tags = [],
type, type,
unitPriceInAssetProfileCurrency unitPriceInAssetProfileCurrency
@ -141,9 +141,9 @@ export abstract class PortfolioCalculator {
} }
return { return {
assetProfile,
tags, tags,
type, type,
assetProfile: SymbolProfile,
date: format(date, DATE_FORMAT), date: format(date, DATE_FORMAT),
fee: new Big(feeInAssetProfileCurrency), fee: new Big(feeInAssetProfileCurrency),
feeInBaseCurrency: new Big(feeInBaseCurrency), feeInBaseCurrency: new Big(feeInBaseCurrency),

26
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 142.9 unitPriceInAssetProfileCurrency: 142.9
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.65, ...assetProfileDummyData,
feeInBaseCurrency: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
} }

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

@ -1,231 +1,231 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
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 { 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 { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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 { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Activity } from '@ghostfolio/common/interfaces'; import { Activity } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
}; };
}); });
jest.mock( jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
}; };
} }
); );
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
}; };
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory; let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService; let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService; let redisCacheService: RedisCacheService;
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService(); configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
null, null,
null, null,
null null
); );
portfolioSnapshotService = new PortfolioSnapshotService(null, null); portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null); redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory( portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService, configurationService,
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
portfolioSnapshotService, portfolioSnapshotService,
redisCacheService redisCacheService
); );
}); });
describe('get current positions', () => { describe('get current positions', () => {
it.only('with BALN.SW buy and sell in two activities', async () => { it.only('with BALN.SW buy and sell in two activities', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime());
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55, currency: 'CHF',
quantity: 2, dataSource: 'YAHOO',
SymbolProfile: { name: 'Bâloise Holding AG',
...symbolProfileDummyData, symbol: 'BALN.SW'
currency: 'CHF', },
dataSource: 'YAHOO', date: new Date('2021-11-22'),
name: 'Bâloise Holding AG', feeInAssetProfileCurrency: 1.55,
symbol: 'BALN.SW' feeInBaseCurrency: 1.55,
}, quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 142.9 unitPriceInAssetProfileCurrency: 142.9
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.65, ...assetProfileDummyData,
feeInBaseCurrency: 1.65, currency: 'CHF',
quantity: 1, dataSource: 'YAHOO',
SymbolProfile: { name: 'Bâloise Holding AG',
...symbolProfileDummyData, symbol: 'BALN.SW'
currency: 'CHF', },
dataSource: 'YAHOO', date: new Date('2021-11-30'),
name: 'Bâloise Holding AG', feeInAssetProfileCurrency: 1.65,
symbol: 'BALN.SW' feeInBaseCurrency: 1.65,
}, quantity: 1,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0, currency: 'CHF',
quantity: 1, dataSource: 'YAHOO',
SymbolProfile: { name: 'Bâloise Holding AG',
...symbolProfileDummyData, symbol: 'BALN.SW'
currency: 'CHF', },
dataSource: 'YAHOO', date: new Date('2021-11-30'),
name: 'Bâloise Holding AG', feeInAssetProfileCurrency: 0,
symbol: 'BALN.SW' feeInBaseCurrency: 0,
}, quantity: 1,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
} }
]; ];
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.ROAI, calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF', currency: 'CHF',
userId: userDummyData.id userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'year' groupBy: 'year'
}); });
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 3, activitiesCount: 3,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-22', dateOfFirstActivity: '2021-11-22',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'), fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'), feeInBaseCurrency: new Big('3.2'),
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.04408677396780965649'), grossPerformancePercentage: new Big('-0.04408677396780965649'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
'-0.04408677396780965649' '-0.04408677396780965649'
), ),
grossPerformanceWithCurrencyEffect: new Big('-12.6'), grossPerformanceWithCurrencyEffect: new Big('-12.6'),
investment: new Big('0'), investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'), investmentWithCurrencyEffect: new Big('0'),
netPerformancePercentageWithCurrencyEffectMap: { netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('-0.0552834149755073478') max: new Big('-0.0552834149755073478')
}, },
netPerformanceWithCurrencyEffectMap: { netPerformanceWithCurrencyEffectMap: {
max: new Big('-15.8') max: new Big('-15.8')
}, },
marketPrice: 148.9, marketPrice: 148.9,
marketPriceInBaseCurrency: 148.9, marketPriceInBaseCurrency: 148.9,
quantity: new Big('0'), quantity: new Big('0'),
symbol: 'BALN.SW', symbol: 'BALN.SW',
tags: [], tags: [],
timeWeightedInvestment: new Big('285.80000000000000396627'), timeWeightedInvestment: new Big('285.80000000000000396627'),
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'285.80000000000000396627' '285.80000000000000396627'
), ),
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
totalFeesWithCurrencyEffect: new Big('3.2'), totalFeesWithCurrencyEffect: new Big('3.2'),
totalInterestWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'), totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0') totalLiabilitiesWithCurrencyEffect: new Big('0')
}); });
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({ expect.objectContaining({
netPerformance: -15.8, netPerformance: -15.8,
netPerformanceInPercentage: -0.05528341497550734703, netPerformanceInPercentage: -0.05528341497550734703,
netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550734703, netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550734703,
netPerformanceWithCurrencyEffect: -15.8, netPerformanceWithCurrencyEffect: -15.8,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0 totalInvestmentValueWithCurrencyEffect: 0
}) })
); );
expect(investments).toEqual([ expect(investments).toEqual([
{ date: '2021-11-22', investment: new Big('285.8') }, { date: '2021-11-22', investment: new Big('285.8') },
{ date: '2021-11-30', investment: new Big('0') } { date: '2021-11-30', investment: new Big('0') }
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: 0 }, { date: '2021-11-01', investment: 0 },
{ date: '2021-12-01', investment: 0 } { date: '2021-12-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([ expect(investmentsByYear).toEqual([
{ date: '2021-01-01', investment: 0 } { date: '2021-01-01', investment: 0 }
]); ]);
}); });
}); });
}); });

26
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 142.9 unitPriceInAssetProfileCurrency: 142.9
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.65, ...assetProfileDummyData,
feeInBaseCurrency: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
} }

38
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 136.6 unitPriceInAssetProfileCurrency: 136.6
} }
@ -217,17 +217,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 135.0 unitPriceInAssetProfileCurrency: 135.0
} }
@ -257,17 +257,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), assetProfile: {
feeInAssetProfileCurrency: 1.55, ...assetProfileDummyData,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bâloise Holding AG', name: 'Bâloise Holding AG',
symbol: 'BALN.SW' symbol: 'BALN.SW'
}, },
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 135.0 unitPriceInAssetProfileCurrency: 135.0
} }

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -107,17 +107,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = exportResponse.activities.map( const activities: Activity[] = exportResponse.activities.map(
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, assetProfile: {
date: parseDate(activity.date), ...assetProfileDummyData,
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 3.94,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Bitcoin', name: 'Bitcoin',
symbol: activity.symbol symbol: activity.symbol
}, },
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 3.94,
unitPriceInAssetProfileCurrency: 44558.42 unitPriceInAssetProfileCurrency: 44558.42
}) })
); );

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -95,17 +95,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = exportResponse.activities.map( const activities: Activity[] = exportResponse.activities.map(
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, assetProfile: {
date: parseDate(activity.date), ...assetProfileDummyData,
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Bitcoin', name: 'Bitcoin',
symbol: activity.symbol symbol: activity.symbol
}, },
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
unitPriceInAssetProfileCurrency: 44558.42 unitPriceInAssetProfileCurrency: 44558.42
}) })
); );

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

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -98,33 +98,33 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2015-01-01'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bitcoin USD', name: 'Bitcoin USD',
symbol: 'BTCUSD' symbol: 'BTCUSD'
}, },
date: new Date('2015-01-01'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 2,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 320.43 unitPriceInAssetProfileCurrency: 320.43
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2017-12-31'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Bitcoin USD', name: 'Bitcoin USD',
symbol: 'BTCUSD' symbol: 'BTCUSD'
}, },
date: new Date('2017-12-31'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 14156.4 unitPriceInAssetProfileCurrency: 14156.4
} }

12
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -96,16 +96,16 @@ describe('PortfolioCalculator', () => {
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: activity.fee, ...assetProfileDummyData,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Bitcoin', name: 'Bitcoin',
symbol: activity.symbol symbol: activity.symbol
}, },
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
unitPriceInAssetProfileCurrency: activity.unitPrice unitPriceInAssetProfileCurrency: activity.unitPrice
}) })
); );

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

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -96,16 +96,16 @@ describe('PortfolioCalculator', () => {
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: 4.46, ...assetProfileDummyData,
feeInBaseCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Bitcoin', name: 'Bitcoin',
symbol: activity.symbol symbol: activity.symbol
}, },
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
unitPriceInAssetProfileCurrency: 44558.42 unitPriceInAssetProfileCurrency: 44558.42
}) })
); );

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-09-01'), assetProfile: {
feeInAssetProfileCurrency: 49, ...assetProfileDummyData,
feeInBaseCurrency: 49,
quantity: 0,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'MANUAL', dataSource: 'MANUAL',
name: 'Account Opening Fee', name: 'Account Opening Fee',
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141' symbol: '2c463fb3-af07-486e-adb0-8301b3d72141'
}, },
date: new Date('2021-09-01'),
feeInAssetProfileCurrency: 49,
feeInBaseCurrency: 49,
quantity: 0,
type: 'FEE', type: 'FEE',
unitPriceInAssetProfileCurrency: 0 unitPriceInAssetProfileCurrency: 0
} }

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -97,17 +97,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2023-01-03'), assetProfile: {
feeInAssetProfileCurrency: 1, ...assetProfileDummyData,
feeInBaseCurrency: 0.9238,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Alphabet Inc.', name: 'Alphabet Inc.',
symbol: 'GOOGL' symbol: 'GOOGL'
}, },
date: new Date('2023-01-03'),
feeInAssetProfileCurrency: 1,
feeInBaseCurrency: 0.9238,
quantity: 1,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 89.12 unitPriceInAssetProfileCurrency: 89.12
} }

380
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts

@ -1,190 +1,190 @@
import { import {
activityDummyData, activityDummyData,
loadExportFile, assetProfileDummyData,
symbolProfileDummyData, loadExportFile,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
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 { 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 { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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 { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { Activity, ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { join } from 'node:path'; import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
}; };
}); });
jest.mock( jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
}; };
} }
); );
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
}; };
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: ExportResponse; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory; let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService; let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService; let redisCacheService: RedisCacheService;
beforeAll(() => { beforeAll(() => {
exportResponse = loadExportFile( exportResponse = loadExportFile(
join( join(
__dirname, __dirname,
'../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json' '../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json'
) )
); );
}); });
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService(); configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
null, null,
null, null,
null null
); );
portfolioSnapshotService = new PortfolioSnapshotService(null, null); portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null); redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory( portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService, configurationService,
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
portfolioSnapshotService, portfolioSnapshotService,
redisCacheService redisCacheService
); );
}); });
describe('get current positions', () => { describe('get current positions', () => {
it.only('with JNUG buy and sell', async () => { it.only('with JNUG buy and sell', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime()); jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime());
const activities: Activity[] = exportResponse.activities.map( const activities: Activity[] = exportResponse.activities.map(
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: activity.fee, ...assetProfileDummyData,
feeInBaseCurrency: activity.fee, currency: activity.currency,
SymbolProfile: { dataSource: activity.dataSource,
...symbolProfileDummyData, name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares',
currency: activity.currency, symbol: activity.symbol
dataSource: activity.dataSource, },
name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares', date: parseDate(activity.date),
symbol: activity.symbol feeInAssetProfileCurrency: activity.fee,
}, feeInBaseCurrency: activity.fee,
unitPriceInAssetProfileCurrency: activity.unitPrice unitPriceInAssetProfileCurrency: activity.unitPrice
}) })
); );
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.ROAI, calculationType: PerformanceCalculationType.ROAI,
currency: exportResponse.user.settings.currency, currency: exportResponse.user.settings.currency,
userId: userDummyData.id userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'year' groupBy: 'year'
}); });
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 4, activitiesCount: 4,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2025-12-11', dateOfFirstActivity: '2025-12-11',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('4'), fee: new Big('4'),
feeInBaseCurrency: new Big('4'), feeInBaseCurrency: new Big('4'),
grossPerformance: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) grossPerformance: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10)
grossPerformanceWithCurrencyEffect: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) grossPerformanceWithCurrencyEffect: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10)
investment: new Big('0'), investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'), investmentWithCurrencyEffect: new Big('0'),
netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4
netPerformanceWithCurrencyEffectMap: { netPerformanceWithCurrencyEffectMap: {
max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4
}, },
marketPrice: 237.8000030517578, marketPrice: 237.8000030517578,
marketPriceInBaseCurrency: 237.8000030517578, marketPriceInBaseCurrency: 237.8000030517578,
quantity: new Big('0'), quantity: new Big('0'),
symbol: 'JNUG', symbol: 'JNUG',
tags: [], tags: [],
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
totalFeesWithCurrencyEffect: new Big('4'), totalFeesWithCurrencyEffect: new Big('4'),
totalInterestWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'), totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0') totalLiabilitiesWithCurrencyEffect: new Big('0')
}); });
expect(investments).toEqual([ expect(investments).toEqual([
{ date: '2025-12-11', investment: new Big('1885.05') }, { date: '2025-12-11', investment: new Big('1885.05') },
{ date: '2025-12-18', investment: new Big('2041.1') }, { date: '2025-12-18', investment: new Big('2041.1') },
{ date: '2025-12-28', investment: new Big('0') } { date: '2025-12-28', investment: new Big('0') }
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2025-12-01', investment: 0 } { date: '2025-12-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([ expect(investmentsByYear).toEqual([
{ date: '2025-01-01', investment: 0 } { date: '2025-01-01', investment: 0 }
]); ]);
}); });
}); });
}); });

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2023-01-01'), // Date in future assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'MANUAL', dataSource: 'MANUAL',
name: 'Loan', name: 'Loan',
symbol: '55196015-1365-4560-aa60-8751ae6d18f8' symbol: '55196015-1365-4560-aa60-8751ae6d18f8'
}, },
date: new Date('2023-01-01'), // Date in future
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'LIABILITY', type: 'LIABILITY',
unitPriceInAssetProfileCurrency: 3000 unitPriceInAssetProfileCurrency: 3000
} }

38
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -78,49 +78,49 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-08'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 0.3333333333333333,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2024-03-08'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 0.3333333333333333,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 408 unitPriceInAssetProfileCurrency: 408
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-13'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 0.6666666666666666,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2024-03-13'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 0.6666666666666666,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 400 unitPriceInAssetProfileCurrency: 400
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-14'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2024-03-14'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'SELL', type: 'SELL',
unitPriceInAssetProfileCurrency: 411 unitPriceInAssetProfileCurrency: 411
} }

26
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-09-16'), assetProfile: {
feeInAssetProfileCurrency: 19, ...assetProfileDummyData,
feeInBaseCurrency: 19,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2021-09-16'),
feeInAssetProfileCurrency: 19,
feeInBaseCurrency: 19,
quantity: 1,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 298.58 unitPriceInAssetProfileCurrency: 298.58
}, },
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-16'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
name: 'Microsoft Inc.', name: 'Microsoft Inc.',
symbol: 'MSFT' symbol: 'MSFT'
}, },
date: new Date('2021-11-16'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'DIVIDEND', type: 'DIVIDEND',
unitPriceInAssetProfileCurrency: 0.62 unitPriceInAssetProfileCurrency: 0.62
} }

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

@ -1,7 +1,7 @@
import { import {
activityDummyData, activityDummyData,
assetProfileDummyData,
loadExportFile, loadExportFile,
symbolProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -99,16 +99,16 @@ describe('PortfolioCalculator', () => {
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: activity.fee, ...assetProfileDummyData,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency, currency: activity.currency,
dataSource: activity.dataSource, dataSource: activity.dataSource,
name: 'Novartis AG', name: 'Novartis AG',
symbol: activity.symbol symbol: activity.symbol
}, },
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
unitPriceInAssetProfileCurrency: activity.unitPrice unitPriceInAssetProfileCurrency: activity.unitPrice
}) })
); );

528
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -1,264 +1,264 @@
import { import {
activityDummyData, activityDummyData,
loadExportFile, assetProfileDummyData,
symbolProfileDummyData, loadExportFile,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
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 { 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 { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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 { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { Activity, ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { join } from 'node:path'; import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return { return {
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock; return CurrentRateServiceMock;
}) })
}; };
}); });
jest.mock( jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => { () => {
return { return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => { PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock; return PortfolioSnapshotServiceMock;
}) })
}; };
} }
); );
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return { return {
RedisCacheService: jest.fn().mockImplementation(() => { RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock; return RedisCacheServiceMock;
}) })
}; };
}); });
describe('PortfolioCalculator', () => { describe('PortfolioCalculator', () => {
let exportResponse: ExportResponse; let exportResponse: ExportResponse;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let currentRateService: CurrentRateService; let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService; let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory; let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService; let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService; let redisCacheService: RedisCacheService;
beforeAll(() => { beforeAll(() => {
exportResponse = loadExportFile( exportResponse = loadExportFile(
join( join(
__dirname, __dirname,
'../../../../../../../test/import/ok/novn-buy-and-sell.json' '../../../../../../../test/import/ok/novn-buy-and-sell.json'
) )
); );
}); });
beforeEach(() => { beforeEach(() => {
configurationService = new ConfigurationService(); configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null); currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService( exchangeRateDataService = new ExchangeRateDataService(
null, null,
null, null,
null, null,
null null
); );
portfolioSnapshotService = new PortfolioSnapshotService(null, null); portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null); redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory( portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService, configurationService,
currentRateService, currentRateService,
exchangeRateDataService, exchangeRateDataService,
portfolioSnapshotService, portfolioSnapshotService,
redisCacheService redisCacheService
); );
}); });
describe('get current positions', () => { describe('get current positions', () => {
it.only('with NOVN.SW buy and sell', async () => { it.only('with NOVN.SW buy and sell', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime());
const activities: Activity[] = exportResponse.activities.map( const activities: Activity[] = exportResponse.activities.map(
(activity) => ({ (activity) => ({
...activityDummyData, ...activityDummyData,
...activity, ...activity,
date: parseDate(activity.date), assetProfile: {
feeInAssetProfileCurrency: activity.fee, ...assetProfileDummyData,
feeInBaseCurrency: activity.fee, currency: activity.currency,
SymbolProfile: { dataSource: activity.dataSource,
...symbolProfileDummyData, name: 'Novartis AG',
currency: activity.currency, symbol: activity.symbol
dataSource: activity.dataSource, },
name: 'Novartis AG', date: parseDate(activity.date),
symbol: activity.symbol feeInAssetProfileCurrency: activity.fee,
}, feeInBaseCurrency: activity.fee,
unitPriceInAssetProfileCurrency: activity.unitPrice unitPriceInAssetProfileCurrency: activity.unitPrice
}) })
); );
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities, activities,
calculationType: PerformanceCalculationType.ROAI, calculationType: PerformanceCalculationType.ROAI,
currency: exportResponse.user.settings.currency, currency: exportResponse.user.settings.currency,
userId: userDummyData.id userId: userDummyData.id
}); });
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData, data: portfolioSnapshot.historicalData,
groupBy: 'year' groupBy: 'year'
}); });
expect(portfolioSnapshot.historicalData[0]).toEqual({ expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2022-03-06', date: '2022-03-06',
investmentValueWithCurrencyEffect: 0, investmentValueWithCurrencyEffect: 0,
netPerformance: 0, netPerformance: 0,
netPerformanceInPercentage: 0, netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0,
netWorth: 0, netWorth: 0,
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0, totalInvestmentValueWithCurrencyEffect: 0,
value: 0, value: 0,
valueWithCurrencyEffect: 0 valueWithCurrencyEffect: 0
}); });
/** /**
* Closing price on 2022-03-07 is unknown, * Closing price on 2022-03-07 is unknown,
* hence it uses the last unit price (2022-04-11): 87.8 * hence it uses the last unit price (2022-04-11): 87.8
*/ */
expect(portfolioSnapshot.historicalData[1]).toEqual({ expect(portfolioSnapshot.historicalData[1]).toEqual({
date: '2022-03-07', date: '2022-03-07',
investmentValueWithCurrencyEffect: 151.6, investmentValueWithCurrencyEffect: 151.6,
netPerformance: 24, // 2 * (87.8 - 75.8) = 24 netPerformance: 24, // 2 * (87.8 - 75.8) = 24
netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438
netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438
netPerformanceWithCurrencyEffect: 24, netPerformanceWithCurrencyEffect: 24,
netWorth: 175.6, // 2 * 87.8 = 175.6 netWorth: 175.6, // 2 * 87.8 = 175.6
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 151.6, totalInvestment: 151.6,
totalInvestmentValueWithCurrencyEffect: 151.6, totalInvestmentValueWithCurrencyEffect: 151.6,
value: 175.6, // 2 * 87.8 = 175.6 value: 175.6, // 2 * 87.8 = 175.6
valueWithCurrencyEffect: 175.6 valueWithCurrencyEffect: 175.6
}); });
expect( expect(
portfolioSnapshot.historicalData[ portfolioSnapshot.historicalData[
portfolioSnapshot.historicalData.length - 1 portfolioSnapshot.historicalData.length - 1
] ]
).toEqual({ ).toEqual({
date: '2022-04-11', date: '2022-04-11',
investmentValueWithCurrencyEffect: 0, investmentValueWithCurrencyEffect: 0,
netPerformance: 19.86, netPerformance: 19.86,
netPerformanceInPercentage: 0.13100263852242744, netPerformanceInPercentage: 0.13100263852242744,
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744,
netPerformanceWithCurrencyEffect: 19.86, netPerformanceWithCurrencyEffect: 19.86,
netWorth: 0, netWorth: 0,
totalAccountBalance: 0, totalAccountBalance: 0,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0, totalInvestmentValueWithCurrencyEffect: 0,
value: 0, value: 0,
valueWithCurrencyEffect: 0 valueWithCurrencyEffect: 0
}); });
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 2, activitiesCount: 2,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2022-03-07', dateOfFirstActivity: '2022-03-07',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
feeInBaseCurrency: new Big('0'), feeInBaseCurrency: new Big('0'),
grossPerformance: new Big('19.86'), grossPerformance: new Big('19.86'),
grossPerformancePercentage: new Big('0.13100263852242744063'), grossPerformancePercentage: new Big('0.13100263852242744063'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
'0.13100263852242744063' '0.13100263852242744063'
), ),
grossPerformanceWithCurrencyEffect: new Big('19.86'), grossPerformanceWithCurrencyEffect: new Big('19.86'),
investment: new Big('0'), investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'), investmentWithCurrencyEffect: new Big('0'),
netPerformance: new Big('19.86'), netPerformance: new Big('19.86'),
netPerformancePercentage: new Big('0.13100263852242744063'), netPerformancePercentage: new Big('0.13100263852242744063'),
netPerformancePercentageWithCurrencyEffectMap: { netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('0.13100263852242744063') max: new Big('0.13100263852242744063')
}, },
netPerformanceWithCurrencyEffectMap: { netPerformanceWithCurrencyEffectMap: {
max: new Big('19.86') max: new Big('19.86')
}, },
marketPrice: 87.8, marketPrice: 87.8,
marketPriceInBaseCurrency: 87.8, marketPriceInBaseCurrency: 87.8,
quantity: new Big('0'), quantity: new Big('0'),
symbol: 'NOVN.SW', symbol: 'NOVN.SW',
tags: [], tags: [],
timeWeightedInvestment: new Big('151.6'), timeWeightedInvestment: new Big('151.6'),
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'),
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
totalFeesWithCurrencyEffect: new Big('0'), totalFeesWithCurrencyEffect: new Big('0'),
totalInterestWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'), totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'), totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0') totalLiabilitiesWithCurrencyEffect: new Big('0')
}); });
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject(
expect.objectContaining({ expect.objectContaining({
netPerformance: 19.86, netPerformance: 19.86,
netPerformanceInPercentage: 0.13100263852242744063, netPerformanceInPercentage: 0.13100263852242744063,
netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063,
netPerformanceWithCurrencyEffect: 19.86, netPerformanceWithCurrencyEffect: 19.86,
totalInvestment: 0, totalInvestment: 0,
totalInvestmentValueWithCurrencyEffect: 0 totalInvestmentValueWithCurrencyEffect: 0
}) })
); );
expect(investments).toEqual([ expect(investments).toEqual([
{ date: '2022-03-07', investment: new Big('151.6') }, { date: '2022-03-07', investment: new Big('151.6') },
{ date: '2022-04-08', investment: new Big('0') } { date: '2022-04-08', investment: new Big('0') }
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2022-03-01', investment: 151.6 }, { date: '2022-03-01', investment: 151.6 },
{ date: '2022-04-01', investment: -151.6 } { date: '2022-04-01', investment: -151.6 }
]); ]);
expect(investmentsByYear).toEqual([ expect(investmentsByYear).toEqual([
{ date: '2022-01-01', investment: 0 } { date: '2022-01-01', investment: 0 }
]); ]);
}); });
}); });
}); });

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts

@ -1,6 +1,6 @@
import { import {
activityDummyData, activityDummyData,
symbolProfileDummyData, assetProfileDummyData,
userDummyData userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => {
const activities: Activity[] = [ const activities: Activity[] = [
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2022-01-01'), assetProfile: {
feeInAssetProfileCurrency: 0, ...assetProfileDummyData,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
dataSource: 'MANUAL', dataSource: 'MANUAL',
name: 'Penthouse Apartment', name: 'Penthouse Apartment',
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde' symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde'
}, },
date: new Date('2022-01-01'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'BUY', type: 'BUY',
unitPriceInAssetProfileCurrency: 500000 unitPriceInAssetProfileCurrency: 500000
} }

2
apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts

@ -2,7 +2,7 @@ import { Activity } from '@ghostfolio/common/interfaces';
export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> { export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
assetProfile: Pick< assetProfile: Pick<
Activity['SymbolProfile'], Activity['assetProfile'],
'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId' 'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId'
>; >;
date: string; date: string;

6
apps/api/src/app/portfolio/portfolio.service.spec.ts

@ -373,15 +373,15 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' },
quantity: 1, quantity: 1,
SymbolProfile: { symbol: 'AAPL' },
type: 'BUY' type: 'BUY'
}, },
{ {
account: null, account: null,
accountId: null, accountId: null,
assetProfile: { symbol: 'BABA' },
quantity: 2, quantity: 2,
SymbolProfile: { symbol: 'BABA' },
type: 'BUY' type: 'BUY'
} }
], ],
@ -409,8 +409,8 @@ describe('PortfolioService', () => {
{ {
account, account,
accountId: account.id, accountId: account.id,
assetProfile: { symbol: 'AAPL' },
quantity: 1, quantity: 1,
SymbolProfile: { symbol: 'AAPL' },
type: 'BUY' type: 'BUY'
} }
], ],

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

@ -829,10 +829,9 @@ export class PortfolioService {
timeWeightedInvestmentWithCurrencyEffect timeWeightedInvestmentWithCurrencyEffect
} = holding; } = holding;
const activitiesOfHolding = activities.filter(({ SymbolProfile }) => { const activitiesOfHolding = activities.filter(({ assetProfile }) => {
return ( return (
SymbolProfile.dataSource === dataSource && assetProfile.dataSource === dataSource && assetProfile.symbol === symbol
SymbolProfile.symbol === symbol
); );
}); });
@ -2056,11 +2055,11 @@ export class PortfolioService {
.filter(({ isDraft, type }) => { .filter(({ isDraft, type }) => {
return isDraft === false && type === activityType; return isDraft === false && type === activityType;
}) })
.map(({ currency, quantity, SymbolProfile, unitPrice }) => { .map(({ assetProfile, currency, quantity, unitPrice }) => {
return new Big( return new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
new Big(quantity).mul(unitPrice).toNumber(), new Big(quantity).mul(unitPrice).toNumber(),
currency ?? SymbolProfile.currency, currency ?? assetProfile.currency,
userCurrency userCurrency
) )
); );
@ -2198,16 +2197,11 @@ export class PortfolioService {
} }
} }
for (const { for (const { account, assetProfile, quantity, type } of ordersByAccount) {
account,
quantity,
SymbolProfile,
type
} of ordersByAccount) {
const currentValueOfSymbolInBaseCurrency = const currentValueOfSymbolInBaseCurrency =
getFactor(type) * getFactor(type) *
quantity * quantity *
(portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? (portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ??
0); 0);
if (accounts[account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) { if (accounts[account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {

7
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -78,8 +78,12 @@ export class TransformDataSourceInResponseInterceptor<
valueMap, valueMap,
object: data, object: data,
paths: [ paths: [
'activities[*].assetProfile.dataSource',
'activities[*].dataSource', 'activities[*].dataSource',
/* @deprecated */
'activities[*].SymbolProfile.dataSource', 'activities[*].SymbolProfile.dataSource',
'assetProfile.dataSource', 'assetProfile.dataSource',
'benchmarks[*].dataSource', 'benchmarks[*].dataSource',
'errors[*].dataSource', 'errors[*].dataSource',
@ -88,7 +92,10 @@ export class TransformDataSourceInResponseInterceptor<
'holdings[*].assetProfile.dataSource', 'holdings[*].assetProfile.dataSource',
'holdings[*].dataSource', 'holdings[*].dataSource',
'items[*].dataSource', 'items[*].dataSource',
/* @deprecated */
'SymbolProfile.dataSource', 'SymbolProfile.dataSource',
'watchlist[*].dataSource' 'watchlist[*].dataSource'
] ]
}); });

2
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -409,10 +409,10 @@ export class GfActivitiesPageComponent implements OnInit {
activity: { activity: {
...aActivity, ...aActivity,
accountId: aActivity?.accountId, accountId: aActivity?.accountId,
assetProfile: null,
date: new Date(), date: new Date(),
id: null, id: null,
fee: 0, fee: 0,
SymbolProfile: null,
type: aActivity?.type ?? 'BUY', type: aActivity?.type ?? 'BUY',
unitPrice: null unitPrice: null
}, },

30
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -119,7 +119,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
} }
public ngOnInit() { public ngOnInit() {
this.currencyOfAssetProfile = this.data.activity?.SymbolProfile?.currency; this.currencyOfAssetProfile = this.data.activity?.assetProfile?.currency;
this.hasPermissionToCreateOwnTag = this.hasPermissionToCreateOwnTag =
this.data.user?.settings?.isExperimentalFeatures && this.data.user?.settings?.isExperimentalFeatures &&
hasPermission(this.data.user?.permissions, permissions.createOwnTag); hasPermission(this.data.user?.permissions, permissions.createOwnTag);
@ -185,31 +185,31 @@ export class GfCreateOrUpdateActivityDialogComponent {
? this.data.accounts[0].id ? this.data.accounts[0].id
: this.data.activity?.accountId : this.data.activity?.accountId
], ],
assetClass: [this.data.activity?.SymbolProfile?.assetClass], assetClass: [this.data.activity?.assetProfile?.assetClass],
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass], assetSubClass: [this.data.activity?.assetProfile?.assetSubClass],
comment: [this.data.activity?.comment], comment: [this.data.activity?.comment],
currency: [ currency: [
this.data.activity?.SymbolProfile?.currency, this.data.activity?.assetProfile?.currency,
Validators.required Validators.required
], ],
currencyOfUnitPrice: [ currencyOfUnitPrice: [
this.data.activity?.currency ?? this.data.activity?.currency ??
this.data.activity?.SymbolProfile?.currency, this.data.activity?.assetProfile?.currency,
Validators.required Validators.required
], ],
dataSource: [ dataSource: [
this.data.activity?.SymbolProfile?.dataSource, this.data.activity?.assetProfile?.dataSource,
Validators.required Validators.required
], ],
date: [this.data.activity?.date, Validators.required], date: [this.data.activity?.date, Validators.required],
fee: [this.data.activity?.fee, Validators.required], fee: [this.data.activity?.fee, Validators.required],
name: [this.data.activity?.SymbolProfile?.name, Validators.required], name: [this.data.activity?.assetProfile?.name, Validators.required],
quantity: [this.data.activity?.quantity, Validators.required], quantity: [this.data.activity?.quantity, Validators.required],
searchSymbol: [ searchSymbol: [
this.data.activity?.SymbolProfile this.data.activity?.assetProfile
? { ? {
dataSource: this.data.activity?.SymbolProfile?.dataSource, dataSource: this.data.activity?.assetProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol symbol: this.data.activity?.assetProfile?.symbol
} }
: null, : null,
Validators.required Validators.required
@ -310,7 +310,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.activityForm.get('searchSymbol')?.valueChanges.subscribe(() => { this.activityForm.get('searchSymbol')?.valueChanges.subscribe(() => {
if (this.activityForm.get('searchSymbol')?.invalid) { if (this.activityForm.get('searchSymbol')?.invalid) {
this.data.activity.SymbolProfile = null; this.data.activity.assetProfile = null;
} else if ( } else if (
['BUY', 'DIVIDEND', 'SELL'].includes( ['BUY', 'DIVIDEND', 'SELL'].includes(
this.activityForm.get('type')?.value this.activityForm.get('type')?.value
@ -454,11 +454,11 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.activityForm.get('type')?.disable(); this.activityForm.get('type')?.disable();
} }
if (this.data.activity?.SymbolProfile?.symbol) { if (this.data.activity?.assetProfile?.symbol) {
this.dataService this.dataService
.fetchSymbolItem({ .fetchSymbolItem({
dataSource: this.data.activity?.SymbolProfile?.dataSource, dataSource: this.data.activity?.assetProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol symbol: this.data.activity?.assetProfile?.symbol
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ marketPrice }) => { .subscribe(({ marketPrice }) => {
@ -569,7 +569,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
}) })
.pipe( .pipe(
catchError(() => { catchError(() => {
this.data.activity.SymbolProfile = null; this.data.activity.assetProfile = null;
this.isLoading = false; this.isLoading = false;

4
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts

@ -4,9 +4,9 @@ import { Account } from '@prisma/client';
export interface CreateOrUpdateActivityDialogParams { export interface CreateOrUpdateActivityDialogParams {
accounts: Account[]; accounts: Account[];
activity: Partial<Omit<Activity, 'id' | 'SymbolProfile' | 'unitPrice'>> & { activity: Partial<Omit<Activity, 'assetProfile' | 'id' | 'unitPrice'>> & {
assetProfile: Activity['assetProfile'] | null;
id: string | null; id: string | null;
SymbolProfile: Activity['SymbolProfile'] | null;
unitPrice: number | null; unitPrice: number | null;
}; };
user: User; user: User;

8
apps/client/src/app/services/import-activities.service.ts

@ -165,12 +165,12 @@ export class ImportActivitiesService {
private convertToCreateOrderDto({ private convertToCreateOrderDto({
accountId, accountId,
assetProfile,
comment, comment,
currency, currency,
date, date,
fee, fee,
quantity, quantity,
SymbolProfile,
tags, tags,
type, type,
unitPrice, unitPrice,
@ -184,10 +184,10 @@ export class ImportActivitiesService {
updateAccountBalance, updateAccountBalance,
accountId: accountId ?? undefined, accountId: accountId ?? undefined,
comment: comment ?? undefined, comment: comment ?? undefined,
currency: currency ?? SymbolProfile.currency ?? '', currency: currency ?? assetProfile.currency ?? '',
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
date: date.toString(), date: date.toString(),
symbol: SymbolProfile.symbol, symbol: assetProfile.symbol,
tags: tags?.map(({ id }) => { tags: tags?.map(({ id }) => {
return id; return id;
}) })

12
libs/common/src/lib/config.ts

@ -106,13 +106,20 @@ export const DEFAULT_REDACTED_PATHS = [
'accounts[*].valueInBaseCurrency', 'accounts[*].valueInBaseCurrency',
'activities[*].account.balance', 'activities[*].account.balance',
'activities[*].account.comment', 'activities[*].account.comment',
'activities[*].assetProfile.symbolMapping',
'activities[*].assetProfile.watchedByCount',
'activities[*].comment', 'activities[*].comment',
'activities[*].fee', 'activities[*].fee',
'activities[*].feeInAssetProfileCurrency', 'activities[*].feeInAssetProfileCurrency',
'activities[*].feeInBaseCurrency', 'activities[*].feeInBaseCurrency',
'activities[*].quantity', 'activities[*].quantity',
/* @deprecated */
'activities[*].SymbolProfile.symbolMapping', 'activities[*].SymbolProfile.symbolMapping',
/* @deprecated */
'activities[*].SymbolProfile.watchedByCount', 'activities[*].SymbolProfile.watchedByCount',
'activities[*].value', 'activities[*].value',
'activities[*].valueInBaseCurrency', 'activities[*].valueInBaseCurrency',
'balance', 'balance',
@ -143,8 +150,13 @@ export const DEFAULT_REDACTED_PATHS = [
'platforms[*].balance', 'platforms[*].balance',
'platforms[*].valueInBaseCurrency', 'platforms[*].valueInBaseCurrency',
'quantity', 'quantity',
/* @deprecated */
'SymbolProfile.symbolMapping', 'SymbolProfile.symbolMapping',
/* @deprecated */
'SymbolProfile.watchedByCount', 'SymbolProfile.watchedByCount',
'totalBalanceInBaseCurrency', 'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency', 'totalDividendInBaseCurrency',
'totalInterestInBaseCurrency', 'totalInterestInBaseCurrency',

8
libs/common/src/lib/interfaces/activities.interface.ts

@ -5,10 +5,16 @@ import { Order, Tag } from '@prisma/client';
export interface Activity extends Order { export interface Activity extends Order {
account?: AccountWithPlatform; account?: AccountWithPlatform;
assetProfile: EnhancedSymbolProfile;
error?: ActivityError; error?: ActivityError;
feeInAssetProfileCurrency: number; feeInAssetProfileCurrency: number;
feeInBaseCurrency: number; feeInBaseCurrency: number;
SymbolProfile: EnhancedSymbolProfile;
/**
* @deprecated Use `assetProfile` instead
*/
SymbolProfile?: EnhancedSymbolProfile;
tagIds?: string[]; tagIds?: string[];
tags?: Tag[]; tags?: Tag[];
unitPriceInAssetProfileCurrency: number; unitPriceInAssetProfileCurrency: number;

6
libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts

@ -26,7 +26,13 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
Order, Order,
'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice' 'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice'
> & { > & {
assetProfile?: EnhancedSymbolProfile;
/**
* @deprecated Use `assetProfile` instead
*/
SymbolProfile?: EnhancedSymbolProfile; SymbolProfile?: EnhancedSymbolProfile;
value: number; value: number;
valueInBaseCurrency: number; valueInBaseCurrency: number;
})[]; })[];

16
libs/ui/src/lib/activities-table/activities-table.component.html

@ -149,9 +149,9 @@
<th *matHeaderCellDef class="px-1" mat-header-cell></th> <th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell> <td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-entity-logo <gf-entity-logo
[dataSource]="element.SymbolProfile?.dataSource" [dataSource]="element.assetProfile?.dataSource"
[symbol]="element.SymbolProfile?.symbol" [symbol]="element.assetProfile?.symbol"
[tooltip]="element.SymbolProfile?.name" [tooltip]="element.assetProfile?.name"
/> />
</td> </td>
</ng-container> </ng-container>
@ -162,18 +162,18 @@
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="align-items-center d-flex line-height-1 text-nowrap"> <div class="align-items-center d-flex line-height-1 text-nowrap">
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span> <span class="text-truncate">{{ element.assetProfile?.name }}</span>
@if (element.isDraft) { @if (element.isDraft) {
<span class="badge badge-secondary ml-1" i18n>Draft</span> <span class="badge badge-secondary ml-1" i18n>Draft</span>
} }
</div> </div>
@if ( @if (
element.SymbolProfile?.dataSource !== 'MANUAL' && element.assetProfile?.dataSource !== 'MANUAL' &&
!isUUID(element.SymbolProfile?.symbol) !isUUID(element.assetProfile?.symbol)
) { ) {
<div> <div>
<small class="text-muted">{{ <small class="text-muted">{{
element.SymbolProfile?.symbol | gfSymbol element.assetProfile?.symbol | gfSymbol
}}</small> }}</small>
</div> </div>
} }
@ -309,7 +309,7 @@
class="d-none d-lg-table-cell px-1" class="d-none d-lg-table-cell px-1"
mat-cell mat-cell
> >
{{ element.currency ?? element.SymbolProfile?.currency }} {{ element.currency ?? element.assetProfile?.currency }}
</td> </td>
</ng-container> </ng-container>

10
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -56,7 +56,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: undefined, comment: undefined,
@ -123,7 +123,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: undefined, comment: undefined,
@ -190,7 +190,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'LIQUIDITY', assetClass: 'LIQUIDITY',
assetSubClass: 'CRYPTOCURRENCY', assetSubClass: 'CRYPTOCURRENCY',
comment: undefined, comment: undefined,
@ -257,7 +257,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'FIXED_INCOME', assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND', assetSubClass: 'BOND',
comment: 'No data', comment: 'No data',
@ -324,7 +324,7 @@ const activities: Activity[] = [
url: 'https://interactivebrokers.com' url: 'https://interactivebrokers.com'
} }
}, },
SymbolProfile: { assetProfile: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
comment: undefined, comment: undefined,

4
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -285,8 +285,8 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
} }
} else if (this.canClickActivity(activity)) { } else if (this.canClickActivity(activity)) {
this.activityClicked.emit({ this.activityClicked.emit({
dataSource: activity.SymbolProfile.dataSource, dataSource: activity.assetProfile.dataSource,
symbol: activity.SymbolProfile.symbol symbol: activity.assetProfile.symbol
}); });
} }
} }

Loading…
Cancel
Save