Browse Source

Merge branch 'main' into fix/removeAccountActivity

pull/3112/head
Thomas Kaul 1 year ago
committed by GitHub
parent
commit
2d068aad25
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 1
      apps/api/jest.config.ts
  3. 4
      apps/api/src/app/admin/admin.service.ts
  4. 2
      apps/api/src/app/auth/web-auth.service.ts
  5. 2
      apps/api/src/app/order/order.service.ts
  6. 11
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  7. 4
      apps/api/src/app/portfolio/interfaces/current-positions.interface.ts
  8. 2
      apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts
  9. 2
      apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts
  10. 7
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts
  11. 7
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts
  12. 7
      apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  13. 7
      apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts
  14. 118
      apps/api/src/app/portfolio/portfolio-calculator-msft-buy-with-dividend.spec.ts
  15. 2
      apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts
  16. 7
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  17. 7
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts
  18. 223
      apps/api/src/app/portfolio/portfolio-calculator.ts
  19. 22
      apps/api/src/app/portfolio/portfolio.controller.ts
  20. 92
      apps/api/src/app/portfolio/portfolio.service.ts
  21. 7
      apps/api/src/services/configuration/configuration.service.ts
  22. 8
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts
  23. 22
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  24. 1
      apps/api/src/services/interfaces/environment.interface.ts
  25. 20
      apps/client/src/app/components/admin-users/admin-users.html
  26. 2
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  27. 4
      apps/client/src/styles.scss
  28. 3
      libs/common/src/lib/interfaces/admin-data.interface.ts
  29. 1
      libs/common/src/lib/interfaces/portfolio-position.interface.ts
  30. 2
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  31. 2
      libs/common/src/lib/interfaces/symbol-metrics.interface.ts
  32. 3
      libs/common/src/lib/interfaces/timeline-position.interface.ts

6
CHANGELOG.md

@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Integrated dividend into the transaction point concept in the portfolio service
- Removed the environment variable `WEB_AUTH_RP_ID`
### Fixed
- Fixed an issue in the calculation of the portfolio summary caused by future liabilities
- Fixed an issue with removing a linked account from a (wealth) item activity
## 2.61.1 - 2024-03-06

1
apps/api/jest.config.ts

@ -13,7 +13,6 @@ export default {
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/api',
testTimeout: 10000,
testEnvironment: 'node',
preset: '../../jest.preset.js'
};

4
apps/api/src/app/admin/admin.service.ts

@ -440,13 +440,14 @@ export class AdminService {
},
createdAt: true,
id: true,
role: true,
Subscription: true
},
take: 30
});
return usersWithAnalytics.map(
({ _count, Analytics, createdAt, id, Subscription }) => {
({ _count, Analytics, createdAt, id, role, Subscription }) => {
const daysSinceRegistration =
differenceInDays(new Date(), createdAt) + 1;
const engagement = Analytics
@ -466,6 +467,7 @@ export class AdminService {
createdAt,
engagement,
id,
role,
subscription,
accountCount: _count.Account || 0,
country: Analytics?.country,

2
apps/api/src/app/auth/web-auth.service.ts

@ -41,7 +41,7 @@ export class WebAuthService {
) {}
get rpID() {
return this.configurationService.get('WEB_AUTH_RP_ID');
return new URL(this.configurationService.get('ROOT_URL')).hostname;
}
get expectedOrigin() {

2
apps/api/src/app/order/order.service.ts

@ -326,11 +326,13 @@ export class OrderService {
return {
...order,
value,
// TODO: Use exchange rate of date
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
),
// TODO: Use exchange rate of date
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.SymbolProfile.currency,

11
apps/api/src/app/portfolio/current-rate.service.mock.ts

@ -43,6 +43,17 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 0 };
case 'MSFT':
if (isSameDay(parseDate('2021-09-16'), date)) {
return { marketPrice: 89.12 };
} else if (isSameDay(parseDate('2021-11-16'), date)) {
return { marketPrice: 339.51 };
} else if (isSameDay(parseDate('2023-07-10'), date)) {
return { marketPrice: 331.83 };
}
return { marketPrice: 0 };
case 'NOVN.SW':
if (isSameDay(parseDate('2022-04-11'), date)) {
return { marketPrice: 87.8 };

4
apps/api/src/app/portfolio/interfaces/current-positions.interface.ts

@ -3,7 +3,7 @@ import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
import Big from 'big.js';
export interface CurrentPositions extends ResponseError {
positions: TimelinePosition[];
currentValueInBaseCurrency: Big;
grossPerformance: Big;
grossPerformanceWithCurrencyEffect: Big;
grossPerformancePercentage: Big;
@ -14,6 +14,6 @@ export interface CurrentPositions extends ResponseError {
netPerformanceWithCurrencyEffect: Big;
netPerformancePercentage: Big;
netPerformancePercentageWithCurrencyEffect: Big;
currentValue: Big;
positions: TimelinePosition[];
totalInvestment: Big;
}

2
apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts

@ -5,7 +5,7 @@ import { PortfolioOrder } from './portfolio-order.interface';
export interface PortfolioOrderItem extends PortfolioOrder {
feeInBaseCurrency?: Big;
feeInBaseCurrencyWithCurrencyEffect?: Big;
itemType?: '' | 'start' | 'end';
itemType?: 'end' | 'start';
unitPriceInBaseCurrency?: Big;
unitPriceInBaseCurrencyWithCurrencyEffect?: Big;
}

2
apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts

@ -2,8 +2,10 @@ import { DataSource, Tag } from '@prisma/client';
import Big from 'big.js';
export interface TransactionPointSymbol {
averagePrice: Big;
currency: string;
dataSource: DataSource;
dividend: Big;
fee: Big;
firstBuyDate: string;
investment: Big;

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

@ -87,7 +87,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
currentValue: new Big('0'),
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.0440867739678096571'),
@ -107,6 +107,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('0'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'),
firstBuyDate: '2021-11-22',
grossPerformance: new Big('-12.6'),
@ -129,7 +131,8 @@ describe('PortfolioCalculator', () => {
symbol: 'BALN.SW',
timeWeightedInvestment: new Big('285.8'),
timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'),
transactionCount: 2
transactionCount: 2,
valueInBaseCurrency: new Big('0')
}
],
totalInvestment: new Big('0'),

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

@ -76,7 +76,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
currentValue: new Big('297.8'),
currentValueInBaseCurrency: new Big('297.8'),
errors: [],
grossPerformance: new Big('24.6'),
grossPerformancePercentage: new Big('0.09004392386530014641'),
@ -96,6 +96,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('136.6'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1.55'),
firstBuyDate: '2021-11-30',
grossPerformance: new Big('24.6'),
@ -118,7 +120,8 @@ describe('PortfolioCalculator', () => {
symbol: 'BALN.SW',
timeWeightedInvestment: new Big('273.2'),
timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'),
transactionCount: 1
transactionCount: 1,
valueInBaseCurrency: new Big('297.8')
}
],
totalInvestment: new Big('273.2'),

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

@ -100,7 +100,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
currentValue: new Big('13298.425356'),
currentValueInBaseCurrency: new Big('13298.425356'),
errors: [],
grossPerformance: new Big('27172.74'),
grossPerformancePercentage: new Big('42.41978276196153750666'),
@ -120,6 +120,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('320.43'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'),
firstBuyDate: '2015-01-01',
grossPerformance: new Big('27172.74'),
@ -149,7 +151,8 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big(
'636.79469348020066587024'
),
transactionCount: 2
transactionCount: 2,
valueInBaseCurrency: new Big('13298.425356')
}
],
totalInvestment: new Big('320.43'),

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

@ -89,7 +89,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
currentValue: new Big('103.10483'),
currentValueInBaseCurrency: new Big('103.10483'),
errors: [],
grossPerformance: new Big('27.33'),
grossPerformancePercentage: new Big('0.3066651705565529623'),
@ -109,6 +109,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('89.12'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1'),
firstBuyDate: '2023-01-03',
grossPerformance: new Big('27.33'),
@ -132,7 +134,8 @@ describe('PortfolioCalculator', () => {
tags: undefined,
timeWeightedInvestment: new Big('89.12'),
timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'),
transactionCount: 1
transactionCount: 1,
valueInBaseCurrency: new Big('103.10483')
}
],
totalInvestment: new Big('89.12'),

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

@ -0,0 +1,118 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.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 { 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', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock;
})
};
}
);
describe('PortfolioCalculator', () => {
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
});
describe('get current positions', () => {
it.only('with MSFT buy', async () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'USD',
orders: [
{
currency: 'USD',
date: '2021-09-16',
dataSource: 'YAHOO',
fee: new Big(19),
name: 'Microsoft Inc.',
quantity: new Big(1),
symbol: 'MSFT',
type: 'BUY',
unitPrice: new Big(298.58)
},
{
currency: 'USD',
date: '2021-11-16',
dataSource: 'YAHOO',
fee: new Big(0),
name: 'Microsoft Inc.',
quantity: new Big(1),
symbol: 'MSFT',
type: 'DIVIDEND',
unitPrice: new Big(0.62)
}
]
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2023-07-10')
);
spy.mockRestore();
expect(currentPositions).toMatchObject({
errors: [],
hasErrors: false,
positions: [
{
averagePrice: new Big('298.58'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0.62'),
dividendInBaseCurrency: new Big('0.62'),
fee: new Big('19'),
firstBuyDate: '2021-09-16',
investment: new Big('298.58'),
investmentWithCurrencyEffect: new Big('298.58'),
marketPrice: 331.83,
marketPriceInBaseCurrency: 331.83,
quantity: new Big('1'),
symbol: 'MSFT',
tags: undefined,
transactionCount: 2
}
],
totalInvestment: new Big('298.58'),
totalInvestmentWithCurrencyEffect: new Big('298.58')
});
});
});
});

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

@ -64,7 +64,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
currentValue: new Big(0),
currentValueInBaseCurrency: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),

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

@ -87,7 +87,7 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
currentValue: new Big('87.8'),
currentValueInBaseCurrency: new Big('87.8'),
errors: [],
grossPerformance: new Big('21.93'),
grossPerformancePercentage: new Big('0.15113417083448194384'),
@ -107,6 +107,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('75.80'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4.25'),
firstBuyDate: '2022-03-07',
grossPerformance: new Big('21.93'),
@ -131,7 +133,8 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big(
'145.10285714285714285714'
),
transactionCount: 2
transactionCount: 2,
valueInBaseCurrency: new Big('87.8')
}
],
totalInvestment: new Big('75.80'),

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

@ -113,7 +113,7 @@ describe('PortfolioCalculator', () => {
});
expect(currentPositions).toEqual({
currentValue: new Big('0'),
currentValueInBaseCurrency: new Big('0'),
errors: [],
grossPerformance: new Big('19.86'),
grossPerformancePercentage: new Big('0.13100263852242744063'),
@ -133,6 +133,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('0'),
currency: 'CHF',
dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'),
firstBuyDate: '2022-03-07',
grossPerformance: new Big('19.86'),
@ -155,7 +157,8 @@ describe('PortfolioCalculator', () => {
symbol: 'NOVN.SW',
timeWeightedInvestment: new Big('151.6'),
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'),
transactionCount: 2
transactionCount: 2,
valueInBaseCurrency: new Big('0')
}
],
totalInvestment: new Big('0'),

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

@ -22,6 +22,7 @@ import {
format,
isBefore,
isSameDay,
max,
subDays
} from 'date-fns';
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
@ -70,6 +71,7 @@ export class PortfolioCalculator {
let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null;
for (const order of this.orders) {
const currentDate = order.date;
@ -77,33 +79,32 @@ export class PortfolioCalculator {
const oldAccumulatedSymbol = symbols[order.symbol];
const factor = getFactor(order.type);
const unitPrice = new Big(order.unitPrice);
if (oldAccumulatedSymbol) {
let investment = oldAccumulatedSymbol.investment;
const newQuantity = order.quantity
.mul(factor)
.plus(oldAccumulatedSymbol.quantity);
let investment = new Big(0);
if (newQuantity.gt(0)) {
if (order.type === 'BUY') {
investment = oldAccumulatedSymbol.investment.plus(
order.quantity.mul(unitPrice)
);
} else if (order.type === 'SELL') {
const averagePrice = oldAccumulatedSymbol.investment.div(
oldAccumulatedSymbol.quantity
);
investment = oldAccumulatedSymbol.investment.minus(
order.quantity.mul(averagePrice)
);
}
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,
@ -113,11 +114,13 @@ export class PortfolioCalculator {
};
} else {
currentTransactionPointItem = {
averagePrice: order.unitPrice,
currency: order.currency,
dataSource: order.dataSource,
dividend: new Big(0),
fee: order.fee,
firstBuyDate: order.date,
investment: unitPrice.mul(order.quantity).mul(factor),
investment: order.unitPrice.mul(order.quantity).mul(factor),
quantity: order.quantity.mul(factor),
symbol: order.symbol,
tags: order.tags,
@ -128,22 +131,28 @@ export class PortfolioCalculator {
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;
}
}
@ -441,16 +450,27 @@ export class PortfolioCalculator {
public async getCurrentPositions(
start: Date,
end = new Date(Date.now())
end?: Date
): Promise<CurrentPositions> {
const transactionPointsBeforeEndDate =
this.transactionPoints?.filter((transactionPoint) => {
return isBefore(parseDate(transactionPoint.date), end);
}) ?? [];
const lastTransactionPoint = last(this.transactionPoints);
if (!transactionPointsBeforeEndDate.length) {
let endDate = end;
if (!endDate) {
endDate = new Date(Date.now());
if (lastTransactionPoint) {
endDate = max([endDate, parseDate(lastTransactionPoint.date)]);
}
}
const transactionPoints = this.transactionPoints?.filter(({ date }) => {
return isBefore(parseDate(date), endDate);
});
if (!transactionPoints.length) {
return {
currentValue: new Big(0),
currentValueInBaseCurrency: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -465,41 +485,40 @@ export class PortfolioCalculator {
};
}
const lastTransactionPoint =
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
let dates: Date[] = [];
let firstIndex = transactionPointsBeforeEndDate.length;
let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null;
dates.push(resetHours(start));
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
for (const { currency, dataSource, symbol } of transactionPoints[
firstIndex - 1
].items) {
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
dataSource,
symbol
});
currencies[item.symbol] = item.currency;
currencies[symbol] = currency;
}
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
for (let i = 0; i < transactionPoints.length; i++) {
if (
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
!isBefore(parseDate(transactionPoints[i].date), start) &&
firstTransactionPoint === null
) {
firstTransactionPoint = transactionPointsBeforeEndDate[i];
firstTransactionPoint = transactionPoints[i];
firstIndex = i;
}
if (firstTransactionPoint !== null) {
dates.push(
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
);
dates.push(resetHours(parseDate(transactionPoints[i].date)));
}
}
dates.push(resetHours(end));
dates.push(resetHours(endDate));
// Add dates of last week for fallback
dates.push(subDays(resetHours(new Date()), 7));
@ -526,7 +545,7 @@ export class PortfolioCalculator {
let exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)),
endDate: endOfDay(end),
endDate: endOfDay(endDate),
startDate: parseDate(this.transactionPoints?.[0]?.date),
targetCurrency: this.currency
});
@ -562,7 +581,7 @@ export class PortfolioCalculator {
}
}
const endDateString = format(end, DATE_FORMAT);
const endDateString = format(endDate, DATE_FORMAT);
if (firstIndex > 0) {
firstIndex--;
@ -574,15 +593,17 @@ export class PortfolioCalculator {
const errors: ResponseError['errors'] = [];
for (const item of lastTransactionPoint.items) {
const marketPriceInBaseCurrency = marketSymbolMap[endDateString]?.[
item.symbol
]?.mul(
const marketPriceInBaseCurrency = (
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
).mul(
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
endDateString
]
);
const {
dividend,
dividendInBaseCurrency,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
@ -597,9 +618,9 @@ export class PortfolioCalculator {
totalInvestment,
totalInvestmentWithCurrencyEffect
} = this.getSymbolMetrics({
end,
marketSymbolMap,
start,
end: endDate,
exchangeRates:
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
symbol: item.symbol
@ -608,11 +629,11 @@ export class PortfolioCalculator {
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
positions.push({
dividend,
dividendInBaseCurrency,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
averagePrice: item.quantity.eq(0)
? new Big(0)
: item.investment.div(item.quantity),
averagePrice: item.averagePrice,
currency: item.currency,
dataSource: item.dataSource,
fee: item.fee,
@ -646,7 +667,10 @@ export class PortfolioCalculator {
quantity: item.quantity,
symbol: item.symbol,
tags: item.tags,
transactionCount: item.transactionCount
transactionCount: item.transactionCount,
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
item.quantity
)
});
if (
@ -715,7 +739,7 @@ export class PortfolioCalculator {
}
private calculateOverallPerformance(positions: TimelinePosition[]) {
let currentValue = new Big(0);
let currentValueInBaseCurrency = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0);
let hasErrors = false;
@ -727,14 +751,9 @@ export class PortfolioCalculator {
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions) {
if (
currentPosition.investment &&
currentPosition.marketPriceInBaseCurrency
) {
currentValue = currentValue.plus(
new Big(currentPosition.marketPriceInBaseCurrency).mul(
currentPosition.quantity
)
if (currentPosition.valueInBaseCurrency) {
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
currentPosition.valueInBaseCurrency
);
} else {
hasErrors = true;
@ -791,7 +810,7 @@ export class PortfolioCalculator {
}
return {
currentValue,
currentValueInBaseCurrency,
grossPerformance,
grossPerformanceWithCurrencyEffect,
hasErrors,
@ -842,6 +861,8 @@ export class PortfolioCalculator {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
let dividend = new Big(0);
let dividendInBaseCurrency = new Big(0);
let fees = new Big(0);
let feesAtStartDate = new Big(0);
let feesAtStartDateWithCurrencyEffect = new Big(0);
@ -894,6 +915,8 @@ export class PortfolioCalculator {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
dividend: new Big(0),
dividendInBaseCurrency: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -934,6 +957,8 @@ export class PortfolioCalculator {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
dividend: new Big(0),
dividendInBaseCurrency: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -1024,28 +1049,26 @@ export class PortfolioCalculator {
}
}
// Sort orders so that the start and end placeholder order are at the right
// Sort orders so that the start and end placeholder order are at the correct
// position
orders = sortBy(orders, (order) => {
let sortIndex = new Date(order.date);
if (order.itemType === 'start') {
sortIndex = addMilliseconds(sortIndex, -1);
}
orders = sortBy(orders, ({ date, itemType }) => {
let sortIndex = new Date(date);
if (order.itemType === 'end') {
if (itemType === 'end') {
sortIndex = addMilliseconds(sortIndex, 1);
} else if (itemType === 'start') {
sortIndex = addMilliseconds(sortIndex, -1);
}
return sortIndex.getTime();
});
const indexOfStartOrder = orders.findIndex((order) => {
return order.itemType === 'start';
const indexOfStartOrder = orders.findIndex(({ itemType }) => {
return itemType === 'start';
});
const indexOfEndOrder = orders.findIndex((order) => {
return order.itemType === 'end';
const indexOfEndOrder = orders.findIndex(({ itemType }) => {
return itemType === 'end';
});
let totalInvestmentDays = 0;
@ -1108,29 +1131,29 @@ export class PortfolioCalculator {
valueOfInvestmentBeforeTransactionWithCurrencyEffect;
}
const transactionInvestment =
order.type === 'BUY'
? order.quantity
.mul(order.unitPriceInBaseCurrency)
.mul(getFactor(order.type))
: totalUnits.gt(0)
? totalInvestment
.div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type))
: new Big(0);
const transactionInvestmentWithCurrencyEffect =
order.type === 'BUY'
? order.quantity
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(order.type))
: totalUnits.gt(0)
? totalInvestmentWithCurrencyEffect
.div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type))
: new Big(0);
let transactionInvestment = new Big(0);
let transactionInvestmentWithCurrencyEffect = new Big(0);
if (order.type === 'BUY') {
transactionInvestment = order.quantity
.mul(order.unitPriceInBaseCurrency)
.mul(getFactor(order.type));
transactionInvestmentWithCurrencyEffect = order.quantity
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(order.type));
} else if (order.type === 'SELL') {
if (totalUnits.gt(0)) {
transactionInvestment = totalInvestment
.div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type));
transactionInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect
.div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type));
}
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('totalInvestment', totalInvestment.toNumber());
@ -1186,6 +1209,13 @@ export class PortfolioCalculator {
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
if (order.type === 'DIVIDEND') {
dividend = dividend.plus(order.quantity.mul(order.unitPrice));
dividendInBaseCurrency = dividendInBaseCurrency.plus(
dividend.mul(exchangeRateAtOrderDate ?? 1)
);
}
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
@ -1277,7 +1307,7 @@ export class PortfolioCalculator {
grossPerformanceWithCurrencyEffect;
}
if (i > indexOfStartOrder) {
if (i > indexOfStartOrder && ['BUY', 'SELL'].includes(order.type)) {
// Only consider periods with an investment for the calculation of
// the time weighted investment
if (valueOfInvestmentBeforeTransaction.gt(0)) {
@ -1471,6 +1501,7 @@ export class PortfolioCalculator {
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
2
)}
Total dividend: ${dividend.toFixed(2)}
Gross performance: ${totalGrossPerformance.toFixed(
2
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
@ -1495,6 +1526,8 @@ export class PortfolioCalculator {
return {
currentValues,
currentValuesWithCurrencyEffect,
dividend,
dividendInBaseCurrency,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
initialValue,

22
apps/api/src/app/portfolio/portfolio.controller.ts

@ -1,4 +1,5 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import {
@ -11,6 +12,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import {
DEFAULT_CURRENCY,
HEADER_KEY_IMPERSONATION
@ -57,6 +59,8 @@ export class PortfolioController {
private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService,
private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
@ -161,7 +165,7 @@ export class PortfolioController {
'currentNetPerformance',
'currentNetPerformanceWithCurrencyEffect',
'currentValue',
'dividend',
'dividendInBaseCurrency',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
@ -231,11 +235,21 @@ export class PortfolioController {
filterByTags
});
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const { activities } = await this.orderService.getOrders({
filters,
userCurrency,
userId: impersonationUserId || this.request.user.id,
types: ['DIVIDEND']
});
let dividends = await this.portfolioService.getDividends({
activities,
dateRange,
filters,
groupBy,
impersonationId
groupBy
});
if (

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

@ -218,27 +218,14 @@ export class PortfolioService {
}
public async getDividends({
dateRange,
filters,
groupBy,
impersonationId
activities,
dateRange = 'max',
groupBy
}: {
dateRange: DateRange;
filters?: Filter[];
activities: Activity[];
dateRange?: DateRange;
groupBy?: GroupBy;
impersonationId: string;
}): Promise<InvestmentItem[]> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
const { activities } = await this.orderService.getOrders({
filters,
userCurrency,
userId,
types: ['DIVIDEND']
});
let dividends = activities.map(({ date, valueInBaseCurrency }) => {
return {
date: format(date, DATE_FORMAT),
@ -391,9 +378,10 @@ export class PortfolioService {
});
const holdings: PortfolioDetails['holdings'] = {};
const totalValueInBaseCurrency = currentPositions.currentValue.plus(
cashDetails.balanceInBaseCurrency
);
const totalValueInBaseCurrency =
currentPositions.currentValueInBaseCurrency.plus(
cashDetails.balanceInBaseCurrency
);
const isFilteredByAccount =
filters?.some((filter) => {
@ -402,7 +390,7 @@ export class PortfolioService {
let filteredValueInBaseCurrency = isFilteredByAccount
? totalValueInBaseCurrency
: currentPositions.currentValue;
: currentPositions.currentValueInBaseCurrency;
if (
filters?.length === 0 ||
@ -441,6 +429,7 @@ export class PortfolioService {
for (const {
currency,
dividend,
firstBuyDate,
grossPerformance,
grossPerformanceWithCurrencyEffect,
@ -456,14 +445,14 @@ export class PortfolioService {
quantity,
symbol,
tags,
transactionCount
transactionCount,
valueInBaseCurrency
} of currentPositions.positions) {
if (quantity.eq(0)) {
// Ignore positions without any quantity
continue;
}
const value = quantity.mul(marketPriceInBaseCurrency ?? 0);
const symbolProfile = symbolProfileMap[symbol];
const dataProviderResponse = dataProviderResponses[symbol];
@ -529,11 +518,11 @@ export class PortfolioService {
}
} else {
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
.plus(value)
.plus(valueInBaseCurrency)
.toNumber();
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
.plus(value)
.plus(valueInBaseCurrency)
.toNumber();
}
@ -547,12 +536,13 @@ export class PortfolioService {
transactionCount,
allocationInPercentage: filteredValueInBaseCurrency.eq(0)
? 0
: value.div(filteredValueInBaseCurrency).toNumber(),
: valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(),
assetClass: symbolProfile.assetClass,
assetSubClass: symbolProfile.assetSubClass,
countries: symbolProfile.countries,
dataSource: symbolProfile.dataSource,
dateOfFirstActivity: parseDate(firstBuyDate),
dividend: dividend?.toNumber() ?? 0,
grossPerformance: grossPerformance?.toNumber() ?? 0,
grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0,
grossPerformancePercentWithCurrencyEffect:
@ -571,7 +561,7 @@ export class PortfolioService {
quantity: quantity.toNumber(),
sectors: symbolProfile.sectors,
url: symbolProfile.url,
valueInBaseCurrency: value.toNumber()
valueInBaseCurrency: valueInBaseCurrency.toNumber()
};
}
@ -723,7 +713,7 @@ export class PortfolioService {
.filter((order) => {
tags = tags.concat(order.tags);
return ['BUY', 'ITEM', 'SELL'].includes(order.type);
return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
})
.map((order) => ({
currency: order.SymbolProfile.currency,
@ -764,6 +754,7 @@ export class PortfolioService {
averagePrice,
currency,
dataSource,
dividendInBaseCurrency,
fee,
firstBuyDate,
marketPrice,
@ -780,16 +771,6 @@ export class PortfolioService {
return Account;
});
const dividendInBaseCurrency = getSum(
orders
.filter(({ type }) => {
return type === 'DIVIDEND';
})
.map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency);
})
);
const historicalData = await this.dataProviderService.getHistorical(
[{ dataSource, symbol: aSymbol }],
'day',
@ -823,9 +804,7 @@ export class PortfolioService {
);
if (currentSymbol) {
currentAveragePrice = currentSymbol.quantity.eq(0)
? 0
: currentSymbol.investment.div(currentSymbol.quantity).toNumber();
currentAveragePrice = currentSymbol.averagePrice.toNumber();
currentQuantity = currentSymbol.quantity.toNumber();
}
@ -1197,7 +1176,7 @@ export class PortfolioService {
const startDate = this.getStartDate(dateRange, portfolioStart);
const {
currentValue,
currentValueInBaseCurrency,
errors,
grossPerformance,
grossPerformancePercentage,
@ -1292,7 +1271,7 @@ export class PortfolioService {
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
currentNetPerformanceWithCurrencyEffect:
currentNetPerformanceWithCurrencyEffect.toNumber(),
currentValue: currentValue.toNumber(),
currentValue: currentValueInBaseCurrency.toNumber(),
totalInvestment: totalInvestment.toNumber()
}
};
@ -1636,6 +1615,7 @@ export class PortfolioService {
countries: [],
dataSource: undefined,
dateOfFirstActivity: undefined,
dividend: 0,
grossPerformance: 0,
grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0,
@ -1765,11 +1745,16 @@ export class PortfolioService {
}
}
const dividend = this.getSumOfActivityType({
activities,
userCurrency,
activityType: 'DIVIDEND'
}).toNumber();
const dividendInBaseCurrency = (
await this.getDividends({
activities: activities.filter(({ type }) => {
return type === 'DIVIDEND';
})
})
).reduce(
(previous, current) => new Big(previous).plus(current.investment),
new Big(0)
);
const emergencyFund = new Big(
Math.max(
@ -1904,7 +1889,6 @@ export class PortfolioService {
annualizedPerformancePercent,
annualizedPerformancePercentWithCurrencyEffect,
cash,
dividend,
excludedAccountsAndActivities,
fees,
firstOrderDate,
@ -1915,6 +1899,7 @@ export class PortfolioService {
totalBuy,
totalSell,
committedFunds: committedFunds.toNumber(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
emergencyFund: {
assets: emergencyFundPositionsValueInBaseCurrency,
cash: emergencyFund
@ -1967,7 +1952,7 @@ export class PortfolioService {
private async getTransactionPoints({
filters,
includeDrafts = false,
types = ['BUY', 'ITEM', 'LIABILITY', 'SELL'],
types = ['BUY', 'DIVIDEND', 'ITEM', 'LIABILITY', 'SELL'],
userId,
withExcludedAccounts = false
}: {
@ -2144,14 +2129,11 @@ export class PortfolioService {
type
} of ordersByAccount) {
let currentValueOfSymbolInBaseCurrency =
getFactor(type) *
quantity *
(portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ??
0);
if (['LIABILITY', 'SELL'].includes(type)) {
currentValueOfSymbolInBaseCurrency *= getFactor(type);
}
if (accounts[Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
accounts[Account?.id || UNKNOWN_KEY].valueInBaseCurrency +=
currentValueOfSymbolInBaseCurrency;

7
apps/api/src/services/configuration/configuration.service.ts

@ -3,7 +3,7 @@ import { DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
import { bool, cleanEnv, host, json, num, port, str, url } from 'envalid';
@Injectable()
export class ConfigurationService {
@ -48,14 +48,13 @@ export class ConfigurationService {
REDIS_PASSWORD: str({ default: '' }),
REDIS_PORT: port({ default: 6379 }),
REQUEST_TIMEOUT: num({ default: 2000 }),
ROOT_URL: str({ default: DEFAULT_ROOT_URL }),
ROOT_URL: url({ default: DEFAULT_ROOT_URL }),
STRIPE_PUBLIC_KEY: str({ default: '' }),
STRIPE_SECRET_KEY: str({ default: '' }),
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),
TWITTER_ACCESS_TOKEN_SECRET: str({ default: 'dummyAccessTokenSecret' }),
TWITTER_API_KEY: str({ default: 'dummyApiKey' }),
TWITTER_API_SECRET: str({ default: 'dummyApiSecret' }),
WEB_AUTH_RP_ID: host({ default: 'localhost' })
TWITTER_API_SECRET: str({ default: 'dummyApiSecret' })
});
}

8
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts

@ -22,6 +22,14 @@ export const ExchangeRateDataServiceMock = {
'2023-07-10': 0.8854
}
});
} else if (targetCurrency === 'USD') {
return Promise.resolve({
USDUSD: {
'2018-01-01': 1,
'2021-11-16': 1,
'2023-07-10': 1
}
});
}
return Promise.resolve({});

22
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -73,7 +73,17 @@ export class ExchangeRateDataService {
currencyTo: targetCurrency
});
let previousExchangeRate = 1;
const dateStrings = Object.keys(
exchangeRatesByCurrency[`${currency}${targetCurrency}`]
);
const lastDateString = dateStrings.reduce((a, b) => {
return a > b ? a : b;
});
let previousExchangeRate =
exchangeRatesByCurrency[`${currency}${targetCurrency}`]?.[
lastDateString
] ?? 1;
// Start from the most recent date and fill in missing exchange rates
// using the latest available rate
@ -94,7 +104,7 @@ export class ExchangeRateDataService {
exchangeRatesByCurrency[`${currency}${targetCurrency}`][dateString] =
previousExchangeRate;
if (currency === DEFAULT_CURRENCY) {
if (currency === DEFAULT_CURRENCY && isBefore(date, new Date())) {
Logger.error(
`No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`,
'ExchangeRateDataService'
@ -433,13 +443,17 @@ export class ExchangeRateDataService {
]) *
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)];
factors[format(date, DATE_FORMAT)] = factor;
if (isNaN(factor)) {
throw new Error('Exchange rate is not a number');
} else {
factors[format(date, DATE_FORMAT)] = factor;
}
} catch {
Logger.error(
`No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format(
date,
DATE_FORMAT
)}`,
)}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom} and ${DEFAULT_CURRENCY}${currencyTo}.`,
'ExchangeRateDataService'
);
}

1
apps/api/src/services/interfaces/environment.interface.ts

@ -42,5 +42,4 @@ export interface Environment extends CleanedEnvAccessors {
TWITTER_ACCESS_TOKEN_SECRET: string;
TWITTER_API_KEY: string;
TWITTER_API_SECRET: string;
WEB_AUTH_RP_ID: string;
}

20
apps/client/src/app/components/admin-users/admin-users.html

@ -35,12 +35,20 @@
mat-cell
>
<div class="d-flex align-items-center">
<span class="d-none d-sm-inline-block text-monospace">{{
element.id
}}</span>
<span class="d-inline-block d-sm-none text-monospace">{{
(element.id | slice: 0 : 5) + '...'
}}</span>
<span
class="d-none d-sm-inline-block text-monospace"
[ngClass]="{
'text-line-through': element.role === 'INACTIVE'
}"
>{{ element.id }}</span
>
<span
class="d-inline-block d-sm-none text-monospace"
[ngClass]="{
'text-line-through': element.role === 'INACTIVE'
}"
>{{ (element.id | slice: 0 : 5) + '...' }}</span
>
<gf-premium-indicator
*ngIf="element?.subscription?.type === 'Premium'"
class="ml-1"

2
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -328,7 +328,7 @@
[isCurrency]="true"
[locale]="locale"
[unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.dividend"
[value]="isLoading ? undefined : summary?.dividendInBaseCurrency"
/>
</div>
</div>

4
apps/client/src/styles.scss

@ -587,6 +587,10 @@ ngx-skeleton-loader {
text-decoration: underline !important;
}
.text-line-through {
text-decoration: line-through;
}
.with-placeholder-as-option {
.mat-mdc-select-placeholder {
color: rgba(var(--dark-primary-text));

3
libs/common/src/lib/interfaces/admin-data.interface.ts

@ -1,3 +1,5 @@
import { Role } from '@prisma/client';
import { UniqueAsset } from './unique-asset.interface';
export interface AdminData {
@ -16,6 +18,7 @@ export interface AdminData {
engagement: number;
id: string;
lastActivity: Date;
role: Role;
transactionCount: number;
}[];
version: string;

1
libs/common/src/lib/interfaces/portfolio-position.interface.ts

@ -14,6 +14,7 @@ export interface PortfolioPosition {
currency: string;
dataSource: DataSource;
dateOfFirstActivity: Date;
dividend: number;
exchange?: string;
grossPerformance: number;
grossPerformancePercent: number;

2
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -5,7 +5,7 @@ export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercentWithCurrencyEffect: number;
cash: number;
committedFunds: number;
dividend: number;
dividendInBaseCurrency: number;
emergencyFund: {
assets: number;
cash: number;

2
libs/common/src/lib/interfaces/symbol-metrics.interface.ts

@ -7,6 +7,8 @@ export interface SymbolMetrics {
currentValuesWithCurrencyEffect: {
[date: string]: Big;
};
dividend: Big;
dividendInBaseCurrency: Big;
grossPerformance: Big;
grossPerformancePercentage: Big;
grossPerformancePercentageWithCurrencyEffect: Big;

3
libs/common/src/lib/interfaces/timeline-position.interface.ts

@ -5,6 +5,8 @@ export interface TimelinePosition {
averagePrice: Big;
currency: string;
dataSource: DataSource;
dividend: Big;
dividendInBaseCurrency: Big;
fee: Big;
firstBuyDate: string;
grossPerformance: Big;
@ -25,4 +27,5 @@ export interface TimelinePosition {
timeWeightedInvestment: Big;
timeWeightedInvestmentWithCurrencyEffect: Big;
transactionCount: number;
valueInBaseCurrency: Big;
}

Loading…
Cancel
Save