Browse Source

Merge branch 'main' into feature/stock-split

pull/3211/head
Nicolas Fedor 1 year ago
parent
commit
74f0cfdb31
  1. 10
      CHANGELOG.md
  2. 47
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  3. 35
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts
  4. 23
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts
  5. 35
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  6. 23
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts
  7. 35
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts
  8. 8
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts
  9. 35
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  10. 35
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts
  11. 6
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts
  12. 288
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  13. 15
      apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts
  14. 196
      apps/api/src/app/portfolio/portfolio.service.ts
  15. 6
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html
  16. 25
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts
  17. 2
      package.json

10
CHANGELOG.md

@ -7,11 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Fixed
- Added missing dates to edit historical market data in the asset profile details dialog of the admin control panel
## 2.68.0 - 2024-03-29
### Added
- 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
### Changed
- Optimized the portfolio calculations
### Fixed
- Fixed the chart tooltip of the benchmark comparator

47
apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts

@ -1,10 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -36,46 +37,50 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'CHF',
orders: [
activities: <Activity[]>[
{
date: new Date('2021-11-22'),
fee: 1.55,
quantity: 2,
SymbolProfile: {
currency: 'CHF',
date: '2021-11-22',
dataSource: 'YAHOO',
fee: new Big(1.55),
name: 'Bâloise Holding AG',
quantity: new Big(2),
symbol: 'BALN.SW',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPrice: new Big(142.9)
unitPrice: 142.9
},
{
date: new Date('2021-11-30'),
fee: 1.65,
quantity: 1,
SymbolProfile: {
currency: 'CHF',
date: '2021-11-30',
dataSource: 'YAHOO',
fee: new Big(1.65),
name: 'Bâloise Holding AG',
quantity: new Big(1),
symbol: 'BALN.SW',
symbol: 'BALN.SW'
},
type: 'SELL',
unitPrice: new Big(136.6)
unitPrice: 136.6
},
{
date: new Date('2021-11-30'),
fee: 0,
quantity: 1,
SymbolProfile: {
currency: 'CHF',
date: '2021-11-30',
dataSource: 'YAHOO',
fee: new Big(0),
name: 'Bâloise Holding AG',
quantity: new Big(1),
symbol: 'BALN.SW',
symbol: 'BALN.SW'
},
type: 'SELL',
unitPrice: new Big(136.6)
unitPrice: 136.6
}
]
],
currency: 'CHF'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());

35
apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -1,10 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -36,35 +37,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'CHF',
orders: [
activities: <Activity[]>[
{
date: new Date('2021-11-22'),
fee: 1.55,
quantity: 2,
SymbolProfile: {
currency: 'CHF',
date: '2021-11-22',
dataSource: 'YAHOO',
fee: new Big(1.55),
name: 'Bâloise Holding AG',
quantity: new Big(2),
symbol: 'BALN.SW',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPrice: new Big(142.9)
unitPrice: 142.9
},
{
date: new Date('2021-11-30'),
fee: 1.65,
quantity: 2,
SymbolProfile: {
currency: 'CHF',
date: '2021-11-30',
dataSource: 'YAHOO',
fee: new Big(1.65),
name: 'Bâloise Holding AG',
quantity: new Big(2),
symbol: 'BALN.SW',
symbol: 'BALN.SW'
},
type: 'SELL',
unitPrice: new Big(136.6)
unitPrice: 136.6
}
]
],
currency: 'CHF'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());

23
apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts

@ -1,10 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -36,24 +37,24 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'CHF',
orders: [
activities: <Activity[]>[
{
date: new Date('2021-11-30'),
fee: 1.55,
quantity: 2,
SymbolProfile: {
currency: 'CHF',
date: '2021-11-30',
dataSource: 'YAHOO',
fee: new Big(1.55),
name: 'Bâloise Holding AG',
quantity: new Big(2),
symbol: 'BALN.SW',
symbol: 'BALN.SW'
},
type: 'BUY',
unitPrice: new Big(136.6)
unitPrice: 136.6
}
]
],
currency: 'CHF'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());

35
apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -1,11 +1,12 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -49,35 +50,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'CHF',
orders: [
activities: <Activity[]>[
{
date: new Date('2015-01-01'),
fee: 0,
quantity: 2,
SymbolProfile: {
currency: 'USD',
date: '2015-01-01',
dataSource: 'YAHOO',
fee: new Big(0),
name: 'Bitcoin USD',
quantity: new Big(2),
symbol: 'BTCUSD',
symbol: 'BTCUSD'
},
type: 'BUY',
unitPrice: new Big(320.43)
unitPrice: 320.43
},
{
date: new Date('2017-12-31'),
fee: 0,
quantity: 1,
SymbolProfile: {
currency: 'USD',
date: '2017-12-31',
dataSource: 'YAHOO',
fee: new Big(0),
name: 'Bitcoin USD',
quantity: new Big(1),
symbol: 'BTCUSD',
symbol: 'BTCUSD'
},
type: 'SELL',
unitPrice: new Big(14156.4)
unitPrice: 14156.4
}
]
],
currency: 'CHF'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2018-01-01').getTime());

23
apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts

@ -1,11 +1,12 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -49,24 +50,24 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'CHF',
orders: [
activities: <Activity[]>[
{
date: new Date('2023-01-03'),
fee: 1,
quantity: 1,
SymbolProfile: {
currency: 'USD',
date: '2023-01-03',
dataSource: 'YAHOO',
fee: new Big(1),
name: 'Alphabet Inc.',
quantity: new Big(1),
symbol: 'GOOGL',
symbol: 'GOOGL'
},
type: 'BUY',
unitPrice: new Big(89.12)
unitPrice: 89.12
}
]
],
currency: 'CHF'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());

35
apps/api/src/app/portfolio/portfolio-calculator-msft-buy-with-dividend.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -1,11 +1,12 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -49,35 +50,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'USD',
orders: [
activities: <Activity[]>[
{
date: new Date('2021-09-16'),
fee: 19,
quantity: 1,
SymbolProfile: {
currency: 'USD',
date: '2021-09-16',
dataSource: 'YAHOO',
fee: new Big(19),
name: 'Microsoft Inc.',
quantity: new Big(1),
symbol: 'MSFT',
symbol: 'MSFT'
},
type: 'BUY',
unitPrice: new Big(298.58)
unitPrice: 298.58
},
{
date: new Date('2021-11-16'),
fee: 0,
quantity: 1,
SymbolProfile: {
currency: 'USD',
date: '2021-11-16',
dataSource: 'YAHOO',
fee: new Big(0),
name: 'Microsoft Inc.',
quantity: new Big(1),
symbol: 'MSFT',
symbol: 'MSFT'
},
type: 'DIVIDEND',
unitPrice: new Big(0.62)
unitPrice: 0.62
}
]
],
currency: 'USD'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());

8
apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts

@ -1,11 +1,11 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { subDays } from 'date-fns';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -37,12 +37,10 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'CHF',
orders: []
activities: [],
currency: 'CHF'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime());

35
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -1,10 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -36,35 +37,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'CHF',
orders: [
activities: <Activity[]>[
{
date: new Date('2022-03-07'),
fee: 1.3,
quantity: 2,
SymbolProfile: {
currency: 'CHF',
date: '2022-03-07',
dataSource: 'YAHOO',
fee: new Big(1.3),
name: 'Novartis AG',
quantity: new Big(2),
symbol: 'NOVN.SW',
symbol: 'NOVN.SW'
},
type: 'BUY',
unitPrice: new Big(75.8)
unitPrice: 75.8
},
{
date: new Date('2022-04-08'),
fee: 2.95,
quantity: 1,
SymbolProfile: {
currency: 'CHF',
date: '2022-04-08',
dataSource: 'YAHOO',
fee: new Big(2.95),
name: 'Novartis AG',
quantity: new Big(1),
symbol: 'NOVN.SW',
symbol: 'NOVN.SW'
},
type: 'SELL',
unitPrice: new Big(85.73)
unitPrice: 85.73
}
]
],
currency: 'CHF'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime());

35
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -1,10 +1,11 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { parseDate } from '@ghostfolio/common/helper';
import { Big } from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
@ -36,35 +37,37 @@ describe('PortfolioCalculator', () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'CHF',
orders: [
activities: <Activity[]>[
{
date: new Date('2022-03-07'),
fee: 0,
quantity: 2,
SymbolProfile: {
currency: 'CHF',
date: '2022-03-07',
dataSource: 'YAHOO',
fee: new Big(0),
name: 'Novartis AG',
quantity: new Big(2),
symbol: 'NOVN.SW',
symbol: 'NOVN.SW'
},
type: 'BUY',
unitPrice: new Big(75.8)
unitPrice: 75.8
},
{
date: new Date('2022-04-08'),
fee: 0,
quantity: 2,
SymbolProfile: {
currency: 'CHF',
date: '2022-04-08',
dataSource: 'YAHOO',
fee: new Big(0),
name: 'Novartis AG',
quantity: new Big(2),
symbol: 'NOVN.SW',
symbol: 'NOVN.SW'
},
type: 'SELL',
unitPrice: new Big(85.73)
unitPrice: 85.73
}
]
],
currency: 'CHF'
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime());

6
apps/api/src/app/portfolio/portfolio-calculator.spec.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts

@ -1,8 +1,8 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { Big } from 'big.js';
import { CurrentRateService } from './current-rate.service';
import { PortfolioCalculator } from './portfolio-calculator';
describe('PortfolioCalculator', () => {
@ -22,10 +22,10 @@ describe('PortfolioCalculator', () => {
describe('annualized performance percentage', () => {
const portfolioCalculator = new PortfolioCalculator({
activities: [],
currentRateService,
exchangeRateDataService,
currency: 'USD',
orders: []
currency: 'USD'
});
it('Get annualized performance', async () => {

288
apps/api/src/app/portfolio/portfolio-calculator.ts → apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -1,3 +1,10 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
@ -8,7 +15,8 @@ import {
InvestmentItem,
ResponseError,
SymbolMetrics,
TimelinePosition
TimelinePosition,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { GroupBy } from '@ghostfolio/common/types';
@ -28,13 +36,6 @@ import {
} from 'date-fns';
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface';
import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculator {
private static readonly ENABLE_LOGGING = false;
@ -46,125 +47,37 @@ export class PortfolioCalculator {
private transactionPoints: TransactionPoint[];
public constructor({
activities,
currency,
currentRateService,
exchangeRateDataService,
orders,
transactionPoints
exchangeRateDataService
}: {
activities: Activity[];
currency: string;
currentRateService: CurrentRateService;
exchangeRateDataService: ExchangeRateDataService;
orders: PortfolioOrder[];
transactionPoints?: TransactionPoint[];
}) {
this.currency = currency;
this.currentRateService = currentRateService;
this.exchangeRateDataService = exchangeRateDataService;
this.orders = orders;
this.orders.sort((a, b) => {
return a.date?.localeCompare(b.date);
});
if (transactionPoints) {
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.type === 'SPLIT'
? order.quantity.s === 1
? oldAccumulatedSymbol.quantity.mul(order.quantity)
: oldAccumulatedSymbol.quantity.div(order.quantity.abs())
: 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
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)
};
}
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);
this.orders.sort((a, b) => {
return a.date?.localeCompare(b.date);
});
if (lastDate !== currentDate || lastTransactionPoint === null) {
lastTransactionPoint = {
date: currentDate,
items: newItems
};
this.transactionPoints.push(lastTransactionPoint);
} else {
lastTransactionPoint.items = newItems;
}
lastDate = currentDate;
}
this.computeTransactionPoints();
}
public getAnnualizedPerformancePercent({
@ -184,10 +97,6 @@ export class PortfolioCalculator {
return new Big(0);
}
public getTransactionPoints(): TransactionPoint[] {
return this.transactionPoints;
}
public async getChartData({
end = new Date(Date.now()),
start,
@ -251,7 +160,7 @@ export class PortfolioCalculator {
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)),
endDate: endOfDay(end),
startDate: parseDate(this.transactionPoints?.[0]?.date),
startDate: this.getStartDate(),
targetCurrency: this.currency
});
@ -312,6 +221,7 @@ export class PortfolioCalculator {
start,
step,
symbol,
dataSource: null,
exchangeRates:
exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`],
isChartMode: true
@ -554,7 +464,7 @@ export class PortfolioCalculator {
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)),
endDate: endOfDay(endDate),
startDate: parseDate(this.transactionPoints?.[0]?.date),
startDate: this.getStartDate(),
targetCurrency: this.currency
});
@ -628,6 +538,7 @@ export class PortfolioCalculator {
} = this.getSymbolMetrics({
marketSymbolMap,
start,
dataSource: item.dataSource,
end: endDate,
exchangeRates:
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
@ -847,7 +758,119 @@ 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 =
type === 'SPLIT'
? quantity.s === 1
? oldAccumulatedSymbol.quantity.mul(quantity)
: oldAccumulatedSymbol.quantity.div(quantity.abs())
: 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({
dataSource,
end,
exchangeRates,
isChartMode = false,
@ -864,8 +887,7 @@ export class PortfolioCalculator {
};
start: Date;
step?: number;
symbol: string;
}): SymbolMetrics {
} & UniqueAsset): SymbolMetrics {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
@ -911,8 +933,8 @@ export class PortfolioCalculator {
// Clone orders to keep the original values in this.orders
let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter(
(order) => {
return order.symbol === symbol;
({ SymbolProfile }) => {
return SymbolProfile.symbol === symbol;
}
);
@ -991,28 +1013,28 @@ export class PortfolioCalculator {
// Add a synthetic order at the start and the end date
orders.push({
symbol,
currency: null,
date: format(start, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
itemType: 'start',
name: '',
quantity: new Big(0),
SymbolProfile: {
dataSource,
symbol
},
type: 'BUY',
unitPrice: unitPriceAtStartDate
});
orders.push({
symbol,
currency: null,
date: format(end, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
itemType: 'end',
name: '',
SymbolProfile: {
dataSource,
symbol
},
quantity: new Big(0),
type: 'BUY',
unitPrice: unitPriceAtEndDate
@ -1033,14 +1055,14 @@ export class PortfolioCalculator {
if (!hasDate) {
orders.push({
symbol,
currency: null,
date: format(day, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
name: '',
quantity: new Big(0),
SymbolProfile: {
dataSource,
symbol
},
type: 'BUY',
unitPrice:
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??

15
apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts

@ -1,15 +1,12 @@
import { DataSource, Tag, Type as ActivityType } from '@prisma/client';
import { Big } from 'big.js';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
export interface PortfolioOrder {
currency: string;
export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
date: string;
dataSource: DataSource;
fee: Big;
name: string;
quantity: Big;
symbol: string;
tags?: Tag[];
type: ActivityType;
SymbolProfile: Pick<
Activity['SymbolProfile'],
'currency' | 'dataSource' | 'name' | 'symbol'
>;
unitPrice: Big;
}

196
apps/api/src/app/portfolio/portfolio.service.ts

@ -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 { OrderService } from '@ghostfolio/api/app/order/order.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 { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
@ -88,11 +86,11 @@ import {
} from 'date-fns';
import { isEmpty, last, uniq, uniqBy } from 'lodash';
import { PortfolioCalculator } from './calculator/twr/portfolio-calculator';
import {
HistoricalDataContainer,
PortfolioPositionDetail
} from './interfaces/portfolio-position-detail.interface';
import { PortfolioCalculator } from './portfolio-calculator';
import { RulesService } from './rules.service';
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
@ -266,15 +264,15 @@ export class PortfolioService {
}): Promise<PortfolioInvestments> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
const { activities } = await this.orderService.getOrders({
filters,
userId,
includeDrafts: true,
types: ['BUY', 'SELL']
types: ['BUY', 'SELL'],
userCurrency: this.getUserCurrency()
});
if (transactionPoints.length === 0) {
if (activities.length === 0) {
return {
investments: [],
streaks: { currentStreak: 0, longestStreak: 0 }
@ -282,18 +280,16 @@ export class PortfolioService {
}
const portfolioCalculator = new PortfolioCalculator({
transactionPoints,
activities,
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: portfolioOrders
exchangeRateDataService: this.exchangeRateDataService
});
const { items } = await this.getChart({
dateRange,
impersonationId,
portfolioCalculator,
transactionPoints,
userId,
withDataDecimation: false
});
@ -364,26 +360,25 @@ export class PortfolioService {
});
}
const { activities, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
const { activities } = await this.orderService.getOrders({
filters,
types,
userCurrency,
userId,
withExcludedAccounts
});
const portfolioCalculator = new PortfolioCalculator({
transactionPoints,
activities,
currency: userCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: portfolioOrders
exchangeRateDataService: this.exchangeRateDataService
});
const portfolioStart = parseDate(
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
const startDate = this.getStartDate(
dateRange,
portfolioCalculator.getStartDate()
);
const startDate = this.getStartDate(dateRange, portfolioStart);
const currentPositions =
await portfolioCalculator.getCurrentPositions(startDate);
@ -737,41 +732,24 @@ export class PortfolioService {
{ dataSource: aDataSource, symbol: aSymbol }
]);
const portfolioOrders: PortfolioOrder[] = orders
.filter((order) => {
tags = uniqBy(tags, 'id');
const portfolioCalculator = new PortfolioCalculator({
activities: orders.filter((order) => {
tags = tags.concat(order.tags);
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL', 'SPLIT'].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');
const portfolioCalculator = new PortfolioCalculator({
}),
currency: userCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: portfolioOrders
exchangeRateDataService: this.exchangeRateDataService
});
portfolioCalculator.computeTransactionPoints();
const portfolioStart = portfolioCalculator.getStartDate();
const transactionPoints = portfolioCalculator.getTransactionPoints();
const portfolioStart = parseDate(transactionPoints[0].date);
const currentPositions =
await portfolioCalculator.getCurrentPositions(portfolioStart);
@ -984,14 +962,14 @@ export class PortfolioService {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
const { activities } = await this.orderService.getOrders({
filters,
userId,
types: ['BUY', 'SELL']
types: ['BUY', 'SELL'],
userCurrency: this.getUserCurrency()
});
if (transactionPoints?.length <= 0) {
if (activities?.length <= 0) {
return {
hasErrors: false,
positions: []
@ -999,15 +977,16 @@ export class PortfolioService {
}
const portfolioCalculator = new PortfolioCalculator({
transactionPoints,
activities,
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: portfolioOrders
exchangeRateDataService: this.exchangeRateDataService
});
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart);
const startDate = this.getStartDate(
dateRange,
portfolioCalculator.getStartDate()
);
const currentPositions =
await portfolioCalculator.getCurrentPositions(startDate);
@ -1156,15 +1135,15 @@ export class PortfolioService {
)
);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
const { activities } = await this.orderService.getOrders({
filters,
userCurrency,
userId,
withExcludedAccounts,
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
});
if (accountBalanceItems?.length <= 0 && transactionPoints?.length <= 0) {
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
return {
chart: [],
firstOrderDate: undefined,
@ -1186,17 +1165,16 @@ export class PortfolioService {
}
const portfolioCalculator = new PortfolioCalculator({
transactionPoints,
activities,
currency: userCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: portfolioOrders
exchangeRateDataService: this.exchangeRateDataService
});
const portfolioStart = min(
[
parseDate(accountBalanceItems[0]?.date),
parseDate(transactionPoints[0]?.date)
portfolioCalculator.getStartDate()
].filter((date) => {
return isValid(date);
})
@ -1232,7 +1210,6 @@ export class PortfolioService {
dateRange,
impersonationId,
portfolioCalculator,
transactionPoints,
userId
});
@ -1309,25 +1286,22 @@ export class PortfolioService {
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
const { activities, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
const { activities } = await this.orderService.getOrders({
userCurrency,
userId,
types: ['BUY', 'SELL']
});
const portfolioCalculator = new PortfolioCalculator({
transactionPoints,
activities,
currency: userCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: portfolioOrders
exchangeRateDataService: this.exchangeRateDataService
});
const portfolioStart = parseDate(
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
const currentPositions = await portfolioCalculator.getCurrentPositions(
portfolioCalculator.getStartDate()
);
const currentPositions =
await portfolioCalculator.getCurrentPositions(portfolioStart);
const positions = currentPositions.positions.filter(
(item) => !item.quantity.eq(0)
@ -1457,18 +1431,16 @@ export class PortfolioService {
dateRange = 'max',
impersonationId,
portfolioCalculator,
transactionPoints,
userId,
withDataDecimation = true
}: {
dateRange?: DateRange;
impersonationId: string;
portfolioCalculator: PortfolioCalculator;
transactionPoints: TransactionPoint[];
userId: string;
withDataDecimation?: boolean;
}): Promise<HistoricalDataContainer> {
if (transactionPoints.length === 0) {
if (portfolioCalculator.getTransactionPoints().length === 0) {
return {
isAllTimeHigh: false,
isAllTimeLow: false,
@ -1478,8 +1450,10 @@ export class PortfolioService {
userId = await this.getUserId(impersonationId, userId);
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart);
const startDate = this.getStartDate(
dateRange,
portfolioCalculator.getStartDate()
);
const endDate = new Date();
const daysInMarket = differenceInDays(endDate, startDate) + 1;
const step = withDataDecimation
@ -1867,10 +1841,10 @@ export class PortfolioService {
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
const annualizedPerformancePercent = new PortfolioCalculator({
activities: [],
currency: userCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: []
exchangeRateDataService: this.exchangeRateDataService
})
.getAnnualizedPerformancePercent({
daysInMarket,
@ -1882,10 +1856,10 @@ export class PortfolioService {
const annualizedPerformancePercentWithCurrencyEffect =
new PortfolioCalculator({
activities: [],
currency: userCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: []
exchangeRateDataService: this.exchangeRateDataService
})
.getAnnualizedPerformancePercent({
daysInMarket,
@ -1957,71 +1931,9 @@ export class PortfolioService {
);
}
private async getTransactionPoints({
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) {
private getUserCurrency(aUser?: UserWithSettings) {
return (
aUser.Settings?.settings.baseCurrency ??
aUser?.Settings?.settings.baseCurrency ??
this.request.user?.Settings?.settings.baseCurrency ??
DEFAULT_CURRENCY
);

6
apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html

@ -9,11 +9,7 @@
[showYAxis]="true"
[symbol]="symbol"
/>
<div
*ngFor="let itemByMonth of marketDataByMonth | keyvalue"
class="d-flex"
[hidden]="!marketData.length > 0"
>
<div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex">
<div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>
<div class="align-items-center d-flex flex-grow-1 px-1">
<div

25
apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts

@ -19,15 +19,17 @@ import { MatDialog } from '@angular/material/dialog';
import { DataSource, MarketData } from '@prisma/client';
import {
addDays,
addMonths,
format,
isBefore,
isSameDay,
isToday,
isValid,
min,
parse,
parseISO
} from 'date-fns';
import { last } from 'lodash';
import { first, last } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, takeUntil } from 'rxjs';
@ -135,6 +137,27 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
marketPrice: marketDataItem.marketPrice
};
}
if (this.dateOfFirstActivity) {
// Fill up missing months
const dates = Object.keys(this.marketDataByMonth).sort();
const startDate = min([
parseISO(this.dateOfFirstActivity),
parseISO(first(dates))
]);
const endDate = parseISO(last(dates));
let currentDate = startDate;
while (isBefore(currentDate, endDate)) {
const key = format(currentDate, 'yyyy-MM');
if (!this.marketDataByMonth[key]) {
this.marketDataByMonth[key] = {};
}
currentDate = addMonths(currentDate, 1);
}
}
}
public isDateOfInterest(aDateString: string) {

2
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.67.0",
"version": "2.68.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",

Loading…
Cancel
Save