Feature/improve chart in account detail dialog (#3314)
* Improve net worth calculation in portfolio performance chart * Improve account balance management * Update changelog
This commit is contained in:
parent
ab59eb5c92
commit
39bd4a349b
@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the chart in the account detail dialog
|
||||||
|
- Improved the account balance management
|
||||||
|
|
||||||
## 2.75.0 - 2024-04-21
|
## 2.75.0 - 2024-04-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import { Filter } from '@ghostfolio/common/interfaces';
|
import { Filter } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
import { Account, Order, Platform, Prisma } from '@prisma/client';
|
||||||
import { Big } from 'big.js';
|
import { Big } from 'big.js';
|
||||||
import { parseISO } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { groupBy } from 'lodash';
|
import { groupBy } from 'lodash';
|
||||||
|
|
||||||
import { CashDetails } from './interfaces/cash-details.interface';
|
import { CashDetails } from './interfaces/cash-details.interface';
|
||||||
@ -86,15 +87,11 @@ export class AccountService {
|
|||||||
data
|
data
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.prismaService.accountBalance.create({
|
await this.accountBalanceService.createOrUpdateAccountBalance({
|
||||||
data: {
|
accountId: account.id,
|
||||||
Account: {
|
balance: data.balance,
|
||||||
connect: {
|
date: format(new Date(), DATE_FORMAT),
|
||||||
id_userId: { id: account.id, userId: aUserId }
|
userId: aUserId
|
||||||
}
|
|
||||||
},
|
|
||||||
value: data.balance
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
@ -197,15 +194,11 @@ export class AccountService {
|
|||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
const { data, where } = params;
|
const { data, where } = params;
|
||||||
|
|
||||||
await this.prismaService.accountBalance.create({
|
await this.accountBalanceService.createOrUpdateAccountBalance({
|
||||||
data: {
|
accountId: <string>data.id,
|
||||||
Account: {
|
balance: <number>data.balance,
|
||||||
connect: {
|
date: format(new Date(), DATE_FORMAT),
|
||||||
id_userId: where.id_userId
|
userId: aUserId
|
||||||
}
|
|
||||||
},
|
|
||||||
value: <number>data.balance
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.prismaService.account.update({
|
return this.prismaService.account.update({
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
|
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -22,11 +23,13 @@ export class PortfolioCalculatorFactory {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public createCalculator({
|
public createCalculator({
|
||||||
|
accountBalanceItems = [],
|
||||||
activities,
|
activities,
|
||||||
calculationType,
|
calculationType,
|
||||||
currency,
|
currency,
|
||||||
dateRange = 'max'
|
dateRange = 'max'
|
||||||
}: {
|
}: {
|
||||||
|
accountBalanceItems?: HistoricalDataItem[];
|
||||||
activities: Activity[];
|
activities: Activity[];
|
||||||
calculationType: PerformanceCalculationType;
|
calculationType: PerformanceCalculationType;
|
||||||
currency: string;
|
currency: string;
|
||||||
@ -35,6 +38,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
switch (calculationType) {
|
switch (calculationType) {
|
||||||
case PerformanceCalculationType.MWR:
|
case PerformanceCalculationType.MWR:
|
||||||
return new MWRPortfolioCalculator({
|
return new MWRPortfolioCalculator({
|
||||||
|
accountBalanceItems,
|
||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
dateRange,
|
dateRange,
|
||||||
@ -43,6 +47,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
});
|
});
|
||||||
case PerformanceCalculationType.TWR:
|
case PerformanceCalculationType.TWR:
|
||||||
return new TWRPortfolioCalculator({
|
return new TWRPortfolioCalculator({
|
||||||
|
accountBalanceItems,
|
||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
|
@ -37,13 +37,15 @@ import {
|
|||||||
isBefore,
|
isBefore,
|
||||||
isSameDay,
|
isSameDay,
|
||||||
max,
|
max,
|
||||||
|
min,
|
||||||
subDays
|
subDays
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { last, uniq, uniqBy } from 'lodash';
|
import { first, last, uniq, uniqBy } from 'lodash';
|
||||||
|
|
||||||
export abstract class PortfolioCalculator {
|
export abstract class PortfolioCalculator {
|
||||||
protected static readonly ENABLE_LOGGING = false;
|
protected static readonly ENABLE_LOGGING = false;
|
||||||
|
|
||||||
|
protected accountBalanceItems: HistoricalDataItem[];
|
||||||
protected orders: PortfolioOrder[];
|
protected orders: PortfolioOrder[];
|
||||||
|
|
||||||
private currency: string;
|
private currency: string;
|
||||||
@ -57,18 +59,21 @@ export abstract class PortfolioCalculator {
|
|||||||
private transactionPoints: TransactionPoint[];
|
private transactionPoints: TransactionPoint[];
|
||||||
|
|
||||||
public constructor({
|
public constructor({
|
||||||
|
accountBalanceItems,
|
||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
dateRange,
|
dateRange,
|
||||||
exchangeRateDataService
|
exchangeRateDataService
|
||||||
}: {
|
}: {
|
||||||
|
accountBalanceItems: HistoricalDataItem[];
|
||||||
activities: Activity[];
|
activities: Activity[];
|
||||||
currency: string;
|
currency: string;
|
||||||
currentRateService: CurrentRateService;
|
currentRateService: CurrentRateService;
|
||||||
dateRange: DateRange;
|
dateRange: DateRange;
|
||||||
exchangeRateDataService: ExchangeRateDataService;
|
exchangeRateDataService: ExchangeRateDataService;
|
||||||
}) {
|
}) {
|
||||||
|
this.accountBalanceItems = accountBalanceItems;
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.currentRateService = currentRateService;
|
this.currentRateService = currentRateService;
|
||||||
this.exchangeRateDataService = exchangeRateDataService;
|
this.exchangeRateDataService = exchangeRateDataService;
|
||||||
@ -383,10 +388,6 @@ export abstract class PortfolioCalculator {
|
|||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
withDataDecimation?: boolean;
|
withDataDecimation?: boolean;
|
||||||
}): Promise<HistoricalDataItem[]> {
|
}): Promise<HistoricalDataItem[]> {
|
||||||
if (this.getTransactionPoints().length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange, this.getStartDate());
|
const { endDate, startDate } = getInterval(dateRange, this.getStartDate());
|
||||||
|
|
||||||
const daysInMarket = differenceInDays(endDate, startDate) + 1;
|
const daysInMarket = differenceInDays(endDate, startDate) + 1;
|
||||||
@ -485,6 +486,7 @@ export abstract class PortfolioCalculator {
|
|||||||
investmentValueWithCurrencyEffect: Big;
|
investmentValueWithCurrencyEffect: Big;
|
||||||
totalCurrentValue: Big;
|
totalCurrentValue: Big;
|
||||||
totalCurrentValueWithCurrencyEffect: Big;
|
totalCurrentValueWithCurrencyEffect: Big;
|
||||||
|
totalAccountBalanceWithCurrencyEffect: Big;
|
||||||
totalInvestmentValue: Big;
|
totalInvestmentValue: Big;
|
||||||
totalInvestmentValueWithCurrencyEffect: Big;
|
totalInvestmentValueWithCurrencyEffect: Big;
|
||||||
totalNetPerformanceValue: Big;
|
totalNetPerformanceValue: Big;
|
||||||
@ -544,9 +546,24 @@ export abstract class PortfolioCalculator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastDate = format(this.startDate, DATE_FORMAT);
|
||||||
|
|
||||||
for (const currentDate of dates) {
|
for (const currentDate of dates) {
|
||||||
const dateString = format(currentDate, DATE_FORMAT);
|
const dateString = format(currentDate, DATE_FORMAT);
|
||||||
|
|
||||||
|
accumulatedValuesByDate[dateString] = {
|
||||||
|
investmentValueWithCurrencyEffect: new Big(0),
|
||||||
|
totalAccountBalanceWithCurrencyEffect: new Big(0),
|
||||||
|
totalCurrentValue: new Big(0),
|
||||||
|
totalCurrentValueWithCurrencyEffect: new Big(0),
|
||||||
|
totalInvestmentValue: new Big(0),
|
||||||
|
totalInvestmentValueWithCurrencyEffect: new Big(0),
|
||||||
|
totalNetPerformanceValue: new Big(0),
|
||||||
|
totalNetPerformanceValueWithCurrencyEffect: new Big(0),
|
||||||
|
totalTimeWeightedInvestmentValue: new Big(0),
|
||||||
|
totalTimeWeightedInvestmentValueWithCurrencyEffect: new Big(0)
|
||||||
|
};
|
||||||
|
|
||||||
for (const symbol of Object.keys(valuesBySymbol)) {
|
for (const symbol of Object.keys(valuesBySymbol)) {
|
||||||
const symbolValues = valuesBySymbol[symbol];
|
const symbolValues = valuesBySymbol[symbol];
|
||||||
|
|
||||||
@ -584,49 +601,94 @@ export abstract class PortfolioCalculator {
|
|||||||
dateString
|
dateString
|
||||||
] ?? new Big(0);
|
] ?? new Big(0);
|
||||||
|
|
||||||
accumulatedValuesByDate[dateString] = {
|
accumulatedValuesByDate[dateString].investmentValueWithCurrencyEffect =
|
||||||
investmentValueWithCurrencyEffect: (
|
accumulatedValuesByDate[
|
||||||
accumulatedValuesByDate[dateString]
|
dateString
|
||||||
?.investmentValueWithCurrencyEffect ?? new Big(0)
|
].investmentValueWithCurrencyEffect.add(
|
||||||
).add(investmentValueWithCurrencyEffect),
|
investmentValueWithCurrencyEffect
|
||||||
totalCurrentValue: (
|
);
|
||||||
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
|
|
||||||
).add(currentValue),
|
accumulatedValuesByDate[dateString].totalCurrentValue =
|
||||||
totalCurrentValueWithCurrencyEffect: (
|
accumulatedValuesByDate[dateString].totalCurrentValue.add(
|
||||||
accumulatedValuesByDate[dateString]
|
currentValue
|
||||||
?.totalCurrentValueWithCurrencyEffect ?? new Big(0)
|
);
|
||||||
).add(currentValueWithCurrencyEffect),
|
|
||||||
totalInvestmentValue: (
|
accumulatedValuesByDate[
|
||||||
accumulatedValuesByDate[dateString]?.totalInvestmentValue ??
|
dateString
|
||||||
new Big(0)
|
].totalCurrentValueWithCurrencyEffect = accumulatedValuesByDate[
|
||||||
).add(investmentValueAccumulated),
|
dateString
|
||||||
totalInvestmentValueWithCurrencyEffect: (
|
].totalCurrentValueWithCurrencyEffect.add(
|
||||||
accumulatedValuesByDate[dateString]
|
currentValueWithCurrencyEffect
|
||||||
?.totalInvestmentValueWithCurrencyEffect ?? new Big(0)
|
);
|
||||||
).add(investmentValueAccumulatedWithCurrencyEffect),
|
|
||||||
totalNetPerformanceValue: (
|
accumulatedValuesByDate[dateString].totalInvestmentValue =
|
||||||
accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ??
|
accumulatedValuesByDate[dateString].totalInvestmentValue.add(
|
||||||
new Big(0)
|
investmentValueAccumulated
|
||||||
).add(netPerformanceValue),
|
);
|
||||||
totalNetPerformanceValueWithCurrencyEffect: (
|
|
||||||
accumulatedValuesByDate[dateString]
|
accumulatedValuesByDate[
|
||||||
?.totalNetPerformanceValueWithCurrencyEffect ?? new Big(0)
|
dateString
|
||||||
).add(netPerformanceValueWithCurrencyEffect),
|
].totalInvestmentValueWithCurrencyEffect = accumulatedValuesByDate[
|
||||||
totalTimeWeightedInvestmentValue: (
|
dateString
|
||||||
accumulatedValuesByDate[dateString]
|
].totalInvestmentValueWithCurrencyEffect.add(
|
||||||
?.totalTimeWeightedInvestmentValue ?? new Big(0)
|
investmentValueAccumulatedWithCurrencyEffect
|
||||||
).add(timeWeightedInvestmentValue),
|
);
|
||||||
totalTimeWeightedInvestmentValueWithCurrencyEffect: (
|
|
||||||
accumulatedValuesByDate[dateString]
|
accumulatedValuesByDate[dateString].totalNetPerformanceValue =
|
||||||
?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0)
|
accumulatedValuesByDate[dateString].totalNetPerformanceValue.add(
|
||||||
).add(timeWeightedInvestmentValueWithCurrencyEffect)
|
netPerformanceValue
|
||||||
};
|
);
|
||||||
|
|
||||||
|
accumulatedValuesByDate[
|
||||||
|
dateString
|
||||||
|
].totalNetPerformanceValueWithCurrencyEffect = accumulatedValuesByDate[
|
||||||
|
dateString
|
||||||
|
].totalNetPerformanceValueWithCurrencyEffect.add(
|
||||||
|
netPerformanceValueWithCurrencyEffect
|
||||||
|
);
|
||||||
|
|
||||||
|
accumulatedValuesByDate[dateString].totalTimeWeightedInvestmentValue =
|
||||||
|
accumulatedValuesByDate[
|
||||||
|
dateString
|
||||||
|
].totalTimeWeightedInvestmentValue.add(timeWeightedInvestmentValue);
|
||||||
|
|
||||||
|
accumulatedValuesByDate[
|
||||||
|
dateString
|
||||||
|
].totalTimeWeightedInvestmentValueWithCurrencyEffect =
|
||||||
|
accumulatedValuesByDate[
|
||||||
|
dateString
|
||||||
|
].totalTimeWeightedInvestmentValueWithCurrencyEffect.add(
|
||||||
|
timeWeightedInvestmentValueWithCurrencyEffect
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.accountBalanceItems.some(({ date }) => {
|
||||||
|
return date === dateString;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
accumulatedValuesByDate[
|
||||||
|
dateString
|
||||||
|
].totalAccountBalanceWithCurrencyEffect = new Big(
|
||||||
|
this.accountBalanceItems.find(({ date }) => {
|
||||||
|
return date === dateString;
|
||||||
|
}).value
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
accumulatedValuesByDate[
|
||||||
|
dateString
|
||||||
|
].totalAccountBalanceWithCurrencyEffect =
|
||||||
|
accumulatedValuesByDate[lastDate]
|
||||||
|
?.totalAccountBalanceWithCurrencyEffect ?? new Big(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDate = dateString;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
|
return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
|
||||||
const {
|
const {
|
||||||
investmentValueWithCurrencyEffect,
|
investmentValueWithCurrencyEffect,
|
||||||
|
totalAccountBalanceWithCurrencyEffect,
|
||||||
totalCurrentValue,
|
totalCurrentValue,
|
||||||
totalCurrentValueWithCurrencyEffect,
|
totalCurrentValueWithCurrencyEffect,
|
||||||
totalInvestmentValue,
|
totalInvestmentValue,
|
||||||
@ -661,6 +723,11 @@ export abstract class PortfolioCalculator {
|
|||||||
netPerformance: totalNetPerformanceValue.toNumber(),
|
netPerformance: totalNetPerformanceValue.toNumber(),
|
||||||
netPerformanceWithCurrencyEffect:
|
netPerformanceWithCurrencyEffect:
|
||||||
totalNetPerformanceValueWithCurrencyEffect.toNumber(),
|
totalNetPerformanceValueWithCurrencyEffect.toNumber(),
|
||||||
|
// TODO: Add valuables
|
||||||
|
netWorth: totalCurrentValueWithCurrencyEffect
|
||||||
|
.plus(totalAccountBalanceWithCurrencyEffect)
|
||||||
|
.toNumber(),
|
||||||
|
totalAccountBalance: totalAccountBalanceWithCurrencyEffect.toNumber(),
|
||||||
totalInvestment: totalInvestmentValue.toNumber(),
|
totalInvestment: totalInvestmentValue.toNumber(),
|
||||||
totalInvestmentValueWithCurrencyEffect:
|
totalInvestmentValueWithCurrencyEffect:
|
||||||
totalInvestmentValueWithCurrencyEffect.toNumber(),
|
totalInvestmentValueWithCurrencyEffect.toNumber(),
|
||||||
@ -749,9 +816,30 @@ export abstract class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getStartDate() {
|
public getStartDate() {
|
||||||
return this.transactionPoints.length > 0
|
let firstAccountBalanceDate: Date;
|
||||||
? parseDate(this.transactionPoints[0].date)
|
let firstActivityDate: Date;
|
||||||
: new Date();
|
|
||||||
|
try {
|
||||||
|
const firstAccountBalanceDateString = first(
|
||||||
|
this.accountBalanceItems
|
||||||
|
)?.date;
|
||||||
|
firstAccountBalanceDate = firstAccountBalanceDateString
|
||||||
|
? parseDate(firstAccountBalanceDateString)
|
||||||
|
: new Date();
|
||||||
|
} catch (error) {
|
||||||
|
firstAccountBalanceDate = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const firstActivityDateString = this.transactionPoints[0].date;
|
||||||
|
firstActivityDate = firstActivityDateString
|
||||||
|
? parseDate(firstActivityDateString)
|
||||||
|
: new Date();
|
||||||
|
} catch (error) {
|
||||||
|
firstActivityDate = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
return min([firstAccountBalanceDate, firstActivityDate]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract getSymbolMetrics({
|
protected abstract getSymbolMetrics({
|
||||||
|
@ -90,7 +90,12 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
expect(investments).toEqual([]);
|
expect(investments).toEqual([]);
|
||||||
|
|
||||||
expect(investmentsByMonth).toEqual([]);
|
expect(investmentsByMonth).toEqual([
|
||||||
|
{
|
||||||
|
date: '2021-12-01',
|
||||||
|
investment: 0
|
||||||
|
}
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -113,6 +113,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
netPerformanceInPercentage: 0,
|
netPerformanceInPercentage: 0,
|
||||||
netPerformanceInPercentageWithCurrencyEffect: 0,
|
netPerformanceInPercentageWithCurrencyEffect: 0,
|
||||||
netPerformanceWithCurrencyEffect: 0,
|
netPerformanceWithCurrencyEffect: 0,
|
||||||
|
netWorth: 151.6,
|
||||||
|
totalAccountBalance: 0,
|
||||||
totalInvestment: 151.6,
|
totalInvestment: 151.6,
|
||||||
totalInvestmentValueWithCurrencyEffect: 151.6,
|
totalInvestmentValueWithCurrencyEffect: 151.6,
|
||||||
value: 151.6,
|
value: 151.6,
|
||||||
@ -126,6 +128,8 @@ describe('PortfolioCalculator', () => {
|
|||||||
netPerformanceInPercentage: 13.100263852242744,
|
netPerformanceInPercentage: 13.100263852242744,
|
||||||
netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744,
|
netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744,
|
||||||
netPerformanceWithCurrencyEffect: 19.86,
|
netPerformanceWithCurrencyEffect: 19.86,
|
||||||
|
netWorth: 0,
|
||||||
|
totalAccountBalance: 0,
|
||||||
totalInvestment: 0,
|
totalInvestment: 0,
|
||||||
totalInvestmentValueWithCurrencyEffect: 0,
|
totalInvestmentValueWithCurrencyEffect: 0,
|
||||||
value: 0,
|
value: 0,
|
||||||
|
@ -188,6 +188,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
[date: string]: Big;
|
[date: string]: Big;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
let totalAccountBalanceInBaseCurrency = new Big(0);
|
||||||
let totalDividend = new Big(0);
|
let totalDividend = new Big(0);
|
||||||
let totalDividendInBaseCurrency = new Big(0);
|
let totalDividendInBaseCurrency = new Big(0);
|
||||||
let totalInterest = new Big(0);
|
let totalInterest = new Big(0);
|
||||||
@ -237,6 +238,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
timeWeightedInvestmentValues: {},
|
timeWeightedInvestmentValues: {},
|
||||||
timeWeightedInvestmentValuesWithCurrencyEffect: {},
|
timeWeightedInvestmentValuesWithCurrencyEffect: {},
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
||||||
|
totalAccountBalanceInBaseCurrency: new Big(0),
|
||||||
totalDividend: new Big(0),
|
totalDividend: new Big(0),
|
||||||
totalDividendInBaseCurrency: new Big(0),
|
totalDividendInBaseCurrency: new Big(0),
|
||||||
totalInterest: new Big(0),
|
totalInterest: new Big(0),
|
||||||
@ -286,6 +288,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
timeWeightedInvestmentValues: {},
|
timeWeightedInvestmentValues: {},
|
||||||
timeWeightedInvestmentValuesWithCurrencyEffect: {},
|
timeWeightedInvestmentValuesWithCurrencyEffect: {},
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
||||||
|
totalAccountBalanceInBaseCurrency: new Big(0),
|
||||||
totalDividend: new Big(0),
|
totalDividend: new Big(0),
|
||||||
totalDividendInBaseCurrency: new Big(0),
|
totalDividendInBaseCurrency: new Big(0),
|
||||||
totalInterest: new Big(0),
|
totalInterest: new Big(0),
|
||||||
@ -875,6 +878,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
netPerformanceValuesWithCurrencyEffect,
|
netPerformanceValuesWithCurrencyEffect,
|
||||||
timeWeightedInvestmentValues,
|
timeWeightedInvestmentValues,
|
||||||
timeWeightedInvestmentValuesWithCurrencyEffect,
|
timeWeightedInvestmentValuesWithCurrencyEffect,
|
||||||
|
totalAccountBalanceInBaseCurrency,
|
||||||
totalDividend,
|
totalDividend,
|
||||||
totalDividendInBaseCurrency,
|
totalDividendInBaseCurrency,
|
||||||
totalInterest,
|
totalInterest,
|
||||||
|
@ -64,7 +64,6 @@ import {
|
|||||||
differenceInDays,
|
differenceInDays,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
isBefore,
|
|
||||||
isSameMonth,
|
isSameMonth,
|
||||||
isSameYear,
|
isSameYear,
|
||||||
parseISO,
|
parseISO,
|
||||||
@ -1056,11 +1055,16 @@ export class PortfolioService {
|
|||||||
) => {
|
) => {
|
||||||
const formattedDate = format(date, DATE_FORMAT);
|
const formattedDate = format(date, DATE_FORMAT);
|
||||||
|
|
||||||
// Store the item in the map, overwriting if the date already exists
|
if (map[formattedDate]) {
|
||||||
map[formattedDate] = {
|
// If the value exists, add the current value to the existing one
|
||||||
date: formattedDate,
|
map[formattedDate].value += valueInBaseCurrency;
|
||||||
value: valueInBaseCurrency
|
} else {
|
||||||
};
|
// Otherwise, initialize the value for that date
|
||||||
|
map[formattedDate] = {
|
||||||
|
date: formattedDate,
|
||||||
|
value: valueInBaseCurrency
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
},
|
},
|
||||||
@ -1100,6 +1104,7 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
|
accountBalanceItems,
|
||||||
activities,
|
activities,
|
||||||
dateRange,
|
dateRange,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
@ -1131,6 +1136,8 @@ export class PortfolioService {
|
|||||||
let currentNetPerformanceWithCurrencyEffect =
|
let currentNetPerformanceWithCurrencyEffect =
|
||||||
netPerformanceWithCurrencyEffect;
|
netPerformanceWithCurrencyEffect;
|
||||||
|
|
||||||
|
let currentNetWorth = 0;
|
||||||
|
|
||||||
const items = await portfolioCalculator.getChart({
|
const items = await portfolioCalculator.getChart({
|
||||||
dateRange
|
dateRange
|
||||||
});
|
});
|
||||||
@ -1153,35 +1160,14 @@ export class PortfolioService {
|
|||||||
currentNetPerformanceWithCurrencyEffect = new Big(
|
currentNetPerformanceWithCurrencyEffect = new Big(
|
||||||
itemOfToday.netPerformanceWithCurrencyEffect
|
itemOfToday.netPerformanceWithCurrencyEffect
|
||||||
);
|
);
|
||||||
|
|
||||||
|
currentNetWorth = itemOfToday.netWorth;
|
||||||
}
|
}
|
||||||
|
|
||||||
accountBalanceItems = accountBalanceItems.filter(({ date }) => {
|
|
||||||
return !isBefore(parseDate(date), startDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
const accountBalanceItemOfToday = accountBalanceItems.find(({ date }) => {
|
|
||||||
return date === format(new Date(), DATE_FORMAT);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!accountBalanceItemOfToday) {
|
|
||||||
accountBalanceItems.push({
|
|
||||||
date: format(new Date(), DATE_FORMAT),
|
|
||||||
value: last(accountBalanceItems)?.value ?? 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedHistoricalDataItems = this.mergeHistoricalDataItems(
|
|
||||||
accountBalanceItems,
|
|
||||||
items
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentHistoricalDataItem = last(mergedHistoricalDataItems);
|
|
||||||
const currentNetWorth = currentHistoricalDataItem?.netWorth ?? 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
errors,
|
errors,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
chart: mergedHistoricalDataItems,
|
chart: items,
|
||||||
firstOrderDate: parseDate(items[0]?.date),
|
firstOrderDate: parseDate(items[0]?.date),
|
||||||
performance: {
|
performance: {
|
||||||
currentNetWorth,
|
currentNetWorth,
|
||||||
@ -1909,44 +1895,4 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return { accounts, platforms };
|
return { accounts, platforms };
|
||||||
}
|
}
|
||||||
|
|
||||||
private mergeHistoricalDataItems(
|
|
||||||
accountBalanceItems: HistoricalDataItem[],
|
|
||||||
performanceChartItems: HistoricalDataItem[]
|
|
||||||
): HistoricalDataItem[] {
|
|
||||||
const historicalDataItemsMap: { [date: string]: HistoricalDataItem } = {};
|
|
||||||
let latestAccountBalance = 0;
|
|
||||||
|
|
||||||
for (const item of accountBalanceItems.concat(performanceChartItems)) {
|
|
||||||
const isAccountBalanceItem = accountBalanceItems.includes(item);
|
|
||||||
|
|
||||||
const totalAccountBalance = isAccountBalanceItem
|
|
||||||
? item.value
|
|
||||||
: latestAccountBalance;
|
|
||||||
|
|
||||||
if (isAccountBalanceItem && performanceChartItems.length > 0) {
|
|
||||||
latestAccountBalance = item.value;
|
|
||||||
} else {
|
|
||||||
historicalDataItemsMap[item.date] = {
|
|
||||||
...item,
|
|
||||||
totalAccountBalance,
|
|
||||||
netWorth:
|
|
||||||
(isAccountBalanceItem ? 0 : item.value) + totalAccountBalance
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to an array and sort by date in ascending order
|
|
||||||
const historicalDataItems = Object.keys(historicalDataItemsMap).map(
|
|
||||||
(date) => {
|
|
||||||
return historicalDataItemsMap[date];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
historicalDataItems.sort((a, b) => {
|
|
||||||
return new Date(a.date).getTime() - new Date(b.date).getTime();
|
|
||||||
});
|
|
||||||
|
|
||||||
return historicalDataItems;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -84,55 +84,10 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.dataService
|
this.fetchAccount();
|
||||||
.fetchAccount(this.data.accountId)
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(
|
|
||||||
({
|
|
||||||
balance,
|
|
||||||
currency,
|
|
||||||
name,
|
|
||||||
Platform,
|
|
||||||
transactionCount,
|
|
||||||
value,
|
|
||||||
valueInBaseCurrency
|
|
||||||
}) => {
|
|
||||||
this.balance = balance;
|
|
||||||
this.currency = currency;
|
|
||||||
|
|
||||||
if (isNumber(balance) && isNumber(value)) {
|
|
||||||
this.equity = new Big(value).minus(balance).toNumber();
|
|
||||||
} else {
|
|
||||||
this.equity = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.name = name;
|
|
||||||
this.platformName = Platform?.name ?? '-';
|
|
||||||
this.transactionCount = transactionCount;
|
|
||||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.dataService
|
|
||||||
.fetchPortfolioHoldings({
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
type: 'ACCOUNT',
|
|
||||||
id: this.data.accountId
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(({ holdings }) => {
|
|
||||||
this.holdings = holdings;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fetchAccountBalances();
|
this.fetchAccountBalances();
|
||||||
this.fetchActivities();
|
this.fetchActivities();
|
||||||
|
this.fetchPortfolioHoldings();
|
||||||
this.fetchPortfolioPerformance();
|
this.fetchPortfolioPerformance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +110,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
|
this.fetchAccount();
|
||||||
this.fetchAccountBalances();
|
this.fetchAccountBalances();
|
||||||
this.fetchPortfolioPerformance();
|
this.fetchPortfolioPerformance();
|
||||||
});
|
});
|
||||||
@ -165,6 +121,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
.deleteAccountBalance(aId)
|
.deleteAccountBalance(aId)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
|
this.fetchAccount();
|
||||||
this.fetchAccountBalances();
|
this.fetchAccountBalances();
|
||||||
this.fetchPortfolioPerformance();
|
this.fetchPortfolioPerformance();
|
||||||
});
|
});
|
||||||
@ -199,6 +156,39 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.fetchActivities();
|
this.fetchActivities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fetchAccount() {
|
||||||
|
this.dataService
|
||||||
|
.fetchAccount(this.data.accountId)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(
|
||||||
|
({
|
||||||
|
balance,
|
||||||
|
currency,
|
||||||
|
name,
|
||||||
|
Platform,
|
||||||
|
transactionCount,
|
||||||
|
value,
|
||||||
|
valueInBaseCurrency
|
||||||
|
}) => {
|
||||||
|
this.balance = balance;
|
||||||
|
this.currency = currency;
|
||||||
|
|
||||||
|
if (isNumber(balance) && isNumber(value)) {
|
||||||
|
this.equity = new Big(value).minus(balance).toNumber();
|
||||||
|
} else {
|
||||||
|
this.equity = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.platformName = Platform?.name ?? '-';
|
||||||
|
this.transactionCount = transactionCount;
|
||||||
|
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private fetchAccountBalances() {
|
private fetchAccountBalances() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchAccountBalances(this.data.accountId)
|
.fetchAccountBalances(this.data.accountId)
|
||||||
@ -230,6 +220,24 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fetchPortfolioHoldings() {
|
||||||
|
this.dataService
|
||||||
|
.fetchPortfolioHoldings({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
type: 'ACCOUNT',
|
||||||
|
id: this.data.accountId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ holdings }) => {
|
||||||
|
this.holdings = holdings;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private fetchPortfolioPerformance() {
|
private fetchPortfolioPerformance() {
|
||||||
this.isLoadingChart = true;
|
this.isLoadingChart = true;
|
||||||
|
|
||||||
@ -251,11 +259,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
({ date, netWorth, netWorthInPercentage }) => {
|
({ date, netWorth, netWorthInPercentage }) => {
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
value:
|
value: isNumber(netWorth) ? netWorth : netWorthInPercentage
|
||||||
this.data.hasImpersonationId ||
|
|
||||||
this.user.settings.isRestrictedView
|
|
||||||
? netWorthInPercentage
|
|
||||||
: netWorth
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -233,6 +233,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ export interface SymbolMetrics {
|
|||||||
[date: string]: Big;
|
[date: string]: Big;
|
||||||
};
|
};
|
||||||
timeWeightedInvestmentWithCurrencyEffect: Big;
|
timeWeightedInvestmentWithCurrencyEffect: Big;
|
||||||
|
totalAccountBalanceInBaseCurrency: Big;
|
||||||
totalDividend: Big;
|
totalDividend: Big;
|
||||||
totalDividendInBaseCurrency: Big;
|
totalDividendInBaseCurrency: Big;
|
||||||
totalInterest: Big;
|
totalInterest: Big;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user