Browse Source

Refactor portfolio calculator

* Consume Activity[]
* Change computeTransactionPoints() to private
pull/3203/head
Thomas Kaul 1 year ago
parent
commit
82c41da81a
  1. 15
      apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts
  2. 45
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  3. 33
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts
  4. 21
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts
  5. 33
      apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  6. 21
      apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts
  7. 33
      apps/api/src/app/portfolio/portfolio-calculator-msft-buy-with-dividend.spec.ts
  8. 6
      apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts
  9. 33
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  10. 33
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts
  11. 4
      apps/api/src/app/portfolio/portfolio-calculator.spec.ts
  12. 84
      apps/api/src/app/portfolio/portfolio-calculator.ts
  13. 94
      apps/api/src/app/portfolio/portfolio.service.ts

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 { 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;
} }

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

@ -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());

33
apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -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());

21
apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts

@ -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());

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

@ -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());

21
apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts

@ -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());

33
apps/api/src/app/portfolio/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -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());

6
apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts

@ -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());

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

@ -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());

33
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -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());

4
apps/api/src/app/portfolio/portfolio-calculator.spec.ts

@ -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 () => {

84
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -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,33 +48,40 @@ 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() { private computeTransactionPoints() {
this.transactionPoints = []; this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {}; const symbols: { [symbol: string]: TransactionPointSymbol } = {};
@ -83,7 +92,7 @@ export class PortfolioCalculator {
const currentDate = order.date; const currentDate = order.date;
let currentTransactionPointItem: TransactionPointSymbol; let currentTransactionPointItem: TransactionPointSymbol;
const oldAccumulatedSymbol = symbols[order.symbol]; const oldAccumulatedSymbol = symbols[order.SymbolProfile.symbol];
const factor = getFactor(order.type); const factor = getFactor(order.type);
@ -109,38 +118,39 @@ export class PortfolioCalculator {
averagePrice: newQuantity.gt(0) averagePrice: newQuantity.gt(0)
? investment.div(newQuantity) ? investment.div(newQuantity)
: new Big(0), : new Big(0),
currency: order.currency, currency: order.SymbolProfile.currency,
dataSource: order.dataSource, dataSource: order.SymbolProfile.dataSource,
dividend: new Big(0), dividend: new Big(0),
fee: order.fee.plus(oldAccumulatedSymbol.fee), fee: order.fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
quantity: newQuantity, quantity: newQuantity,
symbol: order.symbol, symbol: order.SymbolProfile.symbol,
tags: order.tags, tags: order.tags,
transactionCount: oldAccumulatedSymbol.transactionCount + 1 transactionCount: oldAccumulatedSymbol.transactionCount + 1
}; };
} else { } else {
currentTransactionPointItem = { currentTransactionPointItem = {
averagePrice: order.unitPrice, averagePrice: order.unitPrice,
currency: order.currency, currency: order.SymbolProfile.currency,
dataSource: order.dataSource, dataSource: order.SymbolProfile.dataSource,
dividend: new Big(0), dividend: new Big(0),
fee: order.fee, fee: order.fee,
firstBuyDate: order.date, firstBuyDate: order.date,
investment: order.unitPrice.mul(order.quantity).mul(factor), investment: order.unitPrice.mul(order.quantity).mul(factor),
quantity: order.quantity.mul(factor), quantity: order.quantity.mul(factor),
symbol: order.symbol, symbol: order.SymbolProfile.symbol,
tags: order.tags, tags: order.tags,
transactionCount: 1 transactionCount: 1
}; };
} }
symbols[order.symbol] = currentTransactionPointItem; symbols[order.SymbolProfile.symbol] = currentTransactionPointItem;
const items = lastTransactionPoint?.items ?? []; const items = lastTransactionPoint?.items ?? [];
const newItems = items.filter( const newItems = items.filter(
(transactionPointItem) => transactionPointItem.symbol !== order.symbol (transactionPointItem) =>
transactionPointItem.symbol !== order.SymbolProfile.symbol
); );
newItems.push(currentTransactionPointItem); newItems.push(currentTransactionPointItem);
@ -309,6 +319,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
@ -625,6 +636,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}`],
@ -845,6 +857,7 @@ export class PortfolioCalculator {
} }
private getSymbolMetrics({ private getSymbolMetrics({
dataSource,
end, end,
exchangeRates, exchangeRates,
isChartMode = false, isChartMode = false,
@ -861,8 +874,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 +920,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 +1000,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 +1042,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] ??

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

@ -266,8 +266,7 @@ 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, transactionPoints } = await this.getTransactionPoints({
await this.getTransactionPoints({
filters, filters,
userId, userId,
includeDrafts: true, includeDrafts: true,
@ -282,11 +281,10 @@ 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({
@ -364,8 +362,7 @@ export class PortfolioService {
}); });
} }
const { activities, portfolioOrders, transactionPoints } = const { activities, transactionPoints } = await this.getTransactionPoints({
await this.getTransactionPoints({
filters, filters,
types, types,
userId, userId,
@ -373,11 +370,10 @@ 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 = parseDate( const portfolioStart = parseDate(
@ -737,35 +733,19 @@ 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 transactionPoints = portfolioCalculator.getTransactionPoints(); const transactionPoints = portfolioCalculator.getTransactionPoints();
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(transactionPoints[0].date);
@ -982,8 +962,7 @@ 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, transactionPoints } = await this.getTransactionPoints({
await this.getTransactionPoints({
filters, filters,
userId, userId,
types: ['BUY', 'SELL'] types: ['BUY', 'SELL']
@ -997,11 +976,10 @@ 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 portfolioStart = parseDate(transactionPoints[0].date);
@ -1154,8 +1132,7 @@ export class PortfolioService {
) )
); );
const { portfolioOrders, transactionPoints } = const { activities, transactionPoints } = await this.getTransactionPoints({
await this.getTransactionPoints({
filters, filters,
userId, userId,
withExcludedAccounts, withExcludedAccounts,
@ -1184,11 +1161,10 @@ 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(
@ -1307,18 +1283,16 @@ 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, transactionPoints } = await this.getTransactionPoints({
await this.getTransactionPoints({
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 portfolioStart = parseDate(
@ -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,6 +1929,7 @@ export class PortfolioService {
); );
} }
// TODO: Eliminate
private async getTransactionPoints({ private async getTransactionPoints({
filters, filters,
includeDrafts = false, includeDrafts = false,
@ -1989,27 +1964,26 @@ export class PortfolioService {
} }
const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({ const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({
currency: order.SymbolProfile.currency, // currency: order.SymbolProfile.currency,
dataSource: order.SymbolProfile.dataSource, // dataSource: order.SymbolProfile.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big(order.fee), fee: new Big(order.fee),
name: order.SymbolProfile?.name, // name: order.SymbolProfile?.name,
quantity: new Big(order.quantity), quantity: new Big(order.quantity),
symbol: order.SymbolProfile.symbol, // symbol: order.SymbolProfile.symbol,
tags: order.tags, SymbolProfile: order.SymbolProfile,
// tags: order.tags,
type: order.type, type: order.type,
unitPrice: new Big(order.unitPrice) unitPrice: new Big(order.unitPrice)
})); }));
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
activities,
currency: userCurrency, currency: userCurrency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService, exchangeRateDataService: this.exchangeRateDataService
orders: portfolioOrders
}); });
portfolioCalculator.computeTransactionPoints();
return { return {
activities, activities,
portfolioOrders, portfolioOrders,

Loading…
Cancel
Save