diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 712c0ac04..94eafa518 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -140,7 +140,8 @@ export abstract class PortfolioCalculator { totalFeesWithCurrencyEffect: new Big(0), totalInterestWithCurrencyEffect: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalValuablesWithCurrencyEffect: new Big(0) }; } @@ -149,6 +150,8 @@ export abstract class PortfolioCalculator { let dates: Date[] = []; let firstIndex = transactionPoints.length; let firstTransactionPoint: TransactionPoint = null; + let totalInterestWithCurrencyEffect = new Big(0); + let totalValuablesWithCurrencyEffect = new Big(0); dates.push(resetHours(start)); @@ -274,8 +277,10 @@ export abstract class PortfolioCalculator { timeWeightedInvestmentWithCurrencyEffect, totalDividend, totalDividendInBaseCurrency, + totalInterestInBaseCurrency, totalInvestment, - totalInvestmentWithCurrencyEffect + totalInvestmentWithCurrencyEffect, + totalValuablesInBaseCurrency } = this.getSymbolMetrics({ marketSymbolMap, start, @@ -333,6 +338,14 @@ export abstract class PortfolioCalculator { ) }); + totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus( + totalInterestInBaseCurrency + ); + + totalValuablesWithCurrencyEffect = totalValuablesWithCurrencyEffect.plus( + totalValuablesInBaseCurrency + ); + if ( (hasErrors || currentRateErrors.find(({ dataSource, symbol }) => { @@ -350,8 +363,9 @@ export abstract class PortfolioCalculator { ...overall, errors, positions, - hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors, - totalInterestWithCurrencyEffect: lastTransactionPoint.interest + totalInterestWithCurrencyEffect, + totalValuablesWithCurrencyEffect, + hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors }; } @@ -751,6 +765,12 @@ export abstract class PortfolioCalculator { return this.transactionPoints; } + public async getValuablesInBaseCurrency() { + await this.snapshotPromise; + + return this.snapshot.totalValuablesWithCurrencyEffect; + } + private computeTransactionPoints() { this.transactionPoints = []; const symbols: { [symbol: string]: TransactionPointSymbol } = {}; @@ -769,7 +789,7 @@ export abstract class PortfolioCalculator { } of this.orders) { if ( // TODO - ['ITEM', 'LIABILITY'].includes(type) + ['LIABILITY'].includes(type) ) { continue; } @@ -858,11 +878,18 @@ export abstract class PortfolioCalculator { interest = quantity.mul(unitPrice); } + let valuables = new Big(0); + + if (type === 'ITEM') { + valuables = quantity.mul(unitPrice); + } + if (lastDate !== date || lastTransactionPoint === null) { lastTransactionPoint = { date, fees, interest, + valuables, items: newItems }; @@ -872,6 +899,8 @@ export abstract class PortfolioCalculator { lastTransactionPoint.interest = lastTransactionPoint.interest.plus(interest); lastTransactionPoint.items = newItems; + lastTransactionPoint.valuables = + lastTransactionPoint.valuables.plus(valuables); } lastDate = date; diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index 8ddae9df6..b118644ca 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -176,7 +176,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('3.2'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts index febd1769d..edf451487 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -159,7 +159,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('3.2'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts index 2b9fd06f0..1006deb79 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts @@ -144,7 +144,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('1.55'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('273.2'), - totalInvestmentWithCurrencyEffect: new Big('273.2') + totalInvestmentWithCurrencyEffect: new Big('273.2'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index ceff92449..5b6c9e358 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -178,7 +178,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('320.43'), - totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957') + totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts index b689a0c30..ccaa9a46e 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts @@ -125,7 +125,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('49'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts index 911167f7a..ffb4a6989 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts @@ -157,7 +157,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('1'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('89.12'), - totalInvestmentWithCurrencyEffect: new Big('82.329056') + totalInvestmentWithCurrencyEffect: new Big('82.329056'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts new file mode 100644 index 000000000..8c1d60495 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts @@ -0,0 +1,133 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + symbolProfileDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PortfolioCalculatorFactory, + PerformanceCalculationType +} 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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { parseDate } from '@ghostfolio/common/helper'; + +import { Big } from 'big.js'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let factory: PortfolioCalculatorFactory; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + factory = new PortfolioCalculatorFactory( + currentRateService, + exchangeRateDataService + ); + }); + + describe('compute portfolio snapshot', () => { + it.only('with item activity', async () => { + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2022-01-31').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2022-01-01'), + fee: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'MANUAL', + name: 'Penthouse Apartment', + symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde' + }, + type: 'ITEM', + unitPrice: 500000 + } + ]; + + const portfolioCalculator = factory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, + currency: 'USD' + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot( + parseDate('2022-01-01') + ); + + spy.mockRestore(); + + expect(portfolioSnapshot).toEqual({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + grossPerformance: new Big('0'), + grossPerformancePercentage: new Big('0'), + grossPerformancePercentageWithCurrencyEffect: new Big('0'), + grossPerformanceWithCurrencyEffect: new Big('0'), + hasErrors: true, + netPerformance: new Big('0'), + netPerformancePercentage: new Big('0'), + netPerformancePercentageWithCurrencyEffect: new Big('0'), + netPerformanceWithCurrencyEffect: new Big('0'), + positions: [ + { + averagePrice: new Big('500000'), + currency: 'USD', + dataSource: 'MANUAL', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('0'), + firstBuyDate: '2022-01-01', + grossPerformance: null, + grossPerformancePercentage: null, + grossPerformancePercentageWithCurrencyEffect: null, + grossPerformanceWithCurrencyEffect: null, + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + marketPrice: null, + marketPriceInBaseCurrency: 500000, + netPerformance: null, + netPerformancePercentage: null, + netPerformancePercentageWithCurrencyEffect: null, + netPerformanceWithCurrencyEffect: null, + quantity: new Big('0'), + symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde', + tags: [], + timeWeightedInvestment: new Big('0'), + timeWeightedInvestmentWithCurrencyEffect: new Big('0'), + transactionCount: 1, + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts index ece39c87b..075006113 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts @@ -83,7 +83,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([]); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index a3c12829a..870755d47 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -161,7 +161,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('4.25'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('75.80'), - totalInvestmentWithCurrencyEffect: new Big('75.80') + totalInvestmentWithCurrencyEffect: new Big('75.80'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index f1bf56f11..08b16325d 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -185,7 +185,8 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') }); expect(investments).toEqual([ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index b9b7fd900..1c23ef016 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -36,6 +36,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let totalInvestmentWithCurrencyEffect = new Big(0); let totalTimeWeightedInvestment = new Big(0); let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); + let totalValuablesWithCurrencyEffect = new Big(0); for (const currentPosition of positions) { if (currentPosition.fee) { @@ -113,6 +114,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterestWithCurrencyEffect, totalInvestment, totalInvestmentWithCurrencyEffect, + totalValuablesWithCurrencyEffect, netPerformancePercentage: totalTimeWeightedInvestment.eq(0) ? new Big(0) : netPerformance.div(totalTimeWeightedInvestment), @@ -196,6 +198,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let totalInvestmentWithCurrencyEffect = new Big(0); let totalQuantityFromBuyTransactions = new Big(0); let totalUnits = new Big(0); + let totalValuables = new Big(0); + let totalValuablesInBaseCurrency = new Big(0); let valueAtStartDate: Big; let valueAtStartDateWithCurrencyEffect: Big; @@ -236,7 +240,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterest: new Big(0), totalInterestInBaseCurrency: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalValuables: new Big(0), + totalValuablesInBaseCurrency: new Big(0) }; } @@ -281,7 +287,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterest: new Big(0), totalInterestInBaseCurrency: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalValuables: new Big(0), + totalValuablesInBaseCurrency: new Big(0) }; } @@ -536,6 +544,13 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( interest.mul(exchangeRateAtOrderDate ?? 1) ); + } else if (order.type === 'ITEM') { + const valuables = order.quantity.mul(order.unitPrice); + + totalValuables = totalValuables.plus(valuables); + totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus( + valuables.mul(exchangeRateAtOrderDate ?? 1) + ); } const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); @@ -853,6 +868,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterestInBaseCurrency, totalInvestment, totalInvestmentWithCurrencyEffect, + totalValuables, + totalValuablesInBaseCurrency, grossPerformance: totalGrossPerformance, grossPerformanceWithCurrencyEffect: totalGrossPerformanceWithCurrencyEffect, diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts index b8cc904fa..804ef1194 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts @@ -19,4 +19,5 @@ export interface PortfolioSnapshot extends ResponseError { totalInterestWithCurrencyEffect: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; + totalValuablesWithCurrencyEffect: Big; } diff --git a/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts b/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts index 2f5218405..f501b7203 100644 --- a/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts @@ -7,4 +7,5 @@ export interface TransactionPoint { fees: Big; interest: Big; items: TransactionPointSymbol[]; + valuables: Big; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 8714a15ec..7b4c327d0 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1623,20 +1623,7 @@ export class PortfolioService { const interest = await portfolioCalculator.getInterestInBaseCurrency(); - // TODO: Move to portfolio calculator - const items = getSum( - Object.keys(holdings) - .filter((symbol) => { - return ( - isUUID(symbol) && - holdings[symbol].dataSource === 'MANUAL' && - holdings[symbol].valueInBaseCurrency > 0 - ); - }) - .map((symbol) => { - return new Big(holdings[symbol].valueInBaseCurrency).abs(); - }) - ).toNumber(); + const valuables = await portfolioCalculator.getValuablesInBaseCurrency(); // TODO: Move to portfolio calculator const liabilities = getSum( @@ -1701,7 +1688,7 @@ export class PortfolioService { const netWorth = new Big(balanceInBaseCurrency) .plus(performanceInformation.performance.currentValue) - .plus(items) + .plus(valuables) .plus(excludedAccountsAndActivities) .minus(liabilities) .toNumber(); @@ -1730,7 +1717,6 @@ export class PortfolioService { cash, excludedAccountsAndActivities, firstOrderDate, - items, liabilities, totalBuy, totalSell, @@ -1752,6 +1738,7 @@ export class PortfolioService { .minus(emergencyFundPositionsValueInBaseCurrency) .toNumber(), interest: interest.toNumber(), + items: valuables.toNumber(), ordersCount: activities.filter(({ type }) => { return type === 'BUY' || type === 'SELL'; }).length, diff --git a/apps/api/src/helper/portfolio.helper.ts b/apps/api/src/helper/portfolio.helper.ts index f762b2ad5..ea7eef098 100644 --- a/apps/api/src/helper/portfolio.helper.ts +++ b/apps/api/src/helper/portfolio.helper.ts @@ -18,7 +18,6 @@ export function getFactor(activityType: ActivityType) { switch (activityType) { case 'BUY': - case 'ITEM': factor = 1; break; case 'LIABILITY': diff --git a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts index 99a1b3467..fa2d1256a 100644 --- a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts +++ b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts @@ -46,4 +46,6 @@ export interface SymbolMetrics { totalInterestInBaseCurrency: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; + totalValuables: Big; + totalValuablesInBaseCurrency: Big; }