diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da5c3ddd..4fec3111c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved the dividend calculations into the portfolio calculator - Moved the fee calculations into the portfolio calculator - Moved the interest calculations into the portfolio calculator +- Moved the liability calculations into the portfolio calculator +- Moved the (wealth) item calculations into the portfolio calculator ## 2.72.0 - 2024-04-13 diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 712c0ac04..1d2eadfbf 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -140,7 +140,9 @@ export abstract class PortfolioCalculator { totalFeesWithCurrencyEffect: new Big(0), totalInterestWithCurrencyEffect: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalLiabilitiesWithCurrencyEffect: new Big(0), + totalValuablesWithCurrencyEffect: new Big(0) }; } @@ -149,6 +151,9 @@ export abstract class PortfolioCalculator { let dates: Date[] = []; let firstIndex = transactionPoints.length; let firstTransactionPoint: TransactionPoint = null; + let totalInterestWithCurrencyEffect = new Big(0); + let totalLiabilitiesWithCurrencyEffect = new Big(0); + let totalValuablesWithCurrencyEffect = new Big(0); dates.push(resetHours(start)); @@ -274,8 +279,11 @@ export abstract class PortfolioCalculator { timeWeightedInvestmentWithCurrencyEffect, totalDividend, totalDividendInBaseCurrency, + totalInterestInBaseCurrency, totalInvestment, - totalInvestmentWithCurrencyEffect + totalInvestmentWithCurrencyEffect, + totalLiabilitiesInBaseCurrency, + totalValuablesInBaseCurrency } = this.getSymbolMetrics({ marketSymbolMap, start, @@ -333,6 +341,17 @@ export abstract class PortfolioCalculator { ) }); + totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus( + totalInterestInBaseCurrency + ); + + totalLiabilitiesWithCurrencyEffect = + totalLiabilitiesWithCurrencyEffect.plus(totalLiabilitiesInBaseCurrency); + + totalValuablesWithCurrencyEffect = totalValuablesWithCurrencyEffect.plus( + totalValuablesInBaseCurrency + ); + if ( (hasErrors || currentRateErrors.find(({ dataSource, symbol }) => { @@ -350,8 +369,10 @@ export abstract class PortfolioCalculator { ...overall, errors, positions, - hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors, - totalInterestWithCurrencyEffect: lastTransactionPoint.interest + totalInterestWithCurrencyEffect, + totalLiabilitiesWithCurrencyEffect, + totalValuablesWithCurrencyEffect, + hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors }; } @@ -715,6 +736,12 @@ export abstract class PortfolioCalculator { })); } + public async getLiabilitiesInBaseCurrency() { + await this.snapshotPromise; + + return this.snapshot.totalLiabilitiesWithCurrencyEffect; + } + public async getSnapshot() { await this.snapshotPromise; @@ -751,6 +778,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 } = {}; @@ -767,13 +800,6 @@ export abstract class PortfolioCalculator { type, unitPrice } of this.orders) { - if ( - // TODO - ['ITEM', 'LIABILITY'].includes(type) - ) { - continue; - } - let currentTransactionPointItem: TransactionPointSymbol; const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; @@ -858,11 +884,25 @@ export abstract class PortfolioCalculator { interest = quantity.mul(unitPrice); } + let liabilities = new Big(0); + + if (type === 'LIABILITY') { + liabilities = quantity.mul(unitPrice); + } + + let valuables = new Big(0); + + if (type === 'ITEM') { + valuables = quantity.mul(unitPrice); + } + if (lastDate !== date || lastTransactionPoint === null) { lastTransactionPoint = { date, fees, interest, + liabilities, + valuables, items: newItems }; @@ -872,6 +912,10 @@ export abstract class PortfolioCalculator { lastTransactionPoint.interest = lastTransactionPoint.interest.plus(interest); lastTransactionPoint.items = newItems; + lastTransactionPoint.liabilities = + lastTransactionPoint.liabilities.plus(liabilities); + 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..a11ae8896 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,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('3.2'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: 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..8d93d8b97 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,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('3.2'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: 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..f26331134 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,9 @@ 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'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + 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..2a9ba0916 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,9 @@ 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'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + 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..83f99e3cb 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,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('49'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: 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..0642b28ed 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,9 @@ 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'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + 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..b8ef6954e --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts @@ -0,0 +1,134 @@ +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'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts new file mode 100644 index 000000000..9ef369c8f --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts @@ -0,0 +1,134 @@ +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 liability 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: 'Loan', + symbol: '55196015-1365-4560-aa60-8751ae6d18f8' + }, + type: 'LIABILITY', + unitPrice: 3000 + } + ]; + + 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('3000'), + 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: 3000, + netPerformance: null, + netPerformancePercentage: null, + netPerformancePercentageWithCurrencyEffect: null, + netPerformanceWithCurrencyEffect: null, + quantity: new Big('0'), + symbol: '55196015-1365-4560-aa60-8751ae6d18f8', + 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'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts index 6dc489c5d..e50ce4194 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -133,7 +133,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('19'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('298.58'), - totalInvestmentWithCurrencyEffect: new Big('298.58') + totalInvestmentWithCurrencyEffect: new Big('298.58'), + totalLiabilitiesWithCurrencyEffect: 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..1d69abfbf 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,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0), + totalLiabilitiesWithCurrencyEffect: 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..3d63f1a5d 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,9 @@ 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'), + totalLiabilitiesWithCurrencyEffect: 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.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index f1bf56f11..6f0b03800 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,9 @@ describe('PortfolioCalculator', () => { totalFeesWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'), totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0') + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: 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..7dcef89cb 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -109,6 +109,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { hasErrors, netPerformance, netPerformanceWithCurrencyEffect, + positions, totalFeesWithCurrencyEffect, totalInterestWithCurrencyEffect, totalInvestment, @@ -131,7 +132,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { : grossPerformanceWithCurrencyEffect.div( totalTimeWeightedInvestmentWithCurrencyEffect ), - positions + totalLiabilitiesWithCurrencyEffect: new Big(0), + totalValuablesWithCurrencyEffect: new Big(0) }; } @@ -194,8 +196,12 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let totalInvestmentFromBuyTransactions = new Big(0); let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); let totalInvestmentWithCurrencyEffect = new Big(0); + let totalLiabilities = new Big(0); + let totalLiabilitiesInBaseCurrency = 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 +242,11 @@ 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), + totalLiabilities: new Big(0), + totalLiabilitiesInBaseCurrency: new Big(0), + totalValuables: new Big(0), + totalValuablesInBaseCurrency: new Big(0) }; } @@ -281,7 +291,11 @@ 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), + totalLiabilities: new Big(0), + totalLiabilitiesInBaseCurrency: new Big(0), + totalValuables: new Big(0), + totalValuablesInBaseCurrency: new Big(0) }; } @@ -536,6 +550,20 @@ 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) + ); + } else if (order.type === 'LIABILITY') { + const liabilities = order.quantity.mul(order.unitPrice); + + totalLiabilities = totalLiabilities.plus(liabilities); + totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( + liabilities.mul(exchangeRateAtOrderDate ?? 1) + ); } const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); @@ -853,6 +881,10 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { totalInterestInBaseCurrency, totalInvestment, totalInvestmentWithCurrencyEffect, + totalLiabilities, + totalLiabilitiesInBaseCurrency, + 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..d89734987 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,6 @@ export interface PortfolioSnapshot extends ResponseError { totalInterestWithCurrencyEffect: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; + totalLiabilitiesWithCurrencyEffect: 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..fcbea81ca 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,6 @@ export interface TransactionPoint { fees: Big; interest: Big; items: TransactionPointSymbol[]; + liabilities: Big; + valuables: Big; } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 7ee92e91c..56c0a231c 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -78,10 +78,8 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string, - @Query('withLiabilities') withLiabilitiesParam = 'false', @Query('withMarkets') withMarketsParam = 'false' ): Promise { - const withLiabilities = withLiabilitiesParam === 'true'; const withMarkets = withMarketsParam === 'true'; let hasDetails = true; @@ -107,8 +105,6 @@ export class PortfolioController { dateRange, filters, impersonationId, - // TODO - // withLiabilities, withMarkets, userId: this.request.user.id, withSummary: true diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 8714a15ec..95a68eaae 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -60,7 +60,6 @@ import { Prisma } from '@prisma/client'; import { Big } from 'big.js'; -import { isUUID } from 'class-validator'; import { differenceInDays, format, @@ -324,7 +323,6 @@ export class PortfolioService { impersonationId, userId, withExcludedAccounts = false, - withLiabilities = false, withMarkets = false, withSummary = false }: { @@ -333,7 +331,6 @@ export class PortfolioService { impersonationId: string; userId: string; withExcludedAccounts?: boolean; - withLiabilities?: boolean; withMarkets?: boolean; withSummary?: boolean; }): Promise { @@ -1623,35 +1620,10 @@ 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(); - - // TODO: Move to portfolio calculator - const liabilities = 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 liabilities = + await portfolioCalculator.getLiabilitiesInBaseCurrency(); + + const valuables = await portfolioCalculator.getValuablesInBaseCurrency(); const totalBuy = this.getSumOfActivityType({ userCurrency, @@ -1701,7 +1673,7 @@ export class PortfolioService { const netWorth = new Big(balanceInBaseCurrency) .plus(performanceInformation.performance.currentValue) - .plus(items) + .plus(valuables) .plus(excludedAccountsAndActivities) .minus(liabilities) .toNumber(); @@ -1730,8 +1702,6 @@ export class PortfolioService { cash, excludedAccountsAndActivities, firstOrderDate, - items, - liabilities, totalBuy, totalSell, committedFunds: committedFunds.toNumber(), @@ -1752,6 +1722,8 @@ export class PortfolioService { .minus(emergencyFundPositionsValueInBaseCurrency) .toNumber(), interest: interest.toNumber(), + items: valuables.toNumber(), + liabilities: liabilities.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..21b111395 100644 --- a/apps/api/src/helper/portfolio.helper.ts +++ b/apps/api/src/helper/portfolio.helper.ts @@ -18,10 +18,8 @@ export function getFactor(activityType: ActivityType) { switch (activityType) { case 'BUY': - case 'ITEM': factor = 1; break; - case 'LIABILITY': case 'SELL': factor = -1; break; diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts index bb68a4627..b67b67ce5 100644 --- a/apps/client/src/app/components/home-summary/home-summary.component.ts +++ b/apps/client/src/app/components/home-summary/home-summary.component.ts @@ -102,7 +102,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { this.isLoading = true; this.dataService - .fetchPortfolioDetails({ withLiabilities: true }) + .fetchPortfolioDetails() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ summary }) => { this.summary = summary; diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index aeeb2f07b..8a3f7d293 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -411,19 +411,13 @@ export class DataService { public fetchPortfolioDetails({ filters, - withLiabilities = false, withMarkets = false }: { filters?: Filter[]; - withLiabilities?: boolean; withMarkets?: boolean; } = {}): Observable { let params = this.buildFiltersAsQueryParams({ filters }); - if (withLiabilities) { - params = params.append('withLiabilities', withLiabilities); - } - if (withMarkets) { params = params.append('withMarkets', withMarkets); } diff --git a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts index 99a1b3467..57eed9212 100644 --- a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts +++ b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts @@ -46,4 +46,8 @@ export interface SymbolMetrics { totalInterestInBaseCurrency: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; + totalLiabilities: Big; + totalLiabilitiesInBaseCurrency: Big; + totalValuables: Big; + totalValuablesInBaseCurrency: Big; }