Refactor portfolio calculator (#3203)
* Refactor portfolio calculator * Consume Activity[] * Change computeTransactionPoints() to private * Eliminate getTransactionPoints() * Update changelog
This commit is contained in:
parent
5529fdc0ee
commit
5788c6474e
@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Extended the export functionality by the user account’s currency
|
- Extended the export functionality by the user account’s currency
|
||||||
- Added support to override the name of an asset profile in the asset profile details dialog of the admin control
|
- Added support to override the name of an asset profile in the asset profile details dialog of the admin control
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the portfolio calculations
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed the chart tooltip of the benchmark comparator
|
- Fixed the chart tooltip of the benchmark comparator
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import { DataSource, Tag, Type as ActivityType } from '@prisma/client';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { Big } from 'big.js';
|
|
||||||
|
|
||||||
export interface PortfolioOrder {
|
export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
|
||||||
currency: string;
|
|
||||||
date: string;
|
date: string;
|
||||||
dataSource: DataSource;
|
|
||||||
fee: Big;
|
fee: Big;
|
||||||
name: string;
|
|
||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
SymbolProfile: Pick<
|
||||||
tags?: Tag[];
|
Activity['SymbolProfile'],
|
||||||
type: ActivityType;
|
'currency' | 'dataSource' | 'name' | 'symbol'
|
||||||
|
>;
|
||||||
unitPrice: Big;
|
unitPrice: Big;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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 { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -36,46 +37,50 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'CHF',
|
activities: <Activity[]>[
|
||||||
orders: [
|
|
||||||
{
|
{
|
||||||
|
date: new Date('2021-11-22'),
|
||||||
|
fee: 1.55,
|
||||||
|
quantity: 2,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2021-11-22',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(1.55),
|
|
||||||
name: 'Bâloise Holding AG',
|
name: 'Bâloise Holding AG',
|
||||||
quantity: new Big(2),
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW',
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: new Big(142.9)
|
unitPrice: 142.9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
date: new Date('2021-11-30'),
|
||||||
|
fee: 1.65,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2021-11-30',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(1.65),
|
|
||||||
name: 'Bâloise Holding AG',
|
name: 'Bâloise Holding AG',
|
||||||
quantity: new Big(1),
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW',
|
},
|
||||||
type: 'SELL',
|
type: 'SELL',
|
||||||
unitPrice: new Big(136.6)
|
unitPrice: 136.6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
date: new Date('2021-11-30'),
|
||||||
|
fee: 0,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2021-11-30',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(0),
|
|
||||||
name: 'Bâloise Holding AG',
|
name: 'Bâloise Holding AG',
|
||||||
quantity: new Big(1),
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW',
|
},
|
||||||
type: 'SELL',
|
type: 'SELL',
|
||||||
unitPrice: new Big(136.6)
|
unitPrice: 136.6
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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 { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -36,35 +37,37 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'CHF',
|
activities: <Activity[]>[
|
||||||
orders: [
|
|
||||||
{
|
{
|
||||||
|
date: new Date('2021-11-22'),
|
||||||
|
fee: 1.55,
|
||||||
|
quantity: 2,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2021-11-22',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(1.55),
|
|
||||||
name: 'Bâloise Holding AG',
|
name: 'Bâloise Holding AG',
|
||||||
quantity: new Big(2),
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW',
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: new Big(142.9)
|
unitPrice: 142.9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
date: new Date('2021-11-30'),
|
||||||
|
fee: 1.65,
|
||||||
|
quantity: 2,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2021-11-30',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(1.65),
|
|
||||||
name: 'Bâloise Holding AG',
|
name: 'Bâloise Holding AG',
|
||||||
quantity: new Big(2),
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW',
|
},
|
||||||
type: 'SELL',
|
type: 'SELL',
|
||||||
unitPrice: new Big(136.6)
|
unitPrice: 136.6
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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 { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -36,24 +37,24 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'CHF',
|
activities: <Activity[]>[
|
||||||
orders: [
|
|
||||||
{
|
{
|
||||||
|
date: new Date('2021-11-30'),
|
||||||
|
fee: 1.55,
|
||||||
|
quantity: 2,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2021-11-30',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(1.55),
|
|
||||||
name: 'Bâloise Holding AG',
|
name: 'Bâloise Holding AG',
|
||||||
quantity: new Big(2),
|
symbol: 'BALN.SW'
|
||||||
symbol: 'BALN.SW',
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: new Big(136.6)
|
unitPrice: 136.6
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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 { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
@ -49,35 +50,37 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'CHF',
|
activities: <Activity[]>[
|
||||||
orders: [
|
|
||||||
{
|
{
|
||||||
|
date: new Date('2015-01-01'),
|
||||||
|
fee: 0,
|
||||||
|
quantity: 2,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
date: '2015-01-01',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(0),
|
|
||||||
name: 'Bitcoin USD',
|
name: 'Bitcoin USD',
|
||||||
quantity: new Big(2),
|
symbol: 'BTCUSD'
|
||||||
symbol: 'BTCUSD',
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: new Big(320.43)
|
unitPrice: 320.43
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
date: new Date('2017-12-31'),
|
||||||
|
fee: 0,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
date: '2017-12-31',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(0),
|
|
||||||
name: 'Bitcoin USD',
|
name: 'Bitcoin USD',
|
||||||
quantity: new Big(1),
|
symbol: 'BTCUSD'
|
||||||
symbol: 'BTCUSD',
|
},
|
||||||
type: 'SELL',
|
type: 'SELL',
|
||||||
unitPrice: new Big(14156.4)
|
unitPrice: 14156.4
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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 { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
@ -49,24 +50,24 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'CHF',
|
activities: <Activity[]>[
|
||||||
orders: [
|
|
||||||
{
|
{
|
||||||
|
date: new Date('2023-01-03'),
|
||||||
|
fee: 1,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
date: '2023-01-03',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(1),
|
|
||||||
name: 'Alphabet Inc.',
|
name: 'Alphabet Inc.',
|
||||||
quantity: new Big(1),
|
symbol: 'GOOGL'
|
||||||
symbol: 'GOOGL',
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: new Big(89.12)
|
unitPrice: 89.12
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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 { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
|
||||||
@ -49,35 +50,37 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'USD',
|
activities: <Activity[]>[
|
||||||
orders: [
|
|
||||||
{
|
{
|
||||||
|
date: new Date('2021-09-16'),
|
||||||
|
fee: 19,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
date: '2021-09-16',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(19),
|
|
||||||
name: 'Microsoft Inc.',
|
name: 'Microsoft Inc.',
|
||||||
quantity: new Big(1),
|
symbol: 'MSFT'
|
||||||
symbol: 'MSFT',
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: new Big(298.58)
|
unitPrice: 298.58
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
date: new Date('2021-11-16'),
|
||||||
|
fee: 0,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
date: '2021-11-16',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(0),
|
|
||||||
name: 'Microsoft Inc.',
|
name: 'Microsoft Inc.',
|
||||||
quantity: new Big(1),
|
symbol: 'MSFT'
|
||||||
symbol: 'MSFT',
|
},
|
||||||
type: 'DIVIDEND',
|
type: 'DIVIDEND',
|
||||||
unitPrice: new Big(0.62)
|
unitPrice: 0.62
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
currency: 'USD'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||||
|
@ -37,12 +37,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'CHF',
|
activities: [],
|
||||||
orders: []
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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 { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -36,35 +37,37 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'CHF',
|
activities: <Activity[]>[
|
||||||
orders: [
|
|
||||||
{
|
{
|
||||||
|
date: new Date('2022-03-07'),
|
||||||
|
fee: 1.3,
|
||||||
|
quantity: 2,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2022-03-07',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(1.3),
|
|
||||||
name: 'Novartis AG',
|
name: 'Novartis AG',
|
||||||
quantity: new Big(2),
|
symbol: 'NOVN.SW'
|
||||||
symbol: 'NOVN.SW',
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: new Big(75.8)
|
unitPrice: 75.8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
date: new Date('2022-04-08'),
|
||||||
|
fee: 2.95,
|
||||||
|
quantity: 1,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2022-04-08',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(2.95),
|
|
||||||
name: 'Novartis AG',
|
name: 'Novartis AG',
|
||||||
quantity: new Big(1),
|
symbol: 'NOVN.SW'
|
||||||
symbol: 'NOVN.SW',
|
},
|
||||||
type: 'SELL',
|
type: 'SELL',
|
||||||
unitPrice: new Big(85.73)
|
unitPrice: 85.73
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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 { parseDate } from '@ghostfolio/common/helper';
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
@ -36,35 +37,37 @@ describe('PortfolioCalculator', () => {
|
|||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'CHF',
|
activities: <Activity[]>[
|
||||||
orders: [
|
|
||||||
{
|
{
|
||||||
|
date: new Date('2022-03-07'),
|
||||||
|
fee: 0,
|
||||||
|
quantity: 2,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2022-03-07',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(0),
|
|
||||||
name: 'Novartis AG',
|
name: 'Novartis AG',
|
||||||
quantity: new Big(2),
|
symbol: 'NOVN.SW'
|
||||||
symbol: 'NOVN.SW',
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: new Big(75.8)
|
unitPrice: 75.8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
date: new Date('2022-04-08'),
|
||||||
|
fee: 0,
|
||||||
|
quantity: 2,
|
||||||
|
SymbolProfile: {
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
date: '2022-04-08',
|
|
||||||
dataSource: 'YAHOO',
|
dataSource: 'YAHOO',
|
||||||
fee: new Big(0),
|
|
||||||
name: 'Novartis AG',
|
name: 'Novartis AG',
|
||||||
quantity: new Big(2),
|
symbol: 'NOVN.SW'
|
||||||
symbol: 'NOVN.SW',
|
},
|
||||||
type: 'SELL',
|
type: 'SELL',
|
||||||
unitPrice: new Big(85.73)
|
unitPrice: 85.73
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
currency: 'CHF'
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(Date, 'now')
|
.spyOn(Date, 'now')
|
||||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||||
|
@ -22,10 +22,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
|
|
||||||
describe('annualized performance percentage', () => {
|
describe('annualized performance percentage', () => {
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
activities: [],
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
currency: 'USD',
|
currency: 'USD'
|
||||||
orders: []
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Get annualized performance', async () => {
|
it('Get annualized performance', async () => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
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 { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
@ -8,7 +9,8 @@ import {
|
|||||||
InvestmentItem,
|
InvestmentItem,
|
||||||
ResponseError,
|
ResponseError,
|
||||||
SymbolMetrics,
|
SymbolMetrics,
|
||||||
TimelinePosition
|
TimelinePosition,
|
||||||
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { GroupBy } from '@ghostfolio/common/types';
|
import { GroupBy } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
@ -46,122 +48,37 @@ export class PortfolioCalculator {
|
|||||||
private transactionPoints: TransactionPoint[];
|
private transactionPoints: TransactionPoint[];
|
||||||
|
|
||||||
public constructor({
|
public constructor({
|
||||||
|
activities,
|
||||||
currency,
|
currency,
|
||||||
currentRateService,
|
currentRateService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService
|
||||||
orders,
|
|
||||||
transactionPoints
|
|
||||||
}: {
|
}: {
|
||||||
|
activities: Activity[];
|
||||||
currency: string;
|
currency: string;
|
||||||
currentRateService: CurrentRateService;
|
currentRateService: CurrentRateService;
|
||||||
exchangeRateDataService: ExchangeRateDataService;
|
exchangeRateDataService: ExchangeRateDataService;
|
||||||
orders: PortfolioOrder[];
|
|
||||||
transactionPoints?: TransactionPoint[];
|
|
||||||
}) {
|
}) {
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.currentRateService = currentRateService;
|
this.currentRateService = currentRateService;
|
||||||
this.exchangeRateDataService = exchangeRateDataService;
|
this.exchangeRateDataService = exchangeRateDataService;
|
||||||
this.orders = orders;
|
this.orders = activities.map(
|
||||||
|
({ date, fee, quantity, SymbolProfile, type, unitPrice }) => {
|
||||||
|
return {
|
||||||
|
SymbolProfile,
|
||||||
|
type,
|
||||||
|
date: format(date, DATE_FORMAT),
|
||||||
|
fee: new Big(fee),
|
||||||
|
quantity: new Big(quantity),
|
||||||
|
unitPrice: new Big(unitPrice)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.orders.sort((a, b) => {
|
this.orders.sort((a, b) => {
|
||||||
return a.date?.localeCompare(b.date);
|
return a.date?.localeCompare(b.date);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transactionPoints) {
|
this.computeTransactionPoints();
|
||||||
this.transactionPoints = transactionPoints;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public computeTransactionPoints() {
|
|
||||||
this.transactionPoints = [];
|
|
||||||
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
|
|
||||||
|
|
||||||
let lastDate: string = null;
|
|
||||||
let lastTransactionPoint: TransactionPoint = null;
|
|
||||||
|
|
||||||
for (const order of this.orders) {
|
|
||||||
const currentDate = order.date;
|
|
||||||
|
|
||||||
let currentTransactionPointItem: TransactionPointSymbol;
|
|
||||||
const oldAccumulatedSymbol = symbols[order.symbol];
|
|
||||||
|
|
||||||
const factor = getFactor(order.type);
|
|
||||||
|
|
||||||
if (oldAccumulatedSymbol) {
|
|
||||||
let investment = oldAccumulatedSymbol.investment;
|
|
||||||
|
|
||||||
const newQuantity = order.quantity
|
|
||||||
.mul(factor)
|
|
||||||
.plus(oldAccumulatedSymbol.quantity);
|
|
||||||
|
|
||||||
if (order.type === 'BUY') {
|
|
||||||
investment = oldAccumulatedSymbol.investment.plus(
|
|
||||||
order.quantity.mul(order.unitPrice)
|
|
||||||
);
|
|
||||||
} else if (order.type === 'SELL') {
|
|
||||||
investment = oldAccumulatedSymbol.investment.minus(
|
|
||||||
order.quantity.mul(oldAccumulatedSymbol.averagePrice)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTransactionPointItem = {
|
|
||||||
investment,
|
|
||||||
averagePrice: newQuantity.gt(0)
|
|
||||||
? investment.div(newQuantity)
|
|
||||||
: new Big(0),
|
|
||||||
currency: order.currency,
|
|
||||||
dataSource: order.dataSource,
|
|
||||||
dividend: new Big(0),
|
|
||||||
fee: order.fee.plus(oldAccumulatedSymbol.fee),
|
|
||||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
|
||||||
quantity: newQuantity,
|
|
||||||
symbol: order.symbol,
|
|
||||||
tags: order.tags,
|
|
||||||
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
currentTransactionPointItem = {
|
|
||||||
averagePrice: order.unitPrice,
|
|
||||||
currency: order.currency,
|
|
||||||
dataSource: order.dataSource,
|
|
||||||
dividend: new Big(0),
|
|
||||||
fee: order.fee,
|
|
||||||
firstBuyDate: order.date,
|
|
||||||
investment: order.unitPrice.mul(order.quantity).mul(factor),
|
|
||||||
quantity: order.quantity.mul(factor),
|
|
||||||
symbol: order.symbol,
|
|
||||||
tags: order.tags,
|
|
||||||
transactionCount: 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
symbols[order.symbol] = currentTransactionPointItem;
|
|
||||||
|
|
||||||
const items = lastTransactionPoint?.items ?? [];
|
|
||||||
|
|
||||||
const newItems = items.filter(
|
|
||||||
(transactionPointItem) => transactionPointItem.symbol !== order.symbol
|
|
||||||
);
|
|
||||||
|
|
||||||
newItems.push(currentTransactionPointItem);
|
|
||||||
|
|
||||||
newItems.sort((a, b) => {
|
|
||||||
return a.symbol?.localeCompare(b.symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lastDate !== currentDate || lastTransactionPoint === null) {
|
|
||||||
lastTransactionPoint = {
|
|
||||||
date: currentDate,
|
|
||||||
items: newItems
|
|
||||||
};
|
|
||||||
|
|
||||||
this.transactionPoints.push(lastTransactionPoint);
|
|
||||||
} else {
|
|
||||||
lastTransactionPoint.items = newItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastDate = currentDate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAnnualizedPerformancePercent({
|
public getAnnualizedPerformancePercent({
|
||||||
@ -181,10 +98,6 @@ export class PortfolioCalculator {
|
|||||||
return new Big(0);
|
return new Big(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTransactionPoints(): TransactionPoint[] {
|
|
||||||
return this.transactionPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getChartData({
|
public async getChartData({
|
||||||
end = new Date(Date.now()),
|
end = new Date(Date.now()),
|
||||||
start,
|
start,
|
||||||
@ -248,7 +161,7 @@ export class PortfolioCalculator {
|
|||||||
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
||||||
currencies: uniq(Object.values(currencies)),
|
currencies: uniq(Object.values(currencies)),
|
||||||
endDate: endOfDay(end),
|
endDate: endOfDay(end),
|
||||||
startDate: parseDate(this.transactionPoints?.[0]?.date),
|
startDate: this.getStartDate(),
|
||||||
targetCurrency: this.currency
|
targetCurrency: this.currency
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -309,6 +222,7 @@ export class PortfolioCalculator {
|
|||||||
start,
|
start,
|
||||||
step,
|
step,
|
||||||
symbol,
|
symbol,
|
||||||
|
dataSource: null,
|
||||||
exchangeRates:
|
exchangeRates:
|
||||||
exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`],
|
exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`],
|
||||||
isChartMode: true
|
isChartMode: true
|
||||||
@ -551,7 +465,7 @@ export class PortfolioCalculator {
|
|||||||
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
||||||
currencies: uniq(Object.values(currencies)),
|
currencies: uniq(Object.values(currencies)),
|
||||||
endDate: endOfDay(endDate),
|
endDate: endOfDay(endDate),
|
||||||
startDate: parseDate(this.transactionPoints?.[0]?.date),
|
startDate: this.getStartDate(),
|
||||||
targetCurrency: this.currency
|
targetCurrency: this.currency
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -625,6 +539,7 @@ export class PortfolioCalculator {
|
|||||||
} = this.getSymbolMetrics({
|
} = this.getSymbolMetrics({
|
||||||
marketSymbolMap,
|
marketSymbolMap,
|
||||||
start,
|
start,
|
||||||
|
dataSource: item.dataSource,
|
||||||
end: endDate,
|
end: endDate,
|
||||||
exchangeRates:
|
exchangeRates:
|
||||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
||||||
@ -844,7 +759,116 @@ export class PortfolioCalculator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getStartDate() {
|
||||||
|
return this.transactionPoints.length > 0
|
||||||
|
? parseDate(this.transactionPoints[0].date)
|
||||||
|
: new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTransactionPoints() {
|
||||||
|
return this.transactionPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeTransactionPoints() {
|
||||||
|
this.transactionPoints = [];
|
||||||
|
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
|
||||||
|
|
||||||
|
let lastDate: string = null;
|
||||||
|
let lastTransactionPoint: TransactionPoint = null;
|
||||||
|
|
||||||
|
for (const {
|
||||||
|
fee,
|
||||||
|
date,
|
||||||
|
quantity,
|
||||||
|
SymbolProfile,
|
||||||
|
tags,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
} of this.orders) {
|
||||||
|
let currentTransactionPointItem: TransactionPointSymbol;
|
||||||
|
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
||||||
|
|
||||||
|
const factor = getFactor(type);
|
||||||
|
|
||||||
|
if (oldAccumulatedSymbol) {
|
||||||
|
let investment = oldAccumulatedSymbol.investment;
|
||||||
|
|
||||||
|
const newQuantity = quantity
|
||||||
|
.mul(factor)
|
||||||
|
.plus(oldAccumulatedSymbol.quantity);
|
||||||
|
|
||||||
|
if (type === 'BUY') {
|
||||||
|
investment = oldAccumulatedSymbol.investment.plus(
|
||||||
|
quantity.mul(unitPrice)
|
||||||
|
);
|
||||||
|
} else if (type === 'SELL') {
|
||||||
|
investment = oldAccumulatedSymbol.investment.minus(
|
||||||
|
quantity.mul(oldAccumulatedSymbol.averagePrice)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTransactionPointItem = {
|
||||||
|
investment,
|
||||||
|
tags,
|
||||||
|
averagePrice: newQuantity.gt(0)
|
||||||
|
? investment.div(newQuantity)
|
||||||
|
: new Big(0),
|
||||||
|
currency: SymbolProfile.currency,
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
dividend: new Big(0),
|
||||||
|
fee: fee.plus(oldAccumulatedSymbol.fee),
|
||||||
|
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||||
|
quantity: newQuantity,
|
||||||
|
symbol: SymbolProfile.symbol,
|
||||||
|
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
currentTransactionPointItem = {
|
||||||
|
fee,
|
||||||
|
tags,
|
||||||
|
averagePrice: unitPrice,
|
||||||
|
currency: SymbolProfile.currency,
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
dividend: new Big(0),
|
||||||
|
firstBuyDate: date,
|
||||||
|
investment: unitPrice.mul(quantity).mul(factor),
|
||||||
|
quantity: quantity.mul(factor),
|
||||||
|
symbol: SymbolProfile.symbol,
|
||||||
|
transactionCount: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
symbols[SymbolProfile.symbol] = currentTransactionPointItem;
|
||||||
|
|
||||||
|
const items = lastTransactionPoint?.items ?? [];
|
||||||
|
|
||||||
|
const newItems = items.filter(({ symbol }) => {
|
||||||
|
return symbol !== SymbolProfile.symbol;
|
||||||
|
});
|
||||||
|
|
||||||
|
newItems.push(currentTransactionPointItem);
|
||||||
|
|
||||||
|
newItems.sort((a, b) => {
|
||||||
|
return a.symbol?.localeCompare(b.symbol);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastDate !== date || lastTransactionPoint === null) {
|
||||||
|
lastTransactionPoint = {
|
||||||
|
date,
|
||||||
|
items: newItems
|
||||||
|
};
|
||||||
|
|
||||||
|
this.transactionPoints.push(lastTransactionPoint);
|
||||||
|
} else {
|
||||||
|
lastTransactionPoint.items = newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDate = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getSymbolMetrics({
|
private getSymbolMetrics({
|
||||||
|
dataSource,
|
||||||
end,
|
end,
|
||||||
exchangeRates,
|
exchangeRates,
|
||||||
isChartMode = false,
|
isChartMode = false,
|
||||||
@ -861,8 +885,7 @@ export class PortfolioCalculator {
|
|||||||
};
|
};
|
||||||
start: Date;
|
start: Date;
|
||||||
step?: number;
|
step?: number;
|
||||||
symbol: string;
|
} & UniqueAsset): SymbolMetrics {
|
||||||
}): SymbolMetrics {
|
|
||||||
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
|
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
|
||||||
const currentValues: { [date: string]: Big } = {};
|
const currentValues: { [date: string]: Big } = {};
|
||||||
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
|
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
|
||||||
@ -908,8 +931,8 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
// Clone orders to keep the original values in this.orders
|
// Clone orders to keep the original values in this.orders
|
||||||
let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter(
|
let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter(
|
||||||
(order) => {
|
({ SymbolProfile }) => {
|
||||||
return order.symbol === symbol;
|
return SymbolProfile.symbol === symbol;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -988,28 +1011,28 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
// Add a synthetic order at the start and the end date
|
// Add a synthetic order at the start and the end date
|
||||||
orders.push({
|
orders.push({
|
||||||
symbol,
|
|
||||||
currency: null,
|
|
||||||
date: format(start, DATE_FORMAT),
|
date: format(start, DATE_FORMAT),
|
||||||
dataSource: null,
|
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
feeInBaseCurrency: new Big(0),
|
feeInBaseCurrency: new Big(0),
|
||||||
itemType: 'start',
|
itemType: 'start',
|
||||||
name: '',
|
|
||||||
quantity: new Big(0),
|
quantity: new Big(0),
|
||||||
|
SymbolProfile: {
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: unitPriceAtStartDate
|
unitPrice: unitPriceAtStartDate
|
||||||
});
|
});
|
||||||
|
|
||||||
orders.push({
|
orders.push({
|
||||||
symbol,
|
|
||||||
currency: null,
|
|
||||||
date: format(end, DATE_FORMAT),
|
date: format(end, DATE_FORMAT),
|
||||||
dataSource: null,
|
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
feeInBaseCurrency: new Big(0),
|
feeInBaseCurrency: new Big(0),
|
||||||
itemType: 'end',
|
itemType: 'end',
|
||||||
name: '',
|
SymbolProfile: {
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
},
|
||||||
quantity: new Big(0),
|
quantity: new Big(0),
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice: unitPriceAtEndDate
|
unitPrice: unitPriceAtEndDate
|
||||||
@ -1030,14 +1053,14 @@ export class PortfolioCalculator {
|
|||||||
|
|
||||||
if (!hasDate) {
|
if (!hasDate) {
|
||||||
orders.push({
|
orders.push({
|
||||||
symbol,
|
|
||||||
currency: null,
|
|
||||||
date: format(day, DATE_FORMAT),
|
date: format(day, DATE_FORMAT),
|
||||||
dataSource: null,
|
|
||||||
fee: new Big(0),
|
fee: new Big(0),
|
||||||
feeInBaseCurrency: new Big(0),
|
feeInBaseCurrency: new Big(0),
|
||||||
name: '',
|
|
||||||
quantity: new Big(0),
|
quantity: new Big(0),
|
||||||
|
SymbolProfile: {
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
},
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
unitPrice:
|
unitPrice:
|
||||||
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
|
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
|
||||||
|
@ -4,8 +4,6 @@ import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||||
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
||||||
@ -266,15 +264,15 @@ export class PortfolioService {
|
|||||||
}): Promise<PortfolioInvestments> {
|
}): Promise<PortfolioInvestments> {
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { activities } = await this.orderService.getOrders({
|
||||||
await this.getTransactionPoints({
|
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
includeDrafts: true,
|
includeDrafts: true,
|
||||||
types: ['BUY', 'SELL']
|
types: ['BUY', 'SELL'],
|
||||||
|
userCurrency: this.getUserCurrency()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transactionPoints.length === 0) {
|
if (activities.length === 0) {
|
||||||
return {
|
return {
|
||||||
investments: [],
|
investments: [],
|
||||||
streaks: { currentStreak: 0, longestStreak: 0 }
|
streaks: { currentStreak: 0, longestStreak: 0 }
|
||||||
@ -282,18 +280,16 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
transactionPoints,
|
activities,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
orders: portfolioOrders
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { items } = await this.getChart({
|
const { items } = await this.getChart({
|
||||||
dateRange,
|
dateRange,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
portfolioCalculator,
|
portfolioCalculator,
|
||||||
transactionPoints,
|
|
||||||
userId,
|
userId,
|
||||||
withDataDecimation: false
|
withDataDecimation: false
|
||||||
});
|
});
|
||||||
@ -364,26 +360,25 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { activities, portfolioOrders, transactionPoints } =
|
const { activities } = await this.orderService.getOrders({
|
||||||
await this.getTransactionPoints({
|
|
||||||
filters,
|
filters,
|
||||||
types,
|
types,
|
||||||
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts
|
withExcludedAccounts
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
transactionPoints,
|
activities,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
orders: portfolioOrders
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioStart = parseDate(
|
const startDate = this.getStartDate(
|
||||||
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
dateRange,
|
||||||
|
portfolioCalculator.getStartDate()
|
||||||
);
|
);
|
||||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
|
||||||
const currentPositions =
|
const currentPositions =
|
||||||
await portfolioCalculator.getCurrentPositions(startDate);
|
await portfolioCalculator.getCurrentPositions(startDate);
|
||||||
|
|
||||||
@ -737,39 +732,22 @@ export class PortfolioService {
|
|||||||
{ dataSource: aDataSource, symbol: aSymbol }
|
{ dataSource: aDataSource, symbol: aSymbol }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders
|
|
||||||
.filter((order) => {
|
|
||||||
tags = tags.concat(order.tags);
|
|
||||||
|
|
||||||
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
|
|
||||||
})
|
|
||||||
.map((order) => ({
|
|
||||||
currency: order.SymbolProfile.currency,
|
|
||||||
dataSource: order.SymbolProfile.dataSource,
|
|
||||||
date: format(order.date, DATE_FORMAT),
|
|
||||||
fee: new Big(order.fee),
|
|
||||||
name: order.SymbolProfile?.name,
|
|
||||||
quantity: new Big(order.quantity),
|
|
||||||
symbol: order.SymbolProfile.symbol,
|
|
||||||
tags: order.tags,
|
|
||||||
type: order.type,
|
|
||||||
unitPrice: new Big(order.unitPrice)
|
|
||||||
}));
|
|
||||||
|
|
||||||
tags = uniqBy(tags, 'id');
|
tags = uniqBy(tags, 'id');
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
|
activities: orders.filter((order) => {
|
||||||
|
tags = tags.concat(order.tags);
|
||||||
|
|
||||||
|
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
|
||||||
|
}),
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
orders: portfolioOrders
|
|
||||||
});
|
});
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
const portfolioStart = portfolioCalculator.getStartDate();
|
||||||
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
|
||||||
|
|
||||||
const currentPositions =
|
const currentPositions =
|
||||||
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
||||||
|
|
||||||
@ -982,14 +960,14 @@ export class PortfolioService {
|
|||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { activities } = await this.orderService.getOrders({
|
||||||
await this.getTransactionPoints({
|
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
types: ['BUY', 'SELL']
|
types: ['BUY', 'SELL'],
|
||||||
|
userCurrency: this.getUserCurrency()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
if (activities?.length <= 0) {
|
||||||
return {
|
return {
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
positions: []
|
positions: []
|
||||||
@ -997,15 +975,16 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
transactionPoints,
|
activities,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
orders: portfolioOrders
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
const startDate = this.getStartDate(
|
||||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
dateRange,
|
||||||
|
portfolioCalculator.getStartDate()
|
||||||
|
);
|
||||||
const currentPositions =
|
const currentPositions =
|
||||||
await portfolioCalculator.getCurrentPositions(startDate);
|
await portfolioCalculator.getCurrentPositions(startDate);
|
||||||
|
|
||||||
@ -1154,15 +1133,15 @@ export class PortfolioService {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { activities } = await this.orderService.getOrders({
|
||||||
await this.getTransactionPoints({
|
|
||||||
filters,
|
filters,
|
||||||
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts,
|
||||||
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
|
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (accountBalanceItems?.length <= 0 && transactionPoints?.length <= 0) {
|
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
|
||||||
return {
|
return {
|
||||||
chart: [],
|
chart: [],
|
||||||
firstOrderDate: undefined,
|
firstOrderDate: undefined,
|
||||||
@ -1184,17 +1163,16 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
transactionPoints,
|
activities,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
orders: portfolioOrders
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioStart = min(
|
const portfolioStart = min(
|
||||||
[
|
[
|
||||||
parseDate(accountBalanceItems[0]?.date),
|
parseDate(accountBalanceItems[0]?.date),
|
||||||
parseDate(transactionPoints[0]?.date)
|
portfolioCalculator.getStartDate()
|
||||||
].filter((date) => {
|
].filter((date) => {
|
||||||
return isValid(date);
|
return isValid(date);
|
||||||
})
|
})
|
||||||
@ -1230,7 +1208,6 @@ export class PortfolioService {
|
|||||||
dateRange,
|
dateRange,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
portfolioCalculator,
|
portfolioCalculator,
|
||||||
transactionPoints,
|
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1307,25 +1284,22 @@ export class PortfolioService {
|
|||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
const userCurrency = this.getUserCurrency(user);
|
const userCurrency = this.getUserCurrency(user);
|
||||||
|
|
||||||
const { activities, portfolioOrders, transactionPoints } =
|
const { activities } = await this.orderService.getOrders({
|
||||||
await this.getTransactionPoints({
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
types: ['BUY', 'SELL']
|
types: ['BUY', 'SELL']
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
transactionPoints,
|
activities,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
orders: portfolioOrders
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioStart = parseDate(
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
|
portfolioCalculator.getStartDate()
|
||||||
);
|
);
|
||||||
const currentPositions =
|
|
||||||
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
|
||||||
|
|
||||||
const positions = currentPositions.positions.filter(
|
const positions = currentPositions.positions.filter(
|
||||||
(item) => !item.quantity.eq(0)
|
(item) => !item.quantity.eq(0)
|
||||||
@ -1455,18 +1429,16 @@ export class PortfolioService {
|
|||||||
dateRange = 'max',
|
dateRange = 'max',
|
||||||
impersonationId,
|
impersonationId,
|
||||||
portfolioCalculator,
|
portfolioCalculator,
|
||||||
transactionPoints,
|
|
||||||
userId,
|
userId,
|
||||||
withDataDecimation = true
|
withDataDecimation = true
|
||||||
}: {
|
}: {
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
portfolioCalculator: PortfolioCalculator;
|
portfolioCalculator: PortfolioCalculator;
|
||||||
transactionPoints: TransactionPoint[];
|
|
||||||
userId: string;
|
userId: string;
|
||||||
withDataDecimation?: boolean;
|
withDataDecimation?: boolean;
|
||||||
}): Promise<HistoricalDataContainer> {
|
}): Promise<HistoricalDataContainer> {
|
||||||
if (transactionPoints.length === 0) {
|
if (portfolioCalculator.getTransactionPoints().length === 0) {
|
||||||
return {
|
return {
|
||||||
isAllTimeHigh: false,
|
isAllTimeHigh: false,
|
||||||
isAllTimeLow: false,
|
isAllTimeLow: false,
|
||||||
@ -1476,8 +1448,10 @@ export class PortfolioService {
|
|||||||
|
|
||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
|
|
||||||
const portfolioStart = parseDate(transactionPoints[0].date);
|
const startDate = this.getStartDate(
|
||||||
const startDate = this.getStartDate(dateRange, portfolioStart);
|
dateRange,
|
||||||
|
portfolioCalculator.getStartDate()
|
||||||
|
);
|
||||||
const endDate = new Date();
|
const endDate = new Date();
|
||||||
const daysInMarket = differenceInDays(endDate, startDate) + 1;
|
const daysInMarket = differenceInDays(endDate, startDate) + 1;
|
||||||
const step = withDataDecimation
|
const step = withDataDecimation
|
||||||
@ -1865,10 +1839,10 @@ export class PortfolioService {
|
|||||||
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
||||||
|
|
||||||
const annualizedPerformancePercent = new PortfolioCalculator({
|
const annualizedPerformancePercent = new PortfolioCalculator({
|
||||||
|
activities: [],
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
orders: []
|
|
||||||
})
|
})
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
@ -1880,10 +1854,10 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const annualizedPerformancePercentWithCurrencyEffect =
|
const annualizedPerformancePercentWithCurrencyEffect =
|
||||||
new PortfolioCalculator({
|
new PortfolioCalculator({
|
||||||
|
activities: [],
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService
|
||||||
orders: []
|
|
||||||
})
|
})
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
@ -1955,71 +1929,9 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTransactionPoints({
|
private getUserCurrency(aUser?: UserWithSettings) {
|
||||||
filters,
|
|
||||||
includeDrafts = false,
|
|
||||||
types = getAllActivityTypes(),
|
|
||||||
userId,
|
|
||||||
withExcludedAccounts = false
|
|
||||||
}: {
|
|
||||||
filters?: Filter[];
|
|
||||||
includeDrafts?: boolean;
|
|
||||||
types?: ActivityType[];
|
|
||||||
userId: string;
|
|
||||||
withExcludedAccounts?: boolean;
|
|
||||||
}): Promise<{
|
|
||||||
activities: Activity[];
|
|
||||||
transactionPoints: TransactionPoint[];
|
|
||||||
portfolioOrders: PortfolioOrder[];
|
|
||||||
}> {
|
|
||||||
const userCurrency =
|
|
||||||
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
|
|
||||||
|
|
||||||
const { activities, count } = await this.orderService.getOrders({
|
|
||||||
filters,
|
|
||||||
includeDrafts,
|
|
||||||
types,
|
|
||||||
userCurrency,
|
|
||||||
userId,
|
|
||||||
withExcludedAccounts
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count <= 0) {
|
|
||||||
return { activities: [], transactionPoints: [], portfolioOrders: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({
|
|
||||||
currency: order.SymbolProfile.currency,
|
|
||||||
dataSource: order.SymbolProfile.dataSource,
|
|
||||||
date: format(order.date, DATE_FORMAT),
|
|
||||||
fee: new Big(order.fee),
|
|
||||||
name: order.SymbolProfile?.name,
|
|
||||||
quantity: new Big(order.quantity),
|
|
||||||
symbol: order.SymbolProfile.symbol,
|
|
||||||
tags: order.tags,
|
|
||||||
type: order.type,
|
|
||||||
unitPrice: new Big(order.unitPrice)
|
|
||||||
}));
|
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
|
||||||
currency: userCurrency,
|
|
||||||
currentRateService: this.currentRateService,
|
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
|
||||||
orders: portfolioOrders
|
|
||||||
});
|
|
||||||
|
|
||||||
portfolioCalculator.computeTransactionPoints();
|
|
||||||
|
|
||||||
return {
|
|
||||||
activities,
|
|
||||||
portfolioOrders,
|
|
||||||
transactionPoints: portfolioCalculator.getTransactionPoints()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUserCurrency(aUser: UserWithSettings) {
|
|
||||||
return (
|
return (
|
||||||
aUser.Settings?.settings.baseCurrency ??
|
aUser?.Settings?.settings.baseCurrency ??
|
||||||
this.request.user?.Settings?.settings.baseCurrency ??
|
this.request.user?.Settings?.settings.baseCurrency ??
|
||||||
DEFAULT_CURRENCY
|
DEFAULT_CURRENCY
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user