diff --git a/CHANGELOG.md b/CHANGELOG.md index 83146cce0..634f596cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - 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 diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index bd08d05f9..427cddf4c 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -455,6 +455,23 @@ export class ActivitiesService { userId, accountId: account.id, 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, createdAt: new Date(balanceItem.date), currency: account.currency, @@ -823,6 +840,7 @@ export class ActivitiesService { return { ...order, + assetProfile, feeInAssetProfileCurrency, feeInBaseCurrency, unitPriceInAssetProfileCurrency, diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index f43e51f7b..a87d36218 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -110,26 +110,27 @@ export class PublicController { ? [] : activities.map( ({ + assetProfile, currency, date, fee, quantity, - SymbolProfile, type, unitPrice, value, valueInBaseCurrency }) => { return { + assetProfile, currency, date, fee, quantity, - SymbolProfile, type, unitPrice, value, - valueInBaseCurrency + valueInBaseCurrency, + SymbolProfile: assetProfile }; } ); diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 8ebfde13d..a94479562 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -124,8 +124,8 @@ export class ExportService { const customAssetProfiles = uniqBy( activities - .map(({ SymbolProfile }) => { - return SymbolProfile; + .map(({ assetProfile }) => { + return assetProfile; }) .filter(({ userId: assetProfileUserId }) => { return assetProfileUserId === userId; @@ -224,13 +224,13 @@ export class ExportService { activities: activities.map( ({ accountId, + assetProfile, comment, currency, date, fee, id, quantity, - SymbolProfile, tags: currentTags, type, unitPrice @@ -243,10 +243,10 @@ export class ExportService { quantity, type, unitPrice, - currency: currency ?? SymbolProfile.currency, - dataSource: SymbolProfile.dataSource, + currency: currency ?? assetProfile.currency, + dataSource: assetProfile.dataSource, date: date.toISOString(), - symbol: SymbolProfile.symbol, + symbol: assetProfile.symbol, tags: currentTags.map(({ id: tagId }) => { return tagId; }) diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 9b1aa417c..07019e694 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -122,11 +122,11 @@ export class ImportService { const isDuplicate = activities.some((activity) => { return ( activity.accountId === account?.id && - activity.SymbolProfile.currency === assetProfile.currency && - activity.SymbolProfile.dataSource === assetProfile.dataSource && + activity.assetProfile.currency === assetProfile.currency && + activity.assetProfile.dataSource === assetProfile.dataSource && isSameSecond(activity.date, date) && activity.quantity === quantity && - activity.SymbolProfile.symbol === assetProfile.symbol && + activity.assetProfile.symbol === assetProfile.symbol && activity.type === 'DIVIDEND' && activity.unitPrice === marketPrice ); @@ -138,6 +138,7 @@ export class ImportService { return { account, + assetProfile, date, error, quantity, @@ -463,19 +464,18 @@ export class ImportService { const error = activity.error; const fee = activity.fee; const quantity = activity.quantity; - const SymbolProfile = activity.SymbolProfile; const tagIds = activity.tagIds ?? []; const type = activity.type; const unitPrice = activity.unitPrice; const assetProfile = assetProfiles[ getAssetProfileIdentifier({ - dataSource: SymbolProfile.dataSource, - symbol: SymbolProfile.symbol + dataSource: activity.assetProfile.dataSource, + symbol: activity.assetProfile.symbol }) ] ?? { - dataSource: SymbolProfile.dataSource, - symbol: SymbolProfile.symbol + dataSource: activity.assetProfile.dataSource, + symbol: activity.assetProfile.symbol }; const { assetClass, @@ -619,6 +619,8 @@ export class ImportService { activities.push({ ...order, + // @ts-ignore + assetProfile, error, value, valueInBaseCurrency, @@ -633,19 +635,19 @@ export class ImportService { if (!isDryRun) { // Gather symbol data in the background, if not dry run - const uniqueActivities = uniqBy(activities, ({ SymbolProfile }) => { + const uniqueActivities = uniqBy(activities, ({ assetProfile }) => { return getAssetProfileIdentifier({ - dataSource: SymbolProfile.dataSource, - symbol: SymbolProfile.symbol + dataSource: assetProfile.dataSource, + symbol: assetProfile.symbol }); }); this.dataGatheringService.gatherSymbols({ - dataGatheringItems: uniqueActivities.map(({ date, SymbolProfile }) => { + dataGatheringItems: uniqueActivities.map(({ assetProfile, date }) => { return { date, - dataSource: SymbolProfile.dataSource, - symbol: SymbolProfile.symbol + dataSource: assetProfile.dataSource, + symbol: assetProfile.symbol }; }), priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH @@ -692,12 +694,12 @@ export class ImportService { activity.accountId === accountId && activity.comment === comment && (activity.currency === currency || - activity.SymbolProfile.currency === currency) && - activity.SymbolProfile.dataSource === dataSource && + activity.assetProfile.currency === currency) && + activity.assetProfile.dataSource === dataSource && isSameSecond(activity.date, date) && activity.fee === fee && activity.quantity === quantity && - activity.SymbolProfile.symbol === symbol && + activity.assetProfile.symbol === symbol && activity.type === type && activity.unitPrice === unitPrice ); @@ -717,7 +719,7 @@ export class ImportService { quantity, type, unitPrice, - SymbolProfile: { + assetProfile: { dataSource, symbol, activitiesCount: undefined, diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts index f4c99916f..5e6bfba99 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts @@ -22,7 +22,7 @@ export const activityDummyData = { valueInBaseCurrency: undefined }; -export const symbolProfileDummyData = { +export const assetProfileDummyData = { activitiesCount: undefined, assetClass: undefined, assetSubClass: undefined, diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index e73e222ac..cee94f020 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -121,11 +121,11 @@ export abstract class PortfolioCalculator { this.activities = activities .map( ({ + assetProfile, date, feeInAssetProfileCurrency, feeInBaseCurrency, quantity, - SymbolProfile, tags = [], type, unitPriceInAssetProfileCurrency @@ -141,9 +141,9 @@ export abstract class PortfolioCalculator { } return { + assetProfile, tags, type, - assetProfile: SymbolProfile, date: format(date, DATE_FORMAT), fee: new Big(feeInAssetProfileCurrency), feeInBaseCurrency: new Big(feeInBaseCurrency), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts index f21418cb4..fe23af04b 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-22'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-22'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 142.9 }, { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.65, - feeInBaseCurrency: 1.65, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.65, + feeInBaseCurrency: 1.65, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 136.6 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index e2cba2e61..061aaf817 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -1,231 +1,231 @@ -import { - activityDummyData, - symbolProfileDummyData, - userDummyData -} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; -import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; -import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; -import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; -import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; -import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity } from '@ghostfolio/common/interfaces'; -import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; - -import { Big } from 'big.js'; - -jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { - return { - CurrentRateService: jest.fn().mockImplementation(() => { - return CurrentRateServiceMock; - }) - }; -}); - -jest.mock( - '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', - () => { - return { - PortfolioSnapshotService: jest.fn().mockImplementation(() => { - return PortfolioSnapshotServiceMock; - }) - }; - } -); - -jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { - return { - RedisCacheService: jest.fn().mockImplementation(() => { - return RedisCacheServiceMock; - }) - }; -}); - -describe('PortfolioCalculator', () => { - let configurationService: ConfigurationService; - let currentRateService: CurrentRateService; - let exchangeRateDataService: ExchangeRateDataService; - let portfolioCalculatorFactory: PortfolioCalculatorFactory; - let portfolioSnapshotService: PortfolioSnapshotService; - let redisCacheService: RedisCacheService; - - beforeEach(() => { - configurationService = new ConfigurationService(); - - currentRateService = new CurrentRateService(null, null, null, null); - - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - - portfolioSnapshotService = new PortfolioSnapshotService(null, null); - - redisCacheService = new RedisCacheService(null, null); - - portfolioCalculatorFactory = new PortfolioCalculatorFactory( - configurationService, - currentRateService, - exchangeRateDataService, - portfolioSnapshotService, - redisCacheService - ); - }); - - describe('get current positions', () => { - it.only('with BALN.SW buy and sell in two activities', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); - - const activities: Activity[] = [ - { - ...activityDummyData, - date: new Date('2021-11-22'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'BUY', - unitPriceInAssetProfileCurrency: 142.9 - }, - { - ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.65, - feeInBaseCurrency: 1.65, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'SELL', - unitPriceInAssetProfileCurrency: 136.6 - }, - { - ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'SELL', - unitPriceInAssetProfileCurrency: 136.6 - } - ]; - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - const investments = portfolioCalculator.getInvestments(); - - const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'month' - }); - - const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'year' - }); - - expect(portfolioSnapshot).toMatchObject({ - currentValueInBaseCurrency: new Big('0'), - errors: [], - hasErrors: false, - positions: [ - { - activitiesCount: 3, - averagePrice: new Big('0'), - currency: 'CHF', - dataSource: 'YAHOO', - dateOfFirstActivity: '2021-11-22', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('3.2'), - feeInBaseCurrency: new Big('3.2'), - grossPerformance: new Big('-12.6'), - grossPerformancePercentage: new Big('-0.04408677396780965649'), - grossPerformancePercentageWithCurrencyEffect: new Big( - '-0.04408677396780965649' - ), - grossPerformanceWithCurrencyEffect: new Big('-12.6'), - investment: new Big('0'), - investmentWithCurrencyEffect: new Big('0'), - netPerformancePercentageWithCurrencyEffectMap: { - max: new Big('-0.0552834149755073478') - }, - netPerformanceWithCurrencyEffectMap: { - max: new Big('-15.8') - }, - marketPrice: 148.9, - marketPriceInBaseCurrency: 148.9, - quantity: new Big('0'), - symbol: 'BALN.SW', - tags: [], - timeWeightedInvestment: new Big('285.80000000000000396627'), - timeWeightedInvestmentWithCurrencyEffect: new Big( - '285.80000000000000396627' - ), - valueInBaseCurrency: new Big('0') - } - ], - totalFeesWithCurrencyEffect: new Big('3.2'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0'), - totalLiabilitiesWithCurrencyEffect: new Big('0') - }); - - expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( - expect.objectContaining({ - netPerformance: -15.8, - netPerformanceInPercentage: -0.05528341497550734703, - netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550734703, - netPerformanceWithCurrencyEffect: -15.8, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0 - }) - ); - - expect(investments).toEqual([ - { date: '2021-11-22', investment: new Big('285.8') }, - { date: '2021-11-30', investment: new Big('0') } - ]); - - expect(investmentsByMonth).toEqual([ - { date: '2021-11-01', investment: 0 }, - { date: '2021-12-01', investment: 0 } - ]); - - expect(investmentsByYear).toEqual([ - { date: '2021-01-01', investment: 0 } - ]); - }); - }); -}); +import { + activityDummyData, + assetProfileDummyData, + userDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { 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 { parseDate } from '@ghostfolio/common/helper'; +import { Activity } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +import { Big } from 'big.js'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null, null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with BALN.SW buy and sell in two activities', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + assetProfile: { + ...assetProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + date: new Date('2021-11-22'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, + type: 'BUY', + unitPriceInAssetProfileCurrency: 142.9 + }, + { + ...activityDummyData, + assetProfile: { + ...assetProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.65, + feeInBaseCurrency: 1.65, + quantity: 1, + type: 'SELL', + unitPriceInAssetProfileCurrency: 136.6 + }, + { + ...activityDummyData, + assetProfile: { + ...assetProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + type: 'SELL', + unitPriceInAssetProfileCurrency: 136.6 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'CHF', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'month' + }); + + const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); + + expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + hasErrors: false, + positions: [ + { + activitiesCount: 3, + averagePrice: new Big('0'), + currency: 'CHF', + dataSource: 'YAHOO', + dateOfFirstActivity: '2021-11-22', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('3.2'), + feeInBaseCurrency: new Big('3.2'), + grossPerformance: new Big('-12.6'), + grossPerformancePercentage: new Big('-0.04408677396780965649'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '-0.04408677396780965649' + ), + grossPerformanceWithCurrencyEffect: new Big('-12.6'), + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + netPerformancePercentageWithCurrencyEffectMap: { + max: new Big('-0.0552834149755073478') + }, + netPerformanceWithCurrencyEffectMap: { + max: new Big('-15.8') + }, + marketPrice: 148.9, + marketPriceInBaseCurrency: 148.9, + quantity: new Big('0'), + symbol: 'BALN.SW', + tags: [], + timeWeightedInvestment: new Big('285.80000000000000396627'), + timeWeightedInvestmentWithCurrencyEffect: new Big( + '285.80000000000000396627' + ), + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('3.2'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0') + }); + + expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( + expect.objectContaining({ + netPerformance: -15.8, + netPerformanceInPercentage: -0.05528341497550734703, + netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550734703, + netPerformanceWithCurrencyEffect: -15.8, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0 + }) + ); + + expect(investments).toEqual([ + { date: '2021-11-22', investment: new Big('285.8') }, + { date: '2021-11-30', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2021-11-01', investment: 0 }, + { date: '2021-12-01', investment: 0 } + ]); + + expect(investmentsByYear).toEqual([ + { date: '2021-01-01', investment: 0 } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts index fa397de46..c8e0af46d 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-22'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-22'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 142.9 }, { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.65, - feeInBaseCurrency: 1.65, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.65, + feeInBaseCurrency: 1.65, + quantity: 2, type: 'SELL', unitPriceInAssetProfileCurrency: 136.6 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index db44fe0ae..4922382a9 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 136.6 } @@ -217,17 +217,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 135.0 } @@ -257,17 +257,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 135.0 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts index 1afd9225d..21a8d2056 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -107,17 +107,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = exportResponse.activities.map( (activity) => ({ ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - feeInBaseCurrency: 3.94, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: activity.dataSource, name: 'Bitcoin', symbol: activity.symbol }, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + feeInBaseCurrency: 3.94, unitPriceInAssetProfileCurrency: 44558.42 }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts index 0e5750166..bc80e8996 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -95,17 +95,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = exportResponse.activities.map( (activity) => ({ ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - feeInBaseCurrency: 4.46, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: activity.dataSource, name: 'Bitcoin', symbol: activity.symbol }, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + feeInBaseCurrency: 4.46, unitPriceInAssetProfileCurrency: 44558.42 }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 1e556735d..f20506b06 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -98,33 +98,33 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2015-01-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Bitcoin USD', symbol: 'BTCUSD' }, + date: new Date('2015-01-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 320.43 }, { ...activityDummyData, - date: new Date('2017-12-31'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Bitcoin USD', symbol: 'BTCUSD' }, + date: new Date('2017-12-31'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'SELL', unitPriceInAssetProfileCurrency: 14156.4 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts index 9d84540e3..79c6979ef 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -96,16 +96,16 @@ describe('PortfolioCalculator', () => { (activity) => ({ ...activityDummyData, ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: activity.dataSource, name: 'Bitcoin', symbol: activity.symbol }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, unitPriceInAssetProfileCurrency: activity.unitPrice }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index 0916e18a4..18b0e6d64 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -96,16 +96,16 @@ describe('PortfolioCalculator', () => { (activity) => ({ ...activityDummyData, ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - feeInBaseCurrency: 4.46, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: activity.dataSource, name: 'Bitcoin', symbol: activity.symbol }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + feeInBaseCurrency: 4.46, unitPriceInAssetProfileCurrency: 44558.42 }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts index b55c94b2b..e5798818f 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-09-01'), - feeInAssetProfileCurrency: 49, - feeInBaseCurrency: 49, - quantity: 0, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'MANUAL', name: 'Account Opening Fee', symbol: '2c463fb3-af07-486e-adb0-8301b3d72141' }, + date: new Date('2021-09-01'), + feeInAssetProfileCurrency: 49, + feeInBaseCurrency: 49, + quantity: 0, type: 'FEE', unitPriceInAssetProfileCurrency: 0 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts index bb61d637e..984dc1154 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -97,17 +97,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2023-01-03'), - feeInAssetProfileCurrency: 1, - feeInBaseCurrency: 0.9238, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Alphabet Inc.', symbol: 'GOOGL' }, + date: new Date('2023-01-03'), + feeInAssetProfileCurrency: 1, + feeInBaseCurrency: 0.9238, + quantity: 1, type: 'BUY', unitPriceInAssetProfileCurrency: 89.12 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts index 88afc971d..962cfe2d8 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts @@ -1,190 +1,190 @@ -import { - activityDummyData, - loadExportFile, - symbolProfileDummyData, - userDummyData -} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; -import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; -import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; -import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; -import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; -import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; -import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; - -import { Big } from 'big.js'; -import { join } from 'node:path'; - -jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { - return { - CurrentRateService: jest.fn().mockImplementation(() => { - return CurrentRateServiceMock; - }) - }; -}); - -jest.mock( - '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', - () => { - return { - PortfolioSnapshotService: jest.fn().mockImplementation(() => { - return PortfolioSnapshotServiceMock; - }) - }; - } -); - -jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { - return { - RedisCacheService: jest.fn().mockImplementation(() => { - return RedisCacheServiceMock; - }) - }; -}); - -describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - - let configurationService: ConfigurationService; - let currentRateService: CurrentRateService; - let exchangeRateDataService: ExchangeRateDataService; - let portfolioCalculatorFactory: PortfolioCalculatorFactory; - let portfolioSnapshotService: PortfolioSnapshotService; - let redisCacheService: RedisCacheService; - - beforeAll(() => { - exportResponse = loadExportFile( - join( - __dirname, - '../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json' - ) - ); - }); - - beforeEach(() => { - configurationService = new ConfigurationService(); - - currentRateService = new CurrentRateService(null, null, null, null); - - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - - portfolioSnapshotService = new PortfolioSnapshotService(null, null); - - redisCacheService = new RedisCacheService(null, null); - - portfolioCalculatorFactory = new PortfolioCalculatorFactory( - configurationService, - currentRateService, - exchangeRateDataService, - portfolioSnapshotService, - redisCacheService - ); - }); - - describe('get current positions', () => { - it.only('with JNUG buy and sell', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime()); - - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares', - symbol: activity.symbol - }, - unitPriceInAssetProfileCurrency: activity.unitPrice - }) - ); - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - const investments = portfolioCalculator.getInvestments(); - - const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'month' - }); - - const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'year' - }); - - expect(portfolioSnapshot).toMatchObject({ - currentValueInBaseCurrency: new Big('0'), - errors: [], - hasErrors: false, - positions: [ - { - activitiesCount: 4, - averagePrice: new Big('0'), - currency: 'USD', - dataSource: 'YAHOO', - dateOfFirstActivity: '2025-12-11', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('4'), - feeInBaseCurrency: new Big('4'), - 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) - investment: new Big('0'), - investmentWithCurrencyEffect: new Big('0'), - netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 - netPerformanceWithCurrencyEffectMap: { - max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 - }, - marketPrice: 237.8000030517578, - marketPriceInBaseCurrency: 237.8000030517578, - quantity: new Big('0'), - symbol: 'JNUG', - tags: [], - valueInBaseCurrency: new Big('0') - } - ], - totalFeesWithCurrencyEffect: new Big('4'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0'), - totalLiabilitiesWithCurrencyEffect: new Big('0') - }); - - expect(investments).toEqual([ - { date: '2025-12-11', investment: new Big('1885.05') }, - { date: '2025-12-18', investment: new Big('2041.1') }, - { date: '2025-12-28', investment: new Big('0') } - ]); - - expect(investmentsByMonth).toEqual([ - { date: '2025-12-01', investment: 0 } - ]); - - expect(investmentsByYear).toEqual([ - { date: '2025-01-01', investment: 0 } - ]); - }); - }); -}); +import { + activityDummyData, + assetProfileDummyData, + loadExportFile, + userDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { 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 { parseDate } from '@ghostfolio/common/helper'; +import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +import { Big } from 'big.js'; +import { join } from 'node:path'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let exportResponse: ExportResponse; + + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeAll(() => { + exportResponse = loadExportFile( + join( + __dirname, + '../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json' + ) + ); + }); + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null, null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with JNUG buy and sell', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime()); + + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...activityDummyData, + ...activity, + assetProfile: { + ...assetProfileDummyData, + currency: activity.currency, + dataSource: activity.dataSource, + name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares', + symbol: activity.symbol + }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, + unitPriceInAssetProfileCurrency: activity.unitPrice + }) + ); + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: exportResponse.user.settings.currency, + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'month' + }); + + const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); + + expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + hasErrors: false, + positions: [ + { + activitiesCount: 4, + averagePrice: new Big('0'), + currency: 'USD', + dataSource: 'YAHOO', + dateOfFirstActivity: '2025-12-11', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('4'), + feeInBaseCurrency: new Big('4'), + 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) + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 + netPerformanceWithCurrencyEffectMap: { + max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 + }, + marketPrice: 237.8000030517578, + marketPriceInBaseCurrency: 237.8000030517578, + quantity: new Big('0'), + symbol: 'JNUG', + tags: [], + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('4'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0') + }); + + expect(investments).toEqual([ + { date: '2025-12-11', investment: new Big('1885.05') }, + { date: '2025-12-18', investment: new Big('2041.1') }, + { date: '2025-12-28', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2025-12-01', investment: 0 } + ]); + + expect(investmentsByYear).toEqual([ + { date: '2025-01-01', investment: 0 } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts index 4f2058513..94654af61 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2023-01-01'), // Date in future - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'MANUAL', name: 'Loan', symbol: '55196015-1365-4560-aa60-8751ae6d18f8' }, + date: new Date('2023-01-01'), // Date in future + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'LIABILITY', unitPriceInAssetProfileCurrency: 3000 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts index 36d1ea1ec..b1f030aae 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -78,49 +78,49 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2024-03-08'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 0.3333333333333333, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2024-03-08'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 0.3333333333333333, type: 'BUY', unitPriceInAssetProfileCurrency: 408 }, { ...activityDummyData, - date: new Date('2024-03-13'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 0.6666666666666666, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2024-03-13'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 0.6666666666666666, type: 'BUY', unitPriceInAssetProfileCurrency: 400 }, { ...activityDummyData, - date: new Date('2024-03-14'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2024-03-14'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'SELL', unitPriceInAssetProfileCurrency: 411 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts index c50ef2b34..91c095623 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-09-16'), - feeInAssetProfileCurrency: 19, - feeInBaseCurrency: 19, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2021-09-16'), + feeInAssetProfileCurrency: 19, + feeInBaseCurrency: 19, + quantity: 1, type: 'BUY', unitPriceInAssetProfileCurrency: 298.58 }, { ...activityDummyData, - date: new Date('2021-11-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2021-11-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'DIVIDEND', unitPriceInAssetProfileCurrency: 0.62 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index cdfc78906..8ce62db59 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -99,16 +99,16 @@ describe('PortfolioCalculator', () => { (activity) => ({ ...activityDummyData, ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: activity.currency, dataSource: activity.dataSource, name: 'Novartis AG', symbol: activity.symbol }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, unitPriceInAssetProfileCurrency: activity.unitPrice }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts index 2cd754c82..f86b23d9a 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -1,264 +1,264 @@ -import { - activityDummyData, - loadExportFile, - symbolProfileDummyData, - userDummyData -} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; -import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; -import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; -import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; -import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; -import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; -import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; - -import { Big } from 'big.js'; -import { join } from 'node:path'; - -jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { - return { - CurrentRateService: jest.fn().mockImplementation(() => { - return CurrentRateServiceMock; - }) - }; -}); - -jest.mock( - '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', - () => { - return { - PortfolioSnapshotService: jest.fn().mockImplementation(() => { - return PortfolioSnapshotServiceMock; - }) - }; - } -); - -jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { - return { - RedisCacheService: jest.fn().mockImplementation(() => { - return RedisCacheServiceMock; - }) - }; -}); - -describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - - let configurationService: ConfigurationService; - let currentRateService: CurrentRateService; - let exchangeRateDataService: ExchangeRateDataService; - let portfolioCalculatorFactory: PortfolioCalculatorFactory; - let portfolioSnapshotService: PortfolioSnapshotService; - let redisCacheService: RedisCacheService; - - beforeAll(() => { - exportResponse = loadExportFile( - join( - __dirname, - '../../../../../../../test/import/ok/novn-buy-and-sell.json' - ) - ); - }); - - beforeEach(() => { - configurationService = new ConfigurationService(); - - currentRateService = new CurrentRateService(null, null, null, null); - - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - - portfolioSnapshotService = new PortfolioSnapshotService(null, null); - - redisCacheService = new RedisCacheService(null, null); - - portfolioCalculatorFactory = new PortfolioCalculatorFactory( - configurationService, - currentRateService, - exchangeRateDataService, - portfolioSnapshotService, - redisCacheService - ); - }); - - describe('get current positions', () => { - it.only('with NOVN.SW buy and sell', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Novartis AG', - symbol: activity.symbol - }, - unitPriceInAssetProfileCurrency: activity.unitPrice - }) - ); - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - const investments = portfolioCalculator.getInvestments(); - - const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'month' - }); - - const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'year' - }); - - expect(portfolioSnapshot.historicalData[0]).toEqual({ - date: '2022-03-06', - investmentValueWithCurrencyEffect: 0, - netPerformance: 0, - netPerformanceInPercentage: 0, - netPerformanceInPercentageWithCurrencyEffect: 0, - netPerformanceWithCurrencyEffect: 0, - netWorth: 0, - totalAccountBalance: 0, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0, - value: 0, - valueWithCurrencyEffect: 0 - }); - - /** - * Closing price on 2022-03-07 is unknown, - * hence it uses the last unit price (2022-04-11): 87.8 - */ - expect(portfolioSnapshot.historicalData[1]).toEqual({ - date: '2022-03-07', - investmentValueWithCurrencyEffect: 151.6, - netPerformance: 24, // 2 * (87.8 - 75.8) = 24 - netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 - netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 - netPerformanceWithCurrencyEffect: 24, - netWorth: 175.6, // 2 * 87.8 = 175.6 - totalAccountBalance: 0, - totalInvestment: 151.6, - totalInvestmentValueWithCurrencyEffect: 151.6, - value: 175.6, // 2 * 87.8 = 175.6 - valueWithCurrencyEffect: 175.6 - }); - - expect( - portfolioSnapshot.historicalData[ - portfolioSnapshot.historicalData.length - 1 - ] - ).toEqual({ - date: '2022-04-11', - investmentValueWithCurrencyEffect: 0, - netPerformance: 19.86, - netPerformanceInPercentage: 0.13100263852242744, - netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, - netPerformanceWithCurrencyEffect: 19.86, - netWorth: 0, - totalAccountBalance: 0, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0, - value: 0, - valueWithCurrencyEffect: 0 - }); - - expect(portfolioSnapshot).toMatchObject({ - currentValueInBaseCurrency: new Big('0'), - errors: [], - hasErrors: false, - positions: [ - { - activitiesCount: 2, - averagePrice: new Big('0'), - currency: 'CHF', - dataSource: 'YAHOO', - dateOfFirstActivity: '2022-03-07', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('0'), - feeInBaseCurrency: new Big('0'), - grossPerformance: new Big('19.86'), - grossPerformancePercentage: new Big('0.13100263852242744063'), - grossPerformancePercentageWithCurrencyEffect: new Big( - '0.13100263852242744063' - ), - grossPerformanceWithCurrencyEffect: new Big('19.86'), - investment: new Big('0'), - investmentWithCurrencyEffect: new Big('0'), - netPerformance: new Big('19.86'), - netPerformancePercentage: new Big('0.13100263852242744063'), - netPerformancePercentageWithCurrencyEffectMap: { - max: new Big('0.13100263852242744063') - }, - netPerformanceWithCurrencyEffectMap: { - max: new Big('19.86') - }, - marketPrice: 87.8, - marketPriceInBaseCurrency: 87.8, - quantity: new Big('0'), - symbol: 'NOVN.SW', - tags: [], - timeWeightedInvestment: new Big('151.6'), - timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), - valueInBaseCurrency: new Big('0') - } - ], - totalFeesWithCurrencyEffect: new Big('0'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0'), - totalLiabilitiesWithCurrencyEffect: new Big('0') - }); - - expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( - expect.objectContaining({ - netPerformance: 19.86, - netPerformanceInPercentage: 0.13100263852242744063, - netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, - netPerformanceWithCurrencyEffect: 19.86, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0 - }) - ); - - expect(investments).toEqual([ - { date: '2022-03-07', investment: new Big('151.6') }, - { date: '2022-04-08', investment: new Big('0') } - ]); - - expect(investmentsByMonth).toEqual([ - { date: '2022-03-01', investment: 151.6 }, - { date: '2022-04-01', investment: -151.6 } - ]); - - expect(investmentsByYear).toEqual([ - { date: '2022-01-01', investment: 0 } - ]); - }); - }); -}); +import { + activityDummyData, + assetProfileDummyData, + loadExportFile, + userDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { 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 { parseDate } from '@ghostfolio/common/helper'; +import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +import { Big } from 'big.js'; +import { join } from 'node:path'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let exportResponse: ExportResponse; + + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeAll(() => { + exportResponse = loadExportFile( + join( + __dirname, + '../../../../../../../test/import/ok/novn-buy-and-sell.json' + ) + ); + }); + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null, null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with NOVN.SW buy and sell', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); + + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...activityDummyData, + ...activity, + assetProfile: { + ...assetProfileDummyData, + currency: activity.currency, + dataSource: activity.dataSource, + name: 'Novartis AG', + symbol: activity.symbol + }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, + unitPriceInAssetProfileCurrency: activity.unitPrice + }) + ); + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: exportResponse.user.settings.currency, + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'month' + }); + + const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); + + expect(portfolioSnapshot.historicalData[0]).toEqual({ + date: '2022-03-06', + investmentValueWithCurrencyEffect: 0, + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, + netWorth: 0, + totalAccountBalance: 0, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0, + value: 0, + valueWithCurrencyEffect: 0 + }); + + /** + * Closing price on 2022-03-07 is unknown, + * hence it uses the last unit price (2022-04-11): 87.8 + */ + expect(portfolioSnapshot.historicalData[1]).toEqual({ + date: '2022-03-07', + investmentValueWithCurrencyEffect: 151.6, + netPerformance: 24, // 2 * (87.8 - 75.8) = 24 + netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 + netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 + netPerformanceWithCurrencyEffect: 24, + netWorth: 175.6, // 2 * 87.8 = 175.6 + totalAccountBalance: 0, + totalInvestment: 151.6, + totalInvestmentValueWithCurrencyEffect: 151.6, + value: 175.6, // 2 * 87.8 = 175.6 + valueWithCurrencyEffect: 175.6 + }); + + expect( + portfolioSnapshot.historicalData[ + portfolioSnapshot.historicalData.length - 1 + ] + ).toEqual({ + date: '2022-04-11', + investmentValueWithCurrencyEffect: 0, + netPerformance: 19.86, + netPerformanceInPercentage: 0.13100263852242744, + netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, + netPerformanceWithCurrencyEffect: 19.86, + netWorth: 0, + totalAccountBalance: 0, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0, + value: 0, + valueWithCurrencyEffect: 0 + }); + + expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + hasErrors: false, + positions: [ + { + activitiesCount: 2, + averagePrice: new Big('0'), + currency: 'CHF', + dataSource: 'YAHOO', + dateOfFirstActivity: '2022-03-07', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('0'), + feeInBaseCurrency: new Big('0'), + grossPerformance: new Big('19.86'), + grossPerformancePercentage: new Big('0.13100263852242744063'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.13100263852242744063' + ), + grossPerformanceWithCurrencyEffect: new Big('19.86'), + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + netPerformance: new Big('19.86'), + netPerformancePercentage: new Big('0.13100263852242744063'), + netPerformancePercentageWithCurrencyEffectMap: { + max: new Big('0.13100263852242744063') + }, + netPerformanceWithCurrencyEffectMap: { + max: new Big('19.86') + }, + marketPrice: 87.8, + marketPriceInBaseCurrency: 87.8, + quantity: new Big('0'), + symbol: 'NOVN.SW', + tags: [], + timeWeightedInvestment: new Big('151.6'), + timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0') + }); + + expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( + expect.objectContaining({ + netPerformance: 19.86, + netPerformanceInPercentage: 0.13100263852242744063, + netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, + netPerformanceWithCurrencyEffect: 19.86, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0 + }) + ); + + expect(investments).toEqual([ + { date: '2022-03-07', investment: new Big('151.6') }, + { date: '2022-04-08', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2022-03-01', investment: 151.6 }, + { date: '2022-04-01', investment: -151.6 } + ]); + + expect(investmentsByYear).toEqual([ + { date: '2022-01-01', investment: 0 } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts index 1d6cee0fa..226eaa3d8 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2022-01-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'MANUAL', name: 'Penthouse Apartment', symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde' }, + date: new Date('2022-01-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'BUY', unitPriceInAssetProfileCurrency: 500000 } diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index 8d90c5bc6..becab8c04 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/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 { assetProfile: Pick< - Activity['SymbolProfile'], + Activity['assetProfile'], 'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId' >; date: string; diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index 04142c92b..adddb4013 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -373,15 +373,15 @@ describe('PortfolioService', () => { { account, accountId: account.id, + assetProfile: { symbol: 'AAPL' }, quantity: 1, - SymbolProfile: { symbol: 'AAPL' }, type: 'BUY' }, { account: null, accountId: null, + assetProfile: { symbol: 'BABA' }, quantity: 2, - SymbolProfile: { symbol: 'BABA' }, type: 'BUY' } ], @@ -409,8 +409,8 @@ describe('PortfolioService', () => { { account, accountId: account.id, + assetProfile: { symbol: 'AAPL' }, quantity: 1, - SymbolProfile: { symbol: 'AAPL' }, type: 'BUY' } ], diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index e75e54890..6617b8f9b 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -829,10 +829,9 @@ export class PortfolioService { timeWeightedInvestmentWithCurrencyEffect } = holding; - const activitiesOfHolding = activities.filter(({ SymbolProfile }) => { + const activitiesOfHolding = activities.filter(({ assetProfile }) => { return ( - SymbolProfile.dataSource === dataSource && - SymbolProfile.symbol === symbol + assetProfile.dataSource === dataSource && assetProfile.symbol === symbol ); }); @@ -2056,11 +2055,11 @@ export class PortfolioService { .filter(({ isDraft, type }) => { return isDraft === false && type === activityType; }) - .map(({ currency, quantity, SymbolProfile, unitPrice }) => { + .map(({ assetProfile, currency, quantity, unitPrice }) => { return new Big( this.exchangeRateDataService.toCurrency( new Big(quantity).mul(unitPrice).toNumber(), - currency ?? SymbolProfile.currency, + currency ?? assetProfile.currency, userCurrency ) ); @@ -2198,16 +2197,11 @@ export class PortfolioService { } } - for (const { - account, - quantity, - SymbolProfile, - type - } of ordersByAccount) { + for (const { account, assetProfile, quantity, type } of ordersByAccount) { const currentValueOfSymbolInBaseCurrency = getFactor(type) * quantity * - (portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? + (portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ?? 0); if (accounts[account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) { diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index 56d260d4d..4e75ee9b7 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -78,8 +78,12 @@ export class TransformDataSourceInResponseInterceptor< valueMap, object: data, paths: [ + 'activities[*].assetProfile.dataSource', 'activities[*].dataSource', + + /* @deprecated */ 'activities[*].SymbolProfile.dataSource', + 'assetProfile.dataSource', 'benchmarks[*].dataSource', 'errors[*].dataSource', @@ -88,7 +92,10 @@ export class TransformDataSourceInResponseInterceptor< 'holdings[*].assetProfile.dataSource', 'holdings[*].dataSource', 'items[*].dataSource', + + /* @deprecated */ 'SymbolProfile.dataSource', + 'watchlist[*].dataSource' ] }); diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 397fdce57..3d1c0ceeb 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -409,10 +409,10 @@ export class GfActivitiesPageComponent implements OnInit { activity: { ...aActivity, accountId: aActivity?.accountId, + assetProfile: null, date: new Date(), id: null, fee: 0, - SymbolProfile: null, type: aActivity?.type ?? 'BUY', unitPrice: null }, diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 022a8f2a2..be9ac7b04 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/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() { - this.currencyOfAssetProfile = this.data.activity?.SymbolProfile?.currency; + this.currencyOfAssetProfile = this.data.activity?.assetProfile?.currency; this.hasPermissionToCreateOwnTag = this.data.user?.settings?.isExperimentalFeatures && hasPermission(this.data.user?.permissions, permissions.createOwnTag); @@ -185,31 +185,31 @@ export class GfCreateOrUpdateActivityDialogComponent { ? this.data.accounts[0].id : this.data.activity?.accountId ], - assetClass: [this.data.activity?.SymbolProfile?.assetClass], - assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass], + assetClass: [this.data.activity?.assetProfile?.assetClass], + assetSubClass: [this.data.activity?.assetProfile?.assetSubClass], comment: [this.data.activity?.comment], currency: [ - this.data.activity?.SymbolProfile?.currency, + this.data.activity?.assetProfile?.currency, Validators.required ], currencyOfUnitPrice: [ this.data.activity?.currency ?? - this.data.activity?.SymbolProfile?.currency, + this.data.activity?.assetProfile?.currency, Validators.required ], dataSource: [ - this.data.activity?.SymbolProfile?.dataSource, + this.data.activity?.assetProfile?.dataSource, Validators.required ], date: [this.data.activity?.date, 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], searchSymbol: [ - this.data.activity?.SymbolProfile + this.data.activity?.assetProfile ? { - dataSource: this.data.activity?.SymbolProfile?.dataSource, - symbol: this.data.activity?.SymbolProfile?.symbol + dataSource: this.data.activity?.assetProfile?.dataSource, + symbol: this.data.activity?.assetProfile?.symbol } : null, Validators.required @@ -310,7 +310,7 @@ export class GfCreateOrUpdateActivityDialogComponent { this.activityForm.get('searchSymbol')?.valueChanges.subscribe(() => { if (this.activityForm.get('searchSymbol')?.invalid) { - this.data.activity.SymbolProfile = null; + this.data.activity.assetProfile = null; } else if ( ['BUY', 'DIVIDEND', 'SELL'].includes( this.activityForm.get('type')?.value @@ -454,11 +454,11 @@ export class GfCreateOrUpdateActivityDialogComponent { this.activityForm.get('type')?.disable(); } - if (this.data.activity?.SymbolProfile?.symbol) { + if (this.data.activity?.assetProfile?.symbol) { this.dataService .fetchSymbolItem({ - dataSource: this.data.activity?.SymbolProfile?.dataSource, - symbol: this.data.activity?.SymbolProfile?.symbol + dataSource: this.data.activity?.assetProfile?.dataSource, + symbol: this.data.activity?.assetProfile?.symbol }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ marketPrice }) => { @@ -569,7 +569,7 @@ export class GfCreateOrUpdateActivityDialogComponent { }) .pipe( catchError(() => { - this.data.activity.SymbolProfile = null; + this.data.activity.assetProfile = null; this.isLoading = false; diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts index 37faff91f..a3d58f8eb 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts +++ b/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 { accounts: Account[]; - activity: Partial> & { + activity: Partial> & { + assetProfile: Activity['assetProfile'] | null; id: string | null; - SymbolProfile: Activity['SymbolProfile'] | null; unitPrice: number | null; }; user: User; diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index f5cc74106..ff5679718 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -165,12 +165,12 @@ export class ImportActivitiesService { private convertToCreateOrderDto({ accountId, + assetProfile, comment, currency, date, fee, quantity, - SymbolProfile, tags, type, unitPrice, @@ -184,10 +184,10 @@ export class ImportActivitiesService { updateAccountBalance, accountId: accountId ?? undefined, comment: comment ?? undefined, - currency: currency ?? SymbolProfile.currency ?? '', - dataSource: SymbolProfile.dataSource, + currency: currency ?? assetProfile.currency ?? '', + dataSource: assetProfile.dataSource, date: date.toString(), - symbol: SymbolProfile.symbol, + symbol: assetProfile.symbol, tags: tags?.map(({ id }) => { return id; }) diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 633e13f24..e071a72c5 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -106,13 +106,20 @@ export const DEFAULT_REDACTED_PATHS = [ 'accounts[*].valueInBaseCurrency', 'activities[*].account.balance', 'activities[*].account.comment', + 'activities[*].assetProfile.symbolMapping', + 'activities[*].assetProfile.watchedByCount', 'activities[*].comment', 'activities[*].fee', 'activities[*].feeInAssetProfileCurrency', 'activities[*].feeInBaseCurrency', 'activities[*].quantity', + + /* @deprecated */ 'activities[*].SymbolProfile.symbolMapping', + + /* @deprecated */ 'activities[*].SymbolProfile.watchedByCount', + 'activities[*].value', 'activities[*].valueInBaseCurrency', 'balance', @@ -143,8 +150,13 @@ export const DEFAULT_REDACTED_PATHS = [ 'platforms[*].balance', 'platforms[*].valueInBaseCurrency', 'quantity', + + /* @deprecated */ 'SymbolProfile.symbolMapping', + + /* @deprecated */ 'SymbolProfile.watchedByCount', + 'totalBalanceInBaseCurrency', 'totalDividendInBaseCurrency', 'totalInterestInBaseCurrency', diff --git a/libs/common/src/lib/interfaces/activities.interface.ts b/libs/common/src/lib/interfaces/activities.interface.ts index b9e64984b..1cc867d6e 100644 --- a/libs/common/src/lib/interfaces/activities.interface.ts +++ b/libs/common/src/lib/interfaces/activities.interface.ts @@ -5,10 +5,16 @@ import { Order, Tag } from '@prisma/client'; export interface Activity extends Order { account?: AccountWithPlatform; + assetProfile: EnhancedSymbolProfile; error?: ActivityError; feeInAssetProfileCurrency: number; feeInBaseCurrency: number; - SymbolProfile: EnhancedSymbolProfile; + + /** + * @deprecated Use `assetProfile` instead + */ + SymbolProfile?: EnhancedSymbolProfile; + tagIds?: string[]; tags?: Tag[]; unitPriceInAssetProfileCurrency: number; diff --git a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts index 18c7dc57a..04c8e0c07 100644 --- a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts @@ -26,7 +26,13 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { Order, 'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice' > & { + assetProfile?: EnhancedSymbolProfile; + + /** + * @deprecated Use `assetProfile` instead + */ SymbolProfile?: EnhancedSymbolProfile; + value: number; valueInBaseCurrency: number; })[]; diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index a98d51f56..172059d1c 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -149,9 +149,9 @@ @@ -162,18 +162,18 @@
- {{ element.SymbolProfile?.name }} + {{ element.assetProfile?.name }} @if (element.isDraft) { Draft }
@if ( - element.SymbolProfile?.dataSource !== 'MANUAL' && - !isUUID(element.SymbolProfile?.symbol) + element.assetProfile?.dataSource !== 'MANUAL' && + !isUUID(element.assetProfile?.symbol) ) {
{{ - element.SymbolProfile?.symbol | gfSymbol + element.assetProfile?.symbol | gfSymbol }}
} @@ -309,7 +309,7 @@ class="d-none d-lg-table-cell px-1" mat-cell > - {{ element.currency ?? element.SymbolProfile?.currency }} + {{ element.currency ?? element.assetProfile?.currency }} diff --git a/libs/ui/src/lib/activities-table/activities-table.component.stories.ts b/libs/ui/src/lib/activities-table/activities-table.component.stories.ts index 25463e576..957de2262 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.stories.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.stories.ts @@ -56,7 +56,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'EQUITY', assetSubClass: 'ETF', comment: undefined, @@ -123,7 +123,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'EQUITY', assetSubClass: 'ETF', comment: undefined, @@ -190,7 +190,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'LIQUIDITY', assetSubClass: 'CRYPTOCURRENCY', comment: undefined, @@ -257,7 +257,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'FIXED_INCOME', assetSubClass: 'BOND', comment: 'No data', @@ -324,7 +324,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'EQUITY', assetSubClass: 'ETF', comment: undefined, diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index dd792ca35..b1bfcf686 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/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)) { this.activityClicked.emit({ - dataSource: activity.SymbolProfile.dataSource, - symbol: activity.SymbolProfile.symbol + dataSource: activity.assetProfile.dataSource, + symbol: activity.assetProfile.symbol }); } }