Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
e79d607ab8 | |||
5f7d083f7c | |||
15857118fe | |||
ff91ed21df | |||
9241c04d5a | |||
5d4e2fba8c | |||
6c57609db8 |
18
CHANGELOG.md
18
CHANGELOG.md
@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.73.0 - 2024-04-17
|
||||
|
||||
### Added
|
||||
|
||||
- Added a form validation against the DTO in the create or update account dialog
|
||||
- Added a form validation against the DTO in the create or update activity dialog
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved the dividend calculations into the portfolio calculator
|
||||
- Moved the fee calculations into the portfolio calculator
|
||||
- Moved the interest calculations into the portfolio calculator
|
||||
- Moved the liability calculations into the portfolio calculator
|
||||
- Moved the (wealth) item calculations into the portfolio calculator
|
||||
- Let queue jobs for asset profile data gathering fail by throwing an error
|
||||
- Let queue jobs for historical market data gathering fail by throwing an error
|
||||
- Upgraded `yahoo-finance2` from version `2.11.1` to `2.11.2`
|
||||
|
||||
## 2.72.0 - 2024-04-13
|
||||
|
||||
### Added
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface';
|
||||
import {
|
||||
SymbolMetrics,
|
||||
TimelinePosition,
|
||||
@ -9,7 +9,7 @@ import {
|
||||
export class MWRPortfolioCalculator extends PortfolioCalculator {
|
||||
protected calculateOverallPerformance(
|
||||
positions: TimelinePosition[]
|
||||
): CurrentPositions {
|
||||
): PortfolioSnapshot {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@ -23,17 +24,20 @@ export class PortfolioCalculatorFactory {
|
||||
public createCalculator({
|
||||
activities,
|
||||
calculationType,
|
||||
currency
|
||||
currency,
|
||||
dateRange = 'max'
|
||||
}: {
|
||||
activities: Activity[];
|
||||
calculationType: PerformanceCalculationType;
|
||||
currency: string;
|
||||
dateRange?: DateRange;
|
||||
}): PortfolioCalculator {
|
||||
switch (calculationType) {
|
||||
case PerformanceCalculationType.MWR:
|
||||
return new MWRPortfolioCalculator({
|
||||
activities,
|
||||
currency,
|
||||
dateRange,
|
||||
currentRateService: this.currentRateService,
|
||||
exchangeRateDataService: this.exchangeRateDataService
|
||||
});
|
||||
@ -42,6 +46,7 @@ export class PortfolioCalculatorFactory {
|
||||
activities,
|
||||
currency,
|
||||
currentRateService: this.currentRateService,
|
||||
dateRange,
|
||||
exchangeRateDataService: this.exchangeRateDataService
|
||||
});
|
||||
default:
|
||||
|
@ -1,7 +1,7 @@
|
||||
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 { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
||||
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.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 {
|
||||
@ -11,7 +11,12 @@ import {
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { MAX_CHART_ITEMS } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getSum,
|
||||
parseDate,
|
||||
resetHours
|
||||
} from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderInfo,
|
||||
HistoricalDataItem,
|
||||
@ -44,18 +49,24 @@ export abstract class PortfolioCalculator {
|
||||
private currency: string;
|
||||
private currentRateService: CurrentRateService;
|
||||
private dataProviderInfos: DataProviderInfo[];
|
||||
private endDate: Date;
|
||||
private exchangeRateDataService: ExchangeRateDataService;
|
||||
private snapshot: PortfolioSnapshot;
|
||||
private snapshotPromise: Promise<void>;
|
||||
private startDate: Date;
|
||||
private transactionPoints: TransactionPoint[];
|
||||
|
||||
public constructor({
|
||||
activities,
|
||||
currency,
|
||||
currentRateService,
|
||||
dateRange,
|
||||
exchangeRateDataService
|
||||
}: {
|
||||
activities: Activity[];
|
||||
currency: string;
|
||||
currentRateService: CurrentRateService;
|
||||
dateRange: DateRange;
|
||||
exchangeRateDataService: ExchangeRateDataService;
|
||||
}) {
|
||||
this.currency = currency;
|
||||
@ -79,12 +90,291 @@ export abstract class PortfolioCalculator {
|
||||
return a.date?.localeCompare(b.date);
|
||||
});
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
|
||||
this.endDate = endDate;
|
||||
this.startDate = startDate;
|
||||
|
||||
this.computeTransactionPoints();
|
||||
|
||||
this.snapshotPromise = this.initialize();
|
||||
}
|
||||
|
||||
protected abstract calculateOverallPerformance(
|
||||
positions: TimelinePosition[]
|
||||
): CurrentPositions;
|
||||
): PortfolioSnapshot;
|
||||
|
||||
public async computeSnapshot(
|
||||
start: Date,
|
||||
end?: Date
|
||||
): Promise<PortfolioSnapshot> {
|
||||
const lastTransactionPoint = last(this.transactionPoints);
|
||||
|
||||
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 {
|
||||
currentValueInBaseCurrency: new Big(0),
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||
grossPerformanceWithCurrencyEffect: new Big(0),
|
||||
hasErrors: false,
|
||||
netPerformance: new Big(0),
|
||||
netPerformancePercentage: new Big(0),
|
||||
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||
netPerformanceWithCurrencyEffect: new Big(0),
|
||||
positions: [],
|
||||
totalFeesWithCurrencyEffect: new Big(0),
|
||||
totalInterestWithCurrencyEffect: new Big(0),
|
||||
totalInvestment: new Big(0),
|
||||
totalInvestmentWithCurrencyEffect: new Big(0),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big(0),
|
||||
totalValuablesWithCurrencyEffect: new Big(0)
|
||||
};
|
||||
}
|
||||
|
||||
const currencies: { [symbol: string]: string } = {};
|
||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||
let dates: Date[] = [];
|
||||
let firstIndex = transactionPoints.length;
|
||||
let firstTransactionPoint: TransactionPoint = null;
|
||||
let totalInterestWithCurrencyEffect = new Big(0);
|
||||
let totalLiabilitiesWithCurrencyEffect = new Big(0);
|
||||
let totalValuablesWithCurrencyEffect = new Big(0);
|
||||
|
||||
dates.push(resetHours(start));
|
||||
|
||||
for (const { currency, dataSource, symbol } of transactionPoints[
|
||||
firstIndex - 1
|
||||
].items) {
|
||||
dataGatheringItems.push({
|
||||
dataSource,
|
||||
symbol
|
||||
});
|
||||
|
||||
currencies[symbol] = currency;
|
||||
}
|
||||
|
||||
for (let i = 0; i < transactionPoints.length; i++) {
|
||||
if (
|
||||
!isBefore(parseDate(transactionPoints[i].date), start) &&
|
||||
firstTransactionPoint === null
|
||||
) {
|
||||
firstTransactionPoint = transactionPoints[i];
|
||||
firstIndex = i;
|
||||
}
|
||||
|
||||
if (firstTransactionPoint !== null) {
|
||||
dates.push(resetHours(parseDate(transactionPoints[i].date)));
|
||||
}
|
||||
}
|
||||
|
||||
dates.push(resetHours(endDate));
|
||||
|
||||
// Add dates of last week for fallback
|
||||
dates.push(subDays(resetHours(new Date()), 7));
|
||||
dates.push(subDays(resetHours(new Date()), 6));
|
||||
dates.push(subDays(resetHours(new Date()), 5));
|
||||
dates.push(subDays(resetHours(new Date()), 4));
|
||||
dates.push(subDays(resetHours(new Date()), 3));
|
||||
dates.push(subDays(resetHours(new Date()), 2));
|
||||
dates.push(subDays(resetHours(new Date()), 1));
|
||||
dates.push(resetHours(new Date()));
|
||||
|
||||
dates = uniq(
|
||||
dates.map((date) => {
|
||||
return date.getTime();
|
||||
})
|
||||
)
|
||||
.map((timestamp) => {
|
||||
return new Date(timestamp);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.getTime() - b.getTime();
|
||||
});
|
||||
|
||||
let exchangeRatesByCurrency =
|
||||
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
||||
currencies: uniq(Object.values(currencies)),
|
||||
endDate: endOfDay(endDate),
|
||||
startDate: this.getStartDate(),
|
||||
targetCurrency: this.currency
|
||||
});
|
||||
|
||||
const {
|
||||
dataProviderInfos,
|
||||
errors: currentRateErrors,
|
||||
values: marketSymbols
|
||||
} = await this.currentRateService.getValues({
|
||||
dataGatheringItems,
|
||||
dateQuery: {
|
||||
in: dates
|
||||
}
|
||||
});
|
||||
|
||||
this.dataProviderInfos = dataProviderInfos;
|
||||
|
||||
const marketSymbolMap: {
|
||||
[date: string]: { [symbol: string]: Big };
|
||||
} = {};
|
||||
|
||||
for (const marketSymbol of marketSymbols) {
|
||||
const date = format(marketSymbol.date, DATE_FORMAT);
|
||||
|
||||
if (!marketSymbolMap[date]) {
|
||||
marketSymbolMap[date] = {};
|
||||
}
|
||||
|
||||
if (marketSymbol.marketPrice) {
|
||||
marketSymbolMap[date][marketSymbol.symbol] = new Big(
|
||||
marketSymbol.marketPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endDateString = format(endDate, DATE_FORMAT);
|
||||
|
||||
if (firstIndex > 0) {
|
||||
firstIndex--;
|
||||
}
|
||||
|
||||
const positions: TimelinePosition[] = [];
|
||||
let hasAnySymbolMetricsErrors = false;
|
||||
|
||||
const errors: ResponseError['errors'] = [];
|
||||
|
||||
for (const item of lastTransactionPoint.items) {
|
||||
const marketPriceInBaseCurrency = (
|
||||
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
|
||||
).mul(
|
||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
|
||||
endDateString
|
||||
]
|
||||
);
|
||||
|
||||
const {
|
||||
grossPerformance,
|
||||
grossPerformancePercentage,
|
||||
grossPerformancePercentageWithCurrencyEffect,
|
||||
grossPerformanceWithCurrencyEffect,
|
||||
hasErrors,
|
||||
netPerformance,
|
||||
netPerformancePercentage,
|
||||
netPerformancePercentageWithCurrencyEffect,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
timeWeightedInvestment,
|
||||
timeWeightedInvestmentWithCurrencyEffect,
|
||||
totalDividend,
|
||||
totalDividendInBaseCurrency,
|
||||
totalInterestInBaseCurrency,
|
||||
totalInvestment,
|
||||
totalInvestmentWithCurrencyEffect,
|
||||
totalLiabilitiesInBaseCurrency,
|
||||
totalValuablesInBaseCurrency
|
||||
} = this.getSymbolMetrics({
|
||||
marketSymbolMap,
|
||||
start,
|
||||
dataSource: item.dataSource,
|
||||
end: endDate,
|
||||
exchangeRates:
|
||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
||||
symbol: item.symbol
|
||||
});
|
||||
|
||||
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
||||
|
||||
positions.push({
|
||||
dividend: totalDividend,
|
||||
dividendInBaseCurrency: totalDividendInBaseCurrency,
|
||||
timeWeightedInvestment,
|
||||
timeWeightedInvestmentWithCurrencyEffect,
|
||||
averagePrice: item.averagePrice,
|
||||
currency: item.currency,
|
||||
dataSource: item.dataSource,
|
||||
fee: item.fee,
|
||||
firstBuyDate: item.firstBuyDate,
|
||||
grossPerformance: !hasErrors ? grossPerformance ?? null : null,
|
||||
grossPerformancePercentage: !hasErrors
|
||||
? grossPerformancePercentage ?? null
|
||||
: null,
|
||||
grossPerformancePercentageWithCurrencyEffect: !hasErrors
|
||||
? grossPerformancePercentageWithCurrencyEffect ?? null
|
||||
: null,
|
||||
grossPerformanceWithCurrencyEffect: !hasErrors
|
||||
? grossPerformanceWithCurrencyEffect ?? null
|
||||
: null,
|
||||
investment: totalInvestment,
|
||||
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
|
||||
marketPrice:
|
||||
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
|
||||
marketPriceInBaseCurrency:
|
||||
marketPriceInBaseCurrency?.toNumber() ?? null,
|
||||
netPerformance: !hasErrors ? netPerformance ?? null : null,
|
||||
netPerformancePercentage: !hasErrors
|
||||
? netPerformancePercentage ?? null
|
||||
: null,
|
||||
netPerformancePercentageWithCurrencyEffect: !hasErrors
|
||||
? netPerformancePercentageWithCurrencyEffect ?? null
|
||||
: null,
|
||||
netPerformanceWithCurrencyEffect: !hasErrors
|
||||
? netPerformanceWithCurrencyEffect ?? null
|
||||
: null,
|
||||
quantity: item.quantity,
|
||||
symbol: item.symbol,
|
||||
tags: item.tags,
|
||||
transactionCount: item.transactionCount,
|
||||
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
|
||||
item.quantity
|
||||
)
|
||||
});
|
||||
|
||||
totalInterestWithCurrencyEffect = totalInterestWithCurrencyEffect.plus(
|
||||
totalInterestInBaseCurrency
|
||||
);
|
||||
|
||||
totalLiabilitiesWithCurrencyEffect =
|
||||
totalLiabilitiesWithCurrencyEffect.plus(totalLiabilitiesInBaseCurrency);
|
||||
|
||||
totalValuablesWithCurrencyEffect = totalValuablesWithCurrencyEffect.plus(
|
||||
totalValuablesInBaseCurrency
|
||||
);
|
||||
|
||||
if (
|
||||
(hasErrors ||
|
||||
currentRateErrors.find(({ dataSource, symbol }) => {
|
||||
return dataSource === item.dataSource && symbol === item.symbol;
|
||||
})) &&
|
||||
item.investment.gt(0)
|
||||
) {
|
||||
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
||||
}
|
||||
}
|
||||
|
||||
const overall = this.calculateOverallPerformance(positions);
|
||||
|
||||
return {
|
||||
...overall,
|
||||
errors,
|
||||
positions,
|
||||
totalInterestWithCurrencyEffect,
|
||||
totalLiabilitiesWithCurrencyEffect,
|
||||
totalValuablesWithCurrencyEffect,
|
||||
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
||||
};
|
||||
}
|
||||
|
||||
public async getChart({
|
||||
dateRange = 'max',
|
||||
@ -380,258 +670,32 @@ export abstract class PortfolioCalculator {
|
||||
});
|
||||
}
|
||||
|
||||
public async getCurrentPositions(
|
||||
start: Date,
|
||||
end?: Date
|
||||
): Promise<CurrentPositions> {
|
||||
const lastTransactionPoint = last(this.transactionPoints);
|
||||
|
||||
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 {
|
||||
currentValueInBaseCurrency: new Big(0),
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||
grossPerformanceWithCurrencyEffect: new Big(0),
|
||||
hasErrors: false,
|
||||
netPerformance: new Big(0),
|
||||
netPerformancePercentage: new Big(0),
|
||||
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||
netPerformanceWithCurrencyEffect: new Big(0),
|
||||
positions: [],
|
||||
totalInvestment: new Big(0),
|
||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||
};
|
||||
}
|
||||
|
||||
const currencies: { [symbol: string]: string } = {};
|
||||
const dataGatheringItems: IDataGatheringItem[] = [];
|
||||
let dates: Date[] = [];
|
||||
let firstIndex = transactionPoints.length;
|
||||
let firstTransactionPoint: TransactionPoint = null;
|
||||
|
||||
dates.push(resetHours(start));
|
||||
|
||||
for (const { currency, dataSource, symbol } of transactionPoints[
|
||||
firstIndex - 1
|
||||
].items) {
|
||||
dataGatheringItems.push({
|
||||
dataSource,
|
||||
symbol
|
||||
});
|
||||
|
||||
currencies[symbol] = currency;
|
||||
}
|
||||
|
||||
for (let i = 0; i < transactionPoints.length; i++) {
|
||||
if (
|
||||
!isBefore(parseDate(transactionPoints[i].date), start) &&
|
||||
firstTransactionPoint === null
|
||||
) {
|
||||
firstTransactionPoint = transactionPoints[i];
|
||||
firstIndex = i;
|
||||
}
|
||||
|
||||
if (firstTransactionPoint !== null) {
|
||||
dates.push(resetHours(parseDate(transactionPoints[i].date)));
|
||||
}
|
||||
}
|
||||
|
||||
dates.push(resetHours(endDate));
|
||||
|
||||
// Add dates of last week for fallback
|
||||
dates.push(subDays(resetHours(new Date()), 7));
|
||||
dates.push(subDays(resetHours(new Date()), 6));
|
||||
dates.push(subDays(resetHours(new Date()), 5));
|
||||
dates.push(subDays(resetHours(new Date()), 4));
|
||||
dates.push(subDays(resetHours(new Date()), 3));
|
||||
dates.push(subDays(resetHours(new Date()), 2));
|
||||
dates.push(subDays(resetHours(new Date()), 1));
|
||||
dates.push(resetHours(new Date()));
|
||||
|
||||
dates = uniq(
|
||||
dates.map((date) => {
|
||||
return date.getTime();
|
||||
})
|
||||
)
|
||||
.map((timestamp) => {
|
||||
return new Date(timestamp);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.getTime() - b.getTime();
|
||||
});
|
||||
|
||||
let exchangeRatesByCurrency =
|
||||
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
||||
currencies: uniq(Object.values(currencies)),
|
||||
endDate: endOfDay(endDate),
|
||||
startDate: this.getStartDate(),
|
||||
targetCurrency: this.currency
|
||||
});
|
||||
|
||||
const {
|
||||
dataProviderInfos,
|
||||
errors: currentRateErrors,
|
||||
values: marketSymbols
|
||||
} = await this.currentRateService.getValues({
|
||||
dataGatheringItems,
|
||||
dateQuery: {
|
||||
in: dates
|
||||
}
|
||||
});
|
||||
|
||||
this.dataProviderInfos = dataProviderInfos;
|
||||
|
||||
const marketSymbolMap: {
|
||||
[date: string]: { [symbol: string]: Big };
|
||||
} = {};
|
||||
|
||||
for (const marketSymbol of marketSymbols) {
|
||||
const date = format(marketSymbol.date, DATE_FORMAT);
|
||||
|
||||
if (!marketSymbolMap[date]) {
|
||||
marketSymbolMap[date] = {};
|
||||
}
|
||||
|
||||
if (marketSymbol.marketPrice) {
|
||||
marketSymbolMap[date][marketSymbol.symbol] = new Big(
|
||||
marketSymbol.marketPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const endDateString = format(endDate, DATE_FORMAT);
|
||||
|
||||
if (firstIndex > 0) {
|
||||
firstIndex--;
|
||||
}
|
||||
|
||||
const positions: TimelinePosition[] = [];
|
||||
let hasAnySymbolMetricsErrors = false;
|
||||
|
||||
const errors: ResponseError['errors'] = [];
|
||||
|
||||
for (const item of lastTransactionPoint.items) {
|
||||
const marketPriceInBaseCurrency = (
|
||||
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
|
||||
).mul(
|
||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
|
||||
endDateString
|
||||
]
|
||||
);
|
||||
|
||||
const {
|
||||
grossPerformance,
|
||||
grossPerformancePercentage,
|
||||
grossPerformancePercentageWithCurrencyEffect,
|
||||
grossPerformanceWithCurrencyEffect,
|
||||
hasErrors,
|
||||
netPerformance,
|
||||
netPerformancePercentage,
|
||||
netPerformancePercentageWithCurrencyEffect,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
timeWeightedInvestment,
|
||||
timeWeightedInvestmentWithCurrencyEffect,
|
||||
totalDividend,
|
||||
totalDividendInBaseCurrency,
|
||||
totalInvestment,
|
||||
totalInvestmentWithCurrencyEffect
|
||||
} = this.getSymbolMetrics({
|
||||
marketSymbolMap,
|
||||
start,
|
||||
dataSource: item.dataSource,
|
||||
end: endDate,
|
||||
exchangeRates:
|
||||
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
|
||||
symbol: item.symbol
|
||||
});
|
||||
|
||||
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
||||
|
||||
positions.push({
|
||||
dividend: totalDividend,
|
||||
dividendInBaseCurrency: totalDividendInBaseCurrency,
|
||||
timeWeightedInvestment,
|
||||
timeWeightedInvestmentWithCurrencyEffect,
|
||||
averagePrice: item.averagePrice,
|
||||
currency: item.currency,
|
||||
dataSource: item.dataSource,
|
||||
fee: item.fee,
|
||||
firstBuyDate: item.firstBuyDate,
|
||||
grossPerformance: !hasErrors ? grossPerformance ?? null : null,
|
||||
grossPerformancePercentage: !hasErrors
|
||||
? grossPerformancePercentage ?? null
|
||||
: null,
|
||||
grossPerformancePercentageWithCurrencyEffect: !hasErrors
|
||||
? grossPerformancePercentageWithCurrencyEffect ?? null
|
||||
: null,
|
||||
grossPerformanceWithCurrencyEffect: !hasErrors
|
||||
? grossPerformanceWithCurrencyEffect ?? null
|
||||
: null,
|
||||
investment: totalInvestment,
|
||||
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
|
||||
marketPrice:
|
||||
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
|
||||
marketPriceInBaseCurrency:
|
||||
marketPriceInBaseCurrency?.toNumber() ?? null,
|
||||
netPerformance: !hasErrors ? netPerformance ?? null : null,
|
||||
netPerformancePercentage: !hasErrors
|
||||
? netPerformancePercentage ?? null
|
||||
: null,
|
||||
netPerformancePercentageWithCurrencyEffect: !hasErrors
|
||||
? netPerformancePercentageWithCurrencyEffect ?? null
|
||||
: null,
|
||||
netPerformanceWithCurrencyEffect: !hasErrors
|
||||
? netPerformanceWithCurrencyEffect ?? null
|
||||
: null,
|
||||
quantity: item.quantity,
|
||||
symbol: item.symbol,
|
||||
tags: item.tags,
|
||||
transactionCount: item.transactionCount,
|
||||
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
|
||||
item.quantity
|
||||
)
|
||||
});
|
||||
|
||||
if (
|
||||
(hasErrors ||
|
||||
currentRateErrors.find(({ dataSource, symbol }) => {
|
||||
return dataSource === item.dataSource && symbol === item.symbol;
|
||||
})) &&
|
||||
item.investment.gt(0)
|
||||
) {
|
||||
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
||||
}
|
||||
}
|
||||
|
||||
const overall = this.calculateOverallPerformance(positions);
|
||||
|
||||
return {
|
||||
...overall,
|
||||
errors,
|
||||
positions,
|
||||
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
||||
};
|
||||
}
|
||||
|
||||
public getDataProviderInfos() {
|
||||
return this.dataProviderInfos;
|
||||
}
|
||||
|
||||
public async getDividendInBaseCurrency() {
|
||||
await this.snapshotPromise;
|
||||
|
||||
return getSum(
|
||||
this.snapshot.positions.map(({ dividendInBaseCurrency }) => {
|
||||
return dividendInBaseCurrency;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async getFeesInBaseCurrency() {
|
||||
await this.snapshotPromise;
|
||||
|
||||
return this.snapshot.totalFeesWithCurrencyEffect;
|
||||
}
|
||||
|
||||
public async getInterestInBaseCurrency() {
|
||||
await this.snapshotPromise;
|
||||
|
||||
return this.snapshot.totalInterestWithCurrencyEffect;
|
||||
}
|
||||
|
||||
public getInvestments(): { date: string; investment: Big }[] {
|
||||
if (this.transactionPoints.length === 0) {
|
||||
return [];
|
||||
@ -672,6 +736,18 @@ export abstract class PortfolioCalculator {
|
||||
}));
|
||||
}
|
||||
|
||||
public async getLiabilitiesInBaseCurrency() {
|
||||
await this.snapshotPromise;
|
||||
|
||||
return this.snapshot.totalLiabilitiesWithCurrencyEffect;
|
||||
}
|
||||
|
||||
public async getSnapshot() {
|
||||
await this.snapshotPromise;
|
||||
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
public getStartDate() {
|
||||
return this.transactionPoints.length > 0
|
||||
? parseDate(this.transactionPoints[0].date)
|
||||
@ -702,6 +778,12 @@ export abstract class PortfolioCalculator {
|
||||
return this.transactionPoints;
|
||||
}
|
||||
|
||||
public async getValuablesInBaseCurrency() {
|
||||
await this.snapshotPromise;
|
||||
|
||||
return this.snapshot.totalValuablesWithCurrencyEffect;
|
||||
}
|
||||
|
||||
private computeTransactionPoints() {
|
||||
this.transactionPoints = [];
|
||||
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
|
||||
@ -790,18 +872,57 @@ export abstract class PortfolioCalculator {
|
||||
return a.symbol?.localeCompare(b.symbol);
|
||||
});
|
||||
|
||||
let fees = new Big(0);
|
||||
|
||||
if (type === 'FEE') {
|
||||
fees = fee;
|
||||
}
|
||||
|
||||
let interest = new Big(0);
|
||||
|
||||
if (type === 'INTEREST') {
|
||||
interest = quantity.mul(unitPrice);
|
||||
}
|
||||
|
||||
let liabilities = new Big(0);
|
||||
|
||||
if (type === 'LIABILITY') {
|
||||
liabilities = quantity.mul(unitPrice);
|
||||
}
|
||||
|
||||
let valuables = new Big(0);
|
||||
|
||||
if (type === 'ITEM') {
|
||||
valuables = quantity.mul(unitPrice);
|
||||
}
|
||||
|
||||
if (lastDate !== date || lastTransactionPoint === null) {
|
||||
lastTransactionPoint = {
|
||||
date,
|
||||
fees,
|
||||
interest,
|
||||
liabilities,
|
||||
valuables,
|
||||
items: newItems
|
||||
};
|
||||
|
||||
this.transactionPoints.push(lastTransactionPoint);
|
||||
} else {
|
||||
lastTransactionPoint.fees = lastTransactionPoint.fees.plus(fees);
|
||||
lastTransactionPoint.interest =
|
||||
lastTransactionPoint.interest.plus(interest);
|
||||
lastTransactionPoint.items = newItems;
|
||||
lastTransactionPoint.liabilities =
|
||||
lastTransactionPoint.liabilities.plus(liabilities);
|
||||
lastTransactionPoint.valuables =
|
||||
lastTransactionPoint.valuables.plus(valuables);
|
||||
}
|
||||
|
||||
lastDate = date;
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
this.snapshot = await this.computeSnapshot(this.startDate, this.endDate);
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it.only('with BALN.SW buy and sell in two activities', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
@ -100,15 +104,11 @@ describe('PortfolioCalculator', () => {
|
||||
currency: 'CHF'
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2021-11-22')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2021-11-22')
|
||||
);
|
||||
|
||||
@ -121,7 +121,7 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('0'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('-12.6'),
|
||||
@ -173,8 +173,12 @@ describe('PortfolioCalculator', () => {
|
||||
valueInBaseCurrency: new Big('0')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('3.2'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('0'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
||||
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it.only('with BALN.SW buy and sell', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
@ -85,15 +89,11 @@ describe('PortfolioCalculator', () => {
|
||||
currency: 'CHF'
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2021-11-22')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2021-11-22')
|
||||
);
|
||||
|
||||
@ -106,7 +106,7 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('0'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('-12.6'),
|
||||
@ -156,8 +156,12 @@ describe('PortfolioCalculator', () => {
|
||||
valueInBaseCurrency: new Big('0')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('3.2'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('0'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
||||
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it.only('with BALN.SW buy', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
@ -70,15 +74,11 @@ describe('PortfolioCalculator', () => {
|
||||
currency: 'CHF'
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2021-11-30')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2021-11-30')
|
||||
);
|
||||
|
||||
@ -91,7 +91,7 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('297.8'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('24.6'),
|
||||
@ -141,8 +141,12 @@ describe('PortfolioCalculator', () => {
|
||||
valueInBaseCurrency: new Big('297.8')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('1.55'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('273.2'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('273.2')
|
||||
totalInvestmentWithCurrencyEffect: new Big('273.2'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
|
@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it.only('with BTCUSD buy and sell partially', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
@ -98,15 +102,11 @@ describe('PortfolioCalculator', () => {
|
||||
currency: 'CHF'
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2018-01-01').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2015-01-01')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2015-01-01')
|
||||
);
|
||||
|
||||
@ -119,7 +119,7 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('13298.425356'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('27172.74'),
|
||||
@ -175,8 +175,12 @@ describe('PortfolioCalculator', () => {
|
||||
valueInBaseCurrency: new Big('13298.425356')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('0'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('320.43'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957')
|
||||
totalInvestmentWithCurrencyEffect: new Big('318.542667299999967957'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
|
@ -0,0 +1,134 @@
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import {
|
||||
activityDummyData,
|
||||
symbolProfileDummyData
|
||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||
import {
|
||||
PortfolioCalculatorFactory,
|
||||
PerformanceCalculationType
|
||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||
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';
|
||||
|
||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||
return CurrentRateServiceMock;
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('PortfolioCalculator', () => {
|
||||
let currentRateService: CurrentRateService;
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
let factory: PortfolioCalculatorFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
factory = new PortfolioCalculatorFactory(
|
||||
currentRateService,
|
||||
exchangeRateDataService
|
||||
);
|
||||
});
|
||||
|
||||
describe('compute portfolio snapshot', () => {
|
||||
it.only('with fee activity', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
date: new Date('2021-09-01'),
|
||||
fee: 49,
|
||||
quantity: 0,
|
||||
SymbolProfile: {
|
||||
...symbolProfileDummyData,
|
||||
currency: 'USD',
|
||||
dataSource: 'MANUAL',
|
||||
name: 'Account Opening Fee',
|
||||
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141'
|
||||
},
|
||||
type: 'FEE',
|
||||
unitPrice: 0
|
||||
}
|
||||
];
|
||||
|
||||
const portfolioCalculator = factory.createCalculator({
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'USD'
|
||||
});
|
||||
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2021-11-30')
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('0'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('0'),
|
||||
grossPerformancePercentage: new Big('0'),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||
grossPerformanceWithCurrencyEffect: new Big('0'),
|
||||
hasErrors: true,
|
||||
netPerformance: new Big('0'),
|
||||
netPerformancePercentage: new Big('0'),
|
||||
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||
netPerformanceWithCurrencyEffect: new Big('0'),
|
||||
positions: [
|
||||
{
|
||||
averagePrice: new Big('0'),
|
||||
currency: 'USD',
|
||||
dataSource: 'MANUAL',
|
||||
dividend: new Big('0'),
|
||||
dividendInBaseCurrency: new Big('0'),
|
||||
fee: new Big('49'),
|
||||
firstBuyDate: '2021-09-01',
|
||||
grossPerformance: null,
|
||||
grossPerformancePercentage: null,
|
||||
grossPerformancePercentageWithCurrencyEffect: null,
|
||||
grossPerformanceWithCurrencyEffect: null,
|
||||
investment: new Big('0'),
|
||||
investmentWithCurrencyEffect: new Big('0'),
|
||||
marketPrice: null,
|
||||
marketPriceInBaseCurrency: 0,
|
||||
netPerformance: null,
|
||||
netPerformancePercentage: null,
|
||||
netPerformancePercentageWithCurrencyEffect: null,
|
||||
netPerformanceWithCurrencyEffect: null,
|
||||
quantity: new Big('0'),
|
||||
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141',
|
||||
tags: [],
|
||||
timeWeightedInvestment: new Big('0'),
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
||||
transactionCount: 1,
|
||||
valueInBaseCurrency: new Big('0')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('49'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('0'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it.only('with GOOGL buy', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
@ -83,15 +87,11 @@ describe('PortfolioCalculator', () => {
|
||||
currency: 'CHF'
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2023-01-03')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2023-01-03')
|
||||
);
|
||||
|
||||
@ -104,7 +104,7 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('103.10483'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('27.33'),
|
||||
@ -154,8 +154,12 @@ describe('PortfolioCalculator', () => {
|
||||
valueInBaseCurrency: new Big('103.10483')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('1'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('89.12'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('82.329056')
|
||||
totalInvestmentWithCurrencyEffect: new Big('82.329056'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
|
@ -0,0 +1,134 @@
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import {
|
||||
activityDummyData,
|
||||
symbolProfileDummyData
|
||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||
import {
|
||||
PortfolioCalculatorFactory,
|
||||
PerformanceCalculationType
|
||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||
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';
|
||||
|
||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||
return CurrentRateServiceMock;
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('PortfolioCalculator', () => {
|
||||
let currentRateService: CurrentRateService;
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
let factory: PortfolioCalculatorFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
factory = new PortfolioCalculatorFactory(
|
||||
currentRateService,
|
||||
exchangeRateDataService
|
||||
);
|
||||
});
|
||||
|
||||
describe('compute portfolio snapshot', () => {
|
||||
it.only('with item activity', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2022-01-31').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
date: new Date('2022-01-01'),
|
||||
fee: 0,
|
||||
quantity: 1,
|
||||
SymbolProfile: {
|
||||
...symbolProfileDummyData,
|
||||
currency: 'USD',
|
||||
dataSource: 'MANUAL',
|
||||
name: 'Penthouse Apartment',
|
||||
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde'
|
||||
},
|
||||
type: 'ITEM',
|
||||
unitPrice: 500000
|
||||
}
|
||||
];
|
||||
|
||||
const portfolioCalculator = factory.createCalculator({
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'USD'
|
||||
});
|
||||
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2022-01-01')
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('0'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('0'),
|
||||
grossPerformancePercentage: new Big('0'),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||
grossPerformanceWithCurrencyEffect: new Big('0'),
|
||||
hasErrors: true,
|
||||
netPerformance: new Big('0'),
|
||||
netPerformancePercentage: new Big('0'),
|
||||
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||
netPerformanceWithCurrencyEffect: new Big('0'),
|
||||
positions: [
|
||||
{
|
||||
averagePrice: new Big('500000'),
|
||||
currency: 'USD',
|
||||
dataSource: 'MANUAL',
|
||||
dividend: new Big('0'),
|
||||
dividendInBaseCurrency: new Big('0'),
|
||||
fee: new Big('0'),
|
||||
firstBuyDate: '2022-01-01',
|
||||
grossPerformance: null,
|
||||
grossPerformancePercentage: null,
|
||||
grossPerformancePercentageWithCurrencyEffect: null,
|
||||
grossPerformanceWithCurrencyEffect: null,
|
||||
investment: new Big('0'),
|
||||
investmentWithCurrencyEffect: new Big('0'),
|
||||
marketPrice: null,
|
||||
marketPriceInBaseCurrency: 500000,
|
||||
netPerformance: null,
|
||||
netPerformancePercentage: null,
|
||||
netPerformancePercentageWithCurrencyEffect: null,
|
||||
netPerformanceWithCurrencyEffect: null,
|
||||
quantity: new Big('0'),
|
||||
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde',
|
||||
tags: [],
|
||||
timeWeightedInvestment: new Big('0'),
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
||||
transactionCount: 1,
|
||||
valueInBaseCurrency: new Big('0')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('0'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('0'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,134 @@
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import {
|
||||
activityDummyData,
|
||||
symbolProfileDummyData
|
||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
|
||||
import {
|
||||
PortfolioCalculatorFactory,
|
||||
PerformanceCalculationType
|
||||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||
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';
|
||||
|
||||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
CurrentRateService: jest.fn().mockImplementation(() => {
|
||||
return CurrentRateServiceMock;
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('PortfolioCalculator', () => {
|
||||
let currentRateService: CurrentRateService;
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
let factory: PortfolioCalculatorFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
factory = new PortfolioCalculatorFactory(
|
||||
currentRateService,
|
||||
exchangeRateDataService
|
||||
);
|
||||
});
|
||||
|
||||
describe('compute portfolio snapshot', () => {
|
||||
it.only('with liability activity', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2022-01-31').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
date: new Date('2022-01-01'),
|
||||
fee: 0,
|
||||
quantity: 1,
|
||||
SymbolProfile: {
|
||||
...symbolProfileDummyData,
|
||||
currency: 'USD',
|
||||
dataSource: 'MANUAL',
|
||||
name: 'Loan',
|
||||
symbol: '55196015-1365-4560-aa60-8751ae6d18f8'
|
||||
},
|
||||
type: 'LIABILITY',
|
||||
unitPrice: 3000
|
||||
}
|
||||
];
|
||||
|
||||
const portfolioCalculator = factory.createCalculator({
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'USD'
|
||||
});
|
||||
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2022-01-01')
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('0'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('0'),
|
||||
grossPerformancePercentage: new Big('0'),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||
grossPerformanceWithCurrencyEffect: new Big('0'),
|
||||
hasErrors: true,
|
||||
netPerformance: new Big('0'),
|
||||
netPerformancePercentage: new Big('0'),
|
||||
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||
netPerformanceWithCurrencyEffect: new Big('0'),
|
||||
positions: [
|
||||
{
|
||||
averagePrice: new Big('3000'),
|
||||
currency: 'USD',
|
||||
dataSource: 'MANUAL',
|
||||
dividend: new Big('0'),
|
||||
dividendInBaseCurrency: new Big('0'),
|
||||
fee: new Big('0'),
|
||||
firstBuyDate: '2022-01-01',
|
||||
grossPerformance: null,
|
||||
grossPerformancePercentage: null,
|
||||
grossPerformancePercentageWithCurrencyEffect: null,
|
||||
grossPerformanceWithCurrencyEffect: null,
|
||||
investment: new Big('0'),
|
||||
investmentWithCurrencyEffect: new Big('0'),
|
||||
marketPrice: null,
|
||||
marketPriceInBaseCurrency: 3000,
|
||||
netPerformance: null,
|
||||
netPerformancePercentage: null,
|
||||
netPerformancePercentageWithCurrencyEffect: null,
|
||||
netPerformanceWithCurrencyEffect: null,
|
||||
quantity: new Big('0'),
|
||||
symbol: '55196015-1365-4560-aa60-8751ae6d18f8',
|
||||
tags: [],
|
||||
timeWeightedInvestment: new Big('0'),
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
||||
transactionCount: 1,
|
||||
valueInBaseCurrency: new Big('0')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('0'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('0'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -59,6 +59,10 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it.only('with MSFT buy', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
@ -98,17 +102,13 @@ describe('PortfolioCalculator', () => {
|
||||
currency: 'USD'
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2023-07-10').getTime());
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2023-07-10')
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toMatchObject({
|
||||
expect(portfolioSnapshot).toMatchObject({
|
||||
errors: [],
|
||||
hasErrors: false,
|
||||
positions: [
|
||||
@ -130,8 +130,12 @@ describe('PortfolioCalculator', () => {
|
||||
transactionCount: 2
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('19'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('298.58'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('298.58')
|
||||
totalInvestmentWithCurrencyEffect: new Big('298.58'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -42,22 +42,22 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it('with no orders', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const portfolioCalculator = factory.createCalculator({
|
||||
activities: [],
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF'
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2021-12-18').getTime());
|
||||
|
||||
const start = subDays(new Date(Date.now()), 10);
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({ start });
|
||||
|
||||
const currentPositions =
|
||||
await portfolioCalculator.getCurrentPositions(start);
|
||||
const portfolioSnapshot =
|
||||
await portfolioCalculator.computeSnapshot(start);
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
@ -68,7 +68,7 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big(0),
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0),
|
||||
@ -80,8 +80,12 @@ describe('PortfolioCalculator', () => {
|
||||
netPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||
netPerformanceWithCurrencyEffect: new Big(0),
|
||||
positions: [],
|
||||
totalFeesWithCurrencyEffect: new Big('0'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big(0),
|
||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||
totalInvestmentWithCurrencyEffect: new Big(0),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([]);
|
||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it.only('with NOVN.SW buy and sell partially', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
@ -84,15 +88,12 @@ describe('PortfolioCalculator', () => {
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF'
|
||||
});
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2022-03-07')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2022-03-07')
|
||||
);
|
||||
|
||||
@ -105,7 +106,7 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('87.8'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('21.93'),
|
||||
@ -157,8 +158,12 @@ describe('PortfolioCalculator', () => {
|
||||
valueInBaseCurrency: new Big('87.8')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('4.25'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('75.80'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('75.80')
|
||||
totalInvestmentWithCurrencyEffect: new Big('75.80'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
|
@ -46,6 +46,10 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
describe('get current positions', () => {
|
||||
it.only('with NOVN.SW buy and sell', async () => {
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
@ -85,15 +89,11 @@ describe('PortfolioCalculator', () => {
|
||||
currency: 'CHF'
|
||||
});
|
||||
|
||||
const spy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => parseDate('2022-04-11').getTime());
|
||||
|
||||
const chartData = await portfolioCalculator.getChartData({
|
||||
start: parseDate('2022-03-07')
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2022-03-07')
|
||||
);
|
||||
|
||||
@ -132,7 +132,7 @@ describe('PortfolioCalculator', () => {
|
||||
valueWithCurrencyEffect: 0
|
||||
});
|
||||
|
||||
expect(currentPositions).toEqual({
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('0'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('19.86'),
|
||||
@ -182,8 +182,12 @@ describe('PortfolioCalculator', () => {
|
||||
valueInBaseCurrency: new Big('0')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('0'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('0'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('0')
|
||||
totalInvestmentWithCurrencyEffect: new Big('0'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
||||
totalValuablesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
expect(investments).toEqual([
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
|
||||
import { CurrentPositions } from '@ghostfolio/api/app/portfolio/interfaces/current-positions.interface';
|
||||
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
|
||||
import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface';
|
||||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
@ -23,19 +23,27 @@ import { cloneDeep, first, last, sortBy } from 'lodash';
|
||||
export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
protected calculateOverallPerformance(
|
||||
positions: TimelinePosition[]
|
||||
): CurrentPositions {
|
||||
): PortfolioSnapshot {
|
||||
let currentValueInBaseCurrency = new Big(0);
|
||||
let grossPerformance = new Big(0);
|
||||
let grossPerformanceWithCurrencyEffect = new Big(0);
|
||||
let hasErrors = false;
|
||||
let netPerformance = new Big(0);
|
||||
let netPerformanceWithCurrencyEffect = new Big(0);
|
||||
let totalFeesWithCurrencyEffect = new Big(0);
|
||||
let totalInterestWithCurrencyEffect = new Big(0);
|
||||
let totalInvestment = new Big(0);
|
||||
let totalInvestmentWithCurrencyEffect = new Big(0);
|
||||
let totalTimeWeightedInvestment = new Big(0);
|
||||
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
|
||||
|
||||
for (const currentPosition of positions) {
|
||||
if (currentPosition.fee) {
|
||||
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
|
||||
currentPosition.fee
|
||||
);
|
||||
}
|
||||
|
||||
if (currentPosition.valueInBaseCurrency) {
|
||||
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
||||
currentPosition.valueInBaseCurrency
|
||||
@ -101,6 +109,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
hasErrors,
|
||||
netPerformance,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
positions,
|
||||
totalFeesWithCurrencyEffect,
|
||||
totalInterestWithCurrencyEffect,
|
||||
totalInvestment,
|
||||
totalInvestmentWithCurrencyEffect,
|
||||
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
|
||||
@ -121,7 +132,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
: grossPerformanceWithCurrencyEffect.div(
|
||||
totalTimeWeightedInvestmentWithCurrencyEffect
|
||||
),
|
||||
positions
|
||||
totalLiabilitiesWithCurrencyEffect: new Big(0),
|
||||
totalValuablesWithCurrencyEffect: new Big(0)
|
||||
};
|
||||
}
|
||||
|
||||
@ -178,12 +190,18 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
|
||||
let totalDividend = new Big(0);
|
||||
let totalDividendInBaseCurrency = new Big(0);
|
||||
let totalInterest = new Big(0);
|
||||
let totalInterestInBaseCurrency = new Big(0);
|
||||
let totalInvestment = new Big(0);
|
||||
let totalInvestmentFromBuyTransactions = new Big(0);
|
||||
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
|
||||
let totalInvestmentWithCurrencyEffect = new Big(0);
|
||||
let totalLiabilities = new Big(0);
|
||||
let totalLiabilitiesInBaseCurrency = new Big(0);
|
||||
let totalQuantityFromBuyTransactions = new Big(0);
|
||||
let totalUnits = new Big(0);
|
||||
let totalValuables = new Big(0);
|
||||
let totalValuablesInBaseCurrency = new Big(0);
|
||||
let valueAtStartDate: Big;
|
||||
let valueAtStartDateWithCurrencyEffect: Big;
|
||||
|
||||
@ -198,6 +216,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
return {
|
||||
currentValues: {},
|
||||
currentValuesWithCurrencyEffect: {},
|
||||
feesWithCurrencyEffect: new Big(0),
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||
@ -220,8 +239,14 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
||||
totalDividend: new Big(0),
|
||||
totalDividendInBaseCurrency: new Big(0),
|
||||
totalInterest: new Big(0),
|
||||
totalInterestInBaseCurrency: new Big(0),
|
||||
totalInvestment: new Big(0),
|
||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||
totalInvestmentWithCurrencyEffect: new Big(0),
|
||||
totalLiabilities: new Big(0),
|
||||
totalLiabilitiesInBaseCurrency: new Big(0),
|
||||
totalValuables: new Big(0),
|
||||
totalValuablesInBaseCurrency: new Big(0)
|
||||
};
|
||||
}
|
||||
|
||||
@ -240,6 +265,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
return {
|
||||
currentValues: {},
|
||||
currentValuesWithCurrencyEffect: {},
|
||||
feesWithCurrencyEffect: new Big(0),
|
||||
grossPerformance: new Big(0),
|
||||
grossPerformancePercentage: new Big(0),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big(0),
|
||||
@ -262,8 +288,14 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
|
||||
totalDividend: new Big(0),
|
||||
totalDividendInBaseCurrency: new Big(0),
|
||||
totalInterest: new Big(0),
|
||||
totalInterestInBaseCurrency: new Big(0),
|
||||
totalInvestment: new Big(0),
|
||||
totalInvestmentWithCurrencyEffect: new Big(0)
|
||||
totalInvestmentWithCurrencyEffect: new Big(0),
|
||||
totalLiabilities: new Big(0),
|
||||
totalLiabilitiesInBaseCurrency: new Big(0),
|
||||
totalValuables: new Big(0),
|
||||
totalValuablesInBaseCurrency: new Big(0)
|
||||
};
|
||||
}
|
||||
|
||||
@ -511,6 +543,27 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
|
||||
dividend.mul(exchangeRateAtOrderDate ?? 1)
|
||||
);
|
||||
} else if (order.type === 'INTEREST') {
|
||||
const interest = order.quantity.mul(order.unitPrice);
|
||||
|
||||
totalInterest = totalInterest.plus(interest);
|
||||
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
|
||||
interest.mul(exchangeRateAtOrderDate ?? 1)
|
||||
);
|
||||
} else if (order.type === 'ITEM') {
|
||||
const valuables = order.quantity.mul(order.unitPrice);
|
||||
|
||||
totalValuables = totalValuables.plus(valuables);
|
||||
totalValuablesInBaseCurrency = totalValuablesInBaseCurrency.plus(
|
||||
valuables.mul(exchangeRateAtOrderDate ?? 1)
|
||||
);
|
||||
} else if (order.type === 'LIABILITY') {
|
||||
const liabilities = order.quantity.mul(order.unitPrice);
|
||||
|
||||
totalLiabilities = totalLiabilities.plus(liabilities);
|
||||
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
|
||||
liabilities.mul(exchangeRateAtOrderDate ?? 1)
|
||||
);
|
||||
}
|
||||
|
||||
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
|
||||
@ -808,6 +861,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
return {
|
||||
currentValues,
|
||||
currentValuesWithCurrencyEffect,
|
||||
feesWithCurrencyEffect,
|
||||
grossPerformancePercentage,
|
||||
grossPerformancePercentageWithCurrencyEffect,
|
||||
initialValue,
|
||||
@ -823,8 +877,14 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
timeWeightedInvestmentValuesWithCurrencyEffect,
|
||||
totalDividend,
|
||||
totalDividendInBaseCurrency,
|
||||
totalInterest,
|
||||
totalInterestInBaseCurrency,
|
||||
totalInvestment,
|
||||
totalInvestmentWithCurrencyEffect,
|
||||
totalLiabilities,
|
||||
totalLiabilitiesInBaseCurrency,
|
||||
totalValuables,
|
||||
totalValuablesInBaseCurrency,
|
||||
grossPerformance: totalGrossPerformance,
|
||||
grossPerformanceWithCurrencyEffect:
|
||||
totalGrossPerformanceWithCurrencyEffect,
|
||||
|
@ -2,7 +2,7 @@ import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Big } from 'big.js';
|
||||
|
||||
export interface CurrentPositions extends ResponseError {
|
||||
export interface PortfolioSnapshot extends ResponseError {
|
||||
currentValueInBaseCurrency: Big;
|
||||
grossPerformance: Big;
|
||||
grossPerformanceWithCurrencyEffect: Big;
|
||||
@ -15,6 +15,10 @@ export interface CurrentPositions extends ResponseError {
|
||||
netPerformancePercentage: Big;
|
||||
netPerformancePercentageWithCurrencyEffect: Big;
|
||||
positions: TimelinePosition[];
|
||||
totalFeesWithCurrencyEffect: Big;
|
||||
totalInterestWithCurrencyEffect: Big;
|
||||
totalInvestment: Big;
|
||||
totalInvestmentWithCurrencyEffect: Big;
|
||||
totalLiabilitiesWithCurrencyEffect: Big;
|
||||
totalValuablesWithCurrencyEffect: Big;
|
||||
}
|
@ -1,6 +1,12 @@
|
||||
import { Big } from 'big.js';
|
||||
|
||||
import { TransactionPointSymbol } from './transaction-point-symbol.interface';
|
||||
|
||||
export interface TransactionPoint {
|
||||
date: string;
|
||||
fees: Big;
|
||||
interest: Big;
|
||||
items: TransactionPointSymbol[];
|
||||
liabilities: Big;
|
||||
valuables: Big;
|
||||
}
|
||||
|
@ -78,10 +78,8 @@ export class PortfolioController {
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string,
|
||||
@Query('withLiabilities') withLiabilitiesParam = 'false',
|
||||
@Query('withMarkets') withMarketsParam = 'false'
|
||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||
const withLiabilities = withLiabilitiesParam === 'true';
|
||||
const withMarkets = withMarketsParam === 'true';
|
||||
|
||||
let hasDetails = true;
|
||||
@ -107,7 +105,6 @@ export class PortfolioController {
|
||||
dateRange,
|
||||
filters,
|
||||
impersonationId,
|
||||
withLiabilities,
|
||||
withMarkets,
|
||||
userId: this.request.user.id,
|
||||
withSummary: true
|
||||
@ -389,11 +386,9 @@ export class PortfolioController {
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string,
|
||||
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false',
|
||||
@Query('withItems') withItemsParam = 'false'
|
||||
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
|
||||
): Promise<PortfolioPerformanceResponse> {
|
||||
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
||||
const withItems = withItemsParam === 'true';
|
||||
|
||||
const hasReadRestrictedAccessPermission =
|
||||
this.userService.hasReadRestrictedAccessPermission({
|
||||
@ -412,7 +407,6 @@ export class PortfolioController {
|
||||
filters,
|
||||
impersonationId,
|
||||
withExcludedAccounts,
|
||||
withItems,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
|
@ -23,12 +23,7 @@ import {
|
||||
EMERGENCY_FUND_TAG_ID,
|
||||
UNKNOWN_KEY
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getAllActivityTypes,
|
||||
getSum,
|
||||
parseDate
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Accounts,
|
||||
EnhancedSymbolProfile,
|
||||
@ -65,7 +60,6 @@ import {
|
||||
Prisma
|
||||
} from '@prisma/client';
|
||||
import { Big } from 'big.js';
|
||||
import { isUUID } from 'class-validator';
|
||||
import {
|
||||
differenceInDays,
|
||||
format,
|
||||
@ -78,6 +72,7 @@ import {
|
||||
} from 'date-fns';
|
||||
import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash';
|
||||
|
||||
import { PortfolioCalculator } from './calculator/portfolio-calculator';
|
||||
import {
|
||||
PerformanceCalculationType,
|
||||
PortfolioCalculatorFactory
|
||||
@ -328,7 +323,6 @@ export class PortfolioService {
|
||||
impersonationId,
|
||||
userId,
|
||||
withExcludedAccounts = false,
|
||||
withLiabilities = false,
|
||||
withMarkets = false,
|
||||
withSummary = false
|
||||
}: {
|
||||
@ -337,7 +331,6 @@ export class PortfolioService {
|
||||
impersonationId: string;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
withLiabilities?: boolean;
|
||||
withMarkets?: boolean;
|
||||
withSummary?: boolean;
|
||||
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||
@ -349,19 +342,8 @@ export class PortfolioService {
|
||||
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||
);
|
||||
|
||||
let types = getAllActivityTypes().filter((activityType) => {
|
||||
return activityType !== 'FEE';
|
||||
});
|
||||
|
||||
if (withLiabilities === false) {
|
||||
types = types.filter((activityType) => {
|
||||
return activityType !== 'LIABILITY';
|
||||
});
|
||||
}
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
filters,
|
||||
types,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
@ -369,16 +351,13 @@ export class PortfolioService {
|
||||
|
||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||
activities,
|
||||
dateRange,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: userCurrency
|
||||
});
|
||||
|
||||
const { startDate } = getInterval(
|
||||
dateRange,
|
||||
portfolioCalculator.getStartDate()
|
||||
);
|
||||
const currentPositions =
|
||||
await portfolioCalculator.getCurrentPositions(startDate);
|
||||
const { currentValueInBaseCurrency, hasErrors, positions } =
|
||||
await portfolioCalculator.getSnapshot();
|
||||
|
||||
const cashDetails = await this.accountService.getCashDetails({
|
||||
filters,
|
||||
@ -388,10 +367,9 @@ export class PortfolioService {
|
||||
|
||||
const holdings: PortfolioDetails['holdings'] = {};
|
||||
|
||||
const totalValueInBaseCurrency =
|
||||
currentPositions.currentValueInBaseCurrency.plus(
|
||||
cashDetails.balanceInBaseCurrency
|
||||
);
|
||||
const totalValueInBaseCurrency = currentValueInBaseCurrency.plus(
|
||||
cashDetails.balanceInBaseCurrency
|
||||
);
|
||||
|
||||
const isFilteredByAccount =
|
||||
filters?.some(({ type }) => {
|
||||
@ -409,7 +387,7 @@ export class PortfolioService {
|
||||
|
||||
let filteredValueInBaseCurrency = isFilteredByAccount
|
||||
? totalValueInBaseCurrency
|
||||
: currentPositions.currentValueInBaseCurrency;
|
||||
: currentValueInBaseCurrency;
|
||||
|
||||
if (
|
||||
filters?.length === 0 ||
|
||||
@ -422,14 +400,12 @@ export class PortfolioService {
|
||||
);
|
||||
}
|
||||
|
||||
const dataGatheringItems = currentPositions.positions.map(
|
||||
({ dataSource, symbol }) => {
|
||||
return {
|
||||
dataSource,
|
||||
symbol
|
||||
};
|
||||
}
|
||||
);
|
||||
const dataGatheringItems = positions.map(({ dataSource, symbol }) => {
|
||||
return {
|
||||
dataSource,
|
||||
symbol
|
||||
};
|
||||
});
|
||||
|
||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
||||
@ -442,7 +418,7 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
||||
for (const position of currentPositions.positions) {
|
||||
for (const position of positions) {
|
||||
portfolioItemsNow[position.symbol] = position;
|
||||
}
|
||||
|
||||
@ -465,7 +441,7 @@ export class PortfolioService {
|
||||
tags,
|
||||
transactionCount,
|
||||
valueInBaseCurrency
|
||||
} of currentPositions.positions) {
|
||||
} of positions) {
|
||||
if (isFilteredByClosedHoldings === true) {
|
||||
if (!quantity.eq(0)) {
|
||||
// Ignore positions with a quantity
|
||||
@ -593,6 +569,7 @@ export class PortfolioService {
|
||||
filteredValueInBaseCurrency,
|
||||
holdings,
|
||||
impersonationId,
|
||||
portfolioCalculator,
|
||||
userCurrency,
|
||||
userId,
|
||||
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
||||
@ -605,10 +582,10 @@ export class PortfolioService {
|
||||
|
||||
return {
|
||||
accounts,
|
||||
hasErrors,
|
||||
holdings,
|
||||
platforms,
|
||||
summary,
|
||||
hasErrors: currentPositions.hasErrors
|
||||
summary
|
||||
};
|
||||
}
|
||||
|
||||
@ -681,10 +658,9 @@ export class PortfolioService {
|
||||
const portfolioStart = portfolioCalculator.getStartDate();
|
||||
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
||||
|
||||
const currentPositions =
|
||||
await portfolioCalculator.getCurrentPositions(portfolioStart);
|
||||
const { positions } = await portfolioCalculator.getSnapshot();
|
||||
|
||||
const position = currentPositions.positions.find(({ symbol }) => {
|
||||
const position = positions.find(({ symbol }) => {
|
||||
return symbol === aSymbol;
|
||||
});
|
||||
|
||||
@ -846,11 +822,19 @@ export class PortfolioService {
|
||||
);
|
||||
|
||||
if (isEmpty(historicalData)) {
|
||||
historicalData = await this.dataProviderService.getHistoricalRaw(
|
||||
[{ dataSource: DataSource.YAHOO, symbol: aSymbol }],
|
||||
portfolioStart,
|
||||
new Date()
|
||||
);
|
||||
try {
|
||||
historicalData = await this.dataProviderService.getHistoricalRaw({
|
||||
dataGatheringItems: [
|
||||
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
||||
],
|
||||
from: portfolioStart,
|
||||
to: new Date()
|
||||
});
|
||||
} catch {
|
||||
historicalData = {
|
||||
[aSymbol]: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const historicalDataArray: HistoricalDataItem[] = [];
|
||||
@ -916,13 +900,12 @@ export class PortfolioService {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
const { endDate } = getInterval(dateRange);
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
endDate,
|
||||
filters,
|
||||
userId,
|
||||
types: ['BUY', 'SELL'],
|
||||
userCurrency: this.getUserCurrency()
|
||||
});
|
||||
|
||||
@ -935,16 +918,14 @@ export class PortfolioService {
|
||||
|
||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||
activities,
|
||||
dateRange,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: this.request.user.Settings.settings.baseCurrency
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
let { hasErrors, positions } = await portfolioCalculator.getSnapshot();
|
||||
|
||||
let positions = currentPositions.positions.filter(({ quantity }) => {
|
||||
positions = positions.filter(({ quantity }) => {
|
||||
return !quantity.eq(0);
|
||||
});
|
||||
|
||||
@ -983,7 +964,7 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
return {
|
||||
hasErrors: currentPositions.hasErrors,
|
||||
hasErrors,
|
||||
positions: positions.map(
|
||||
({
|
||||
averagePrice,
|
||||
@ -1050,15 +1031,13 @@ export class PortfolioService {
|
||||
filters,
|
||||
impersonationId,
|
||||
userId,
|
||||
withExcludedAccounts = false,
|
||||
withItems = false
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
dateRange?: DateRange;
|
||||
filters?: Filter[];
|
||||
impersonationId: string;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
withItems?: boolean;
|
||||
}): Promise<PortfolioPerformanceResponse> {
|
||||
userId = await this.getUserId(impersonationId, userId);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
@ -1096,8 +1075,7 @@ export class PortfolioService {
|
||||
filters,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts,
|
||||
types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL']
|
||||
withExcludedAccounts
|
||||
});
|
||||
|
||||
if (accountBalanceItems?.length <= 0 && activities?.length <= 0) {
|
||||
@ -1123,6 +1101,7 @@ export class PortfolioService {
|
||||
|
||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||
activities,
|
||||
dateRange,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: userCurrency
|
||||
});
|
||||
@ -1140,7 +1119,7 @@ export class PortfolioService {
|
||||
netPerformancePercentageWithCurrencyEffect,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
totalInvestment
|
||||
} = await portfolioCalculator.getCurrentPositions(startDate, endDate);
|
||||
} = await portfolioCalculator.getSnapshot();
|
||||
|
||||
let currentNetPerformance = netPerformance;
|
||||
|
||||
@ -1231,8 +1210,7 @@ export class PortfolioService {
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
types: ['BUY', 'SELL']
|
||||
userId
|
||||
});
|
||||
|
||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||
@ -1241,13 +1219,10 @@ export class PortfolioService {
|
||||
currency: this.request.user.Settings.settings.baseCurrency
|
||||
});
|
||||
|
||||
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||
portfolioCalculator.getStartDate()
|
||||
);
|
||||
let { totalFeesWithCurrencyEffect, positions, totalInvestment } =
|
||||
await portfolioCalculator.getSnapshot();
|
||||
|
||||
const positions = currentPositions.positions.filter(
|
||||
(item) => !item.quantity.eq(0)
|
||||
);
|
||||
positions = positions.filter((item) => !item.quantity.eq(0));
|
||||
|
||||
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
||||
|
||||
@ -1309,8 +1284,8 @@ export class PortfolioService {
|
||||
[
|
||||
new FeeRatioInitialInvestment(
|
||||
this.exchangeRateDataService,
|
||||
currentPositions.totalInvestment.toNumber(),
|
||||
this.getFees({ activities, userCurrency }).toNumber()
|
||||
totalInvestment.toNumber(),
|
||||
totalFeesWithCurrencyEffect.toNumber()
|
||||
)
|
||||
],
|
||||
userSettings
|
||||
@ -1454,30 +1429,6 @@ export class PortfolioService {
|
||||
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
|
||||
}
|
||||
|
||||
private getFees({
|
||||
activities,
|
||||
userCurrency
|
||||
}: {
|
||||
activities: Activity[];
|
||||
userCurrency: string;
|
||||
}) {
|
||||
return getSum(
|
||||
activities
|
||||
.filter(({ isDraft }) => {
|
||||
return isDraft === false;
|
||||
})
|
||||
.map(({ fee, SymbolProfile }) => {
|
||||
return new Big(
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
fee,
|
||||
SymbolProfile.currency,
|
||||
userCurrency
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getInitialCashPosition({
|
||||
balance,
|
||||
currency
|
||||
@ -1623,6 +1574,7 @@ export class PortfolioService {
|
||||
filteredValueInBaseCurrency,
|
||||
holdings,
|
||||
impersonationId,
|
||||
portfolioCalculator,
|
||||
userCurrency,
|
||||
userId
|
||||
}: {
|
||||
@ -1631,6 +1583,7 @@ export class PortfolioService {
|
||||
filteredValueInBaseCurrency: Big;
|
||||
holdings: PortfolioDetails['holdings'];
|
||||
impersonationId: string;
|
||||
portfolioCalculator: PortfolioCalculator;
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<PortfolioSummary> {
|
||||
@ -1659,17 +1612,8 @@ export class PortfolioService {
|
||||
}
|
||||
}
|
||||
|
||||
const dividendInBaseCurrency = getSum(
|
||||
(
|
||||
await this.getDividends({
|
||||
activities: activities.filter(({ type }) => {
|
||||
return type === 'DIVIDEND';
|
||||
})
|
||||
})
|
||||
).map(({ investment }) => {
|
||||
return new Big(investment);
|
||||
})
|
||||
);
|
||||
const dividendInBaseCurrency =
|
||||
await portfolioCalculator.getDividendInBaseCurrency();
|
||||
|
||||
const emergencyFund = new Big(
|
||||
Math.max(
|
||||
@ -1678,42 +1622,16 @@ export class PortfolioService {
|
||||
)
|
||||
);
|
||||
|
||||
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
||||
const firstOrderDate = activities[0]?.date;
|
||||
const fees = await portfolioCalculator.getFeesInBaseCurrency();
|
||||
|
||||
const interest = this.getSumOfActivityType({
|
||||
activities,
|
||||
userCurrency,
|
||||
activityType: 'INTEREST'
|
||||
}).toNumber();
|
||||
const firstOrderDate = portfolioCalculator.getStartDate();
|
||||
|
||||
const items = getSum(
|
||||
Object.keys(holdings)
|
||||
.filter((symbol) => {
|
||||
return (
|
||||
isUUID(symbol) &&
|
||||
holdings[symbol].dataSource === 'MANUAL' &&
|
||||
holdings[symbol].valueInBaseCurrency > 0
|
||||
);
|
||||
})
|
||||
.map((symbol) => {
|
||||
return new Big(holdings[symbol].valueInBaseCurrency).abs();
|
||||
})
|
||||
).toNumber();
|
||||
const interest = await portfolioCalculator.getInterestInBaseCurrency();
|
||||
|
||||
const liabilities = getSum(
|
||||
Object.keys(holdings)
|
||||
.filter((symbol) => {
|
||||
return (
|
||||
isUUID(symbol) &&
|
||||
holdings[symbol].dataSource === 'MANUAL' &&
|
||||
holdings[symbol].valueInBaseCurrency < 0
|
||||
);
|
||||
})
|
||||
.map((symbol) => {
|
||||
return new Big(holdings[symbol].valueInBaseCurrency).abs();
|
||||
})
|
||||
).toNumber();
|
||||
const liabilities =
|
||||
await portfolioCalculator.getLiabilitiesInBaseCurrency();
|
||||
|
||||
const valuables = await portfolioCalculator.getValuablesInBaseCurrency();
|
||||
|
||||
const totalBuy = this.getSumOfActivityType({
|
||||
userCurrency,
|
||||
@ -1763,7 +1681,7 @@ export class PortfolioService {
|
||||
|
||||
const netWorth = new Big(balanceInBaseCurrency)
|
||||
.plus(performanceInformation.performance.currentValue)
|
||||
.plus(items)
|
||||
.plus(valuables)
|
||||
.plus(excludedAccountsAndActivities)
|
||||
.minus(liabilities)
|
||||
.toNumber();
|
||||
@ -1791,11 +1709,7 @@ export class PortfolioService {
|
||||
annualizedPerformancePercentWithCurrencyEffect,
|
||||
cash,
|
||||
excludedAccountsAndActivities,
|
||||
fees,
|
||||
firstOrderDate,
|
||||
interest,
|
||||
items,
|
||||
liabilities,
|
||||
totalBuy,
|
||||
totalSell,
|
||||
committedFunds: committedFunds.toNumber(),
|
||||
@ -1807,6 +1721,7 @@ export class PortfolioService {
|
||||
.toNumber(),
|
||||
total: emergencyFund.toNumber()
|
||||
},
|
||||
fees: fees.toNumber(),
|
||||
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
||||
filteredValueInPercentage: netWorth
|
||||
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
||||
@ -1814,6 +1729,9 @@ export class PortfolioService {
|
||||
fireWealth: new Big(performanceInformation.performance.currentValue)
|
||||
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||
.toNumber(),
|
||||
interest: interest.toNumber(),
|
||||
items: valuables.toNumber(),
|
||||
liabilities: liabilities.toNumber(),
|
||||
ordersCount: activities.filter(({ type }) => {
|
||||
return type === 'BUY' || type === 'SELL';
|
||||
}).length,
|
||||
|
@ -74,11 +74,21 @@ export class SymbolService {
|
||||
date = new Date(),
|
||||
symbol
|
||||
}: IDataGatheringItem): Promise<IDataProviderHistoricalResponse> {
|
||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
||||
[{ dataSource, symbol }],
|
||||
date,
|
||||
date
|
||||
);
|
||||
let historicalData: {
|
||||
[symbol: string]: {
|
||||
[date: string]: IDataProviderHistoricalResponse;
|
||||
};
|
||||
} = {
|
||||
[symbol]: {}
|
||||
};
|
||||
|
||||
try {
|
||||
historicalData = await this.dataProviderService.getHistoricalRaw({
|
||||
dataGatheringItems: [{ dataSource, symbol }],
|
||||
from: date,
|
||||
to: date
|
||||
});
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
marketPrice:
|
||||
|
@ -18,10 +18,8 @@ export function getFactor(activityType: ActivityType) {
|
||||
|
||||
switch (activityType) {
|
||||
case 'BUY':
|
||||
case 'ITEM':
|
||||
factor = 1;
|
||||
break;
|
||||
case 'LIABILITY':
|
||||
case 'SELL':
|
||||
factor = -1;
|
||||
break;
|
||||
@ -37,36 +35,48 @@ export function getInterval(
|
||||
aDateRange: DateRange,
|
||||
portfolioStart = new Date(0)
|
||||
) {
|
||||
let endDate = endOfDay(new Date());
|
||||
let endDate = endOfDay(new Date(Date.now()));
|
||||
let startDate = portfolioStart;
|
||||
|
||||
switch (aDateRange) {
|
||||
case '1d':
|
||||
startDate = max([startDate, subDays(resetHours(new Date()), 1)]);
|
||||
startDate = max([
|
||||
startDate,
|
||||
subDays(resetHours(new Date(Date.now())), 1)
|
||||
]);
|
||||
break;
|
||||
case 'mtd':
|
||||
startDate = max([
|
||||
startDate,
|
||||
subDays(startOfMonth(resetHours(new Date())), 1)
|
||||
subDays(startOfMonth(resetHours(new Date(Date.now()))), 1)
|
||||
]);
|
||||
break;
|
||||
case 'wtd':
|
||||
startDate = max([
|
||||
startDate,
|
||||
subDays(startOfWeek(resetHours(new Date()), { weekStartsOn: 1 }), 1)
|
||||
subDays(
|
||||
startOfWeek(resetHours(new Date(Date.now())), { weekStartsOn: 1 }),
|
||||
1
|
||||
)
|
||||
]);
|
||||
break;
|
||||
case 'ytd':
|
||||
startDate = max([
|
||||
startDate,
|
||||
subDays(startOfYear(resetHours(new Date())), 1)
|
||||
subDays(startOfYear(resetHours(new Date(Date.now()))), 1)
|
||||
]);
|
||||
break;
|
||||
case '1y':
|
||||
startDate = max([startDate, subYears(resetHours(new Date()), 1)]);
|
||||
startDate = max([
|
||||
startDate,
|
||||
subYears(resetHours(new Date(Date.now())), 1)
|
||||
]);
|
||||
break;
|
||||
case '5y':
|
||||
startDate = max([startDate, subYears(resetHours(new Date()), 5)]);
|
||||
startDate = max([
|
||||
startDate,
|
||||
subYears(resetHours(new Date(Date.now())), 5)
|
||||
]);
|
||||
break;
|
||||
case 'max':
|
||||
break;
|
||||
|
@ -37,7 +37,17 @@ export class DataGatheringProcessor {
|
||||
@Process({ concurrency: 1, name: GATHER_ASSET_PROFILE_PROCESS })
|
||||
public async gatherAssetProfile(job: Job<UniqueAsset>) {
|
||||
try {
|
||||
Logger.log(
|
||||
`Asset profile data gathering has been started for ${job.data.symbol} (${job.data.dataSource})`,
|
||||
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS})`
|
||||
);
|
||||
|
||||
await this.dataGatheringService.gatherAssetProfiles([job.data]);
|
||||
|
||||
Logger.log(
|
||||
`Asset profile data gathering has been completed for ${job.data.symbol} (${job.data.dataSource})`,
|
||||
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS})`
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
error,
|
||||
@ -62,11 +72,11 @@ export class DataGatheringProcessor {
|
||||
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS})`
|
||||
);
|
||||
|
||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
||||
[{ dataSource, symbol }],
|
||||
currentDate,
|
||||
new Date()
|
||||
);
|
||||
const historicalData = await this.dataProviderService.getHistoricalRaw({
|
||||
dataGatheringItems: [{ dataSource, symbol }],
|
||||
from: currentDate,
|
||||
to: new Date()
|
||||
});
|
||||
|
||||
const data: Prisma.MarketDataUpdateInput[] = [];
|
||||
let lastMarketPrice: number;
|
||||
|
@ -104,11 +104,11 @@ export class DataGatheringService {
|
||||
symbol: string;
|
||||
}) {
|
||||
try {
|
||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
||||
[{ dataSource, symbol }],
|
||||
date,
|
||||
date
|
||||
);
|
||||
const historicalData = await this.dataProviderService.getHistoricalRaw({
|
||||
dataGatheringItems: [{ dataSource, symbol }],
|
||||
from: date,
|
||||
to: date
|
||||
});
|
||||
|
||||
const marketPrice =
|
||||
historicalData[symbol][format(date, DATE_FORMAT)].marketPrice;
|
||||
@ -230,17 +230,12 @@ export class DataGatheringService {
|
||||
error,
|
||||
'DataGatheringService'
|
||||
);
|
||||
|
||||
if (uniqueAssets.length === 1) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.log(
|
||||
`Asset profile data gathering has been completed for ${uniqueAssets
|
||||
.map(({ dataSource, symbol }) => {
|
||||
return `${symbol} (${dataSource})`;
|
||||
})
|
||||
.join(',')}.`,
|
||||
'DataGatheringService'
|
||||
);
|
||||
}
|
||||
|
||||
public async gatherSymbols({
|
||||
|
@ -233,15 +233,17 @@ export class DataProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getHistoricalRaw(
|
||||
aDataGatheringItems: UniqueAsset[],
|
||||
from: Date,
|
||||
to: Date
|
||||
): Promise<{
|
||||
public async getHistoricalRaw({
|
||||
dataGatheringItems,
|
||||
from,
|
||||
to
|
||||
}: {
|
||||
dataGatheringItems: UniqueAsset[];
|
||||
from: Date;
|
||||
to: Date;
|
||||
}): Promise<{
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}> {
|
||||
let dataGatheringItems = aDataGatheringItems;
|
||||
|
||||
for (const { currency, rootCurrency } of DERIVED_CURRENCIES) {
|
||||
if (
|
||||
this.hasCurrency({
|
||||
@ -330,6 +332,8 @@ export class DataProviderService {
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error, 'DataProviderService');
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -102,7 +102,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
this.isLoading = true;
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioDetails({ withLiabilities: true })
|
||||
.fetchPortfolioDetails()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ summary }) => {
|
||||
this.summary = summary;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||
import { Currency } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import {
|
||||
@ -102,7 +103,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
public async onSubmit() {
|
||||
const account: CreateAccountDto | UpdateAccountDto = {
|
||||
balance: this.accountForm.controls['balance'].value,
|
||||
comment: this.accountForm.controls['comment'].value,
|
||||
@ -113,13 +114,29 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
platformId: this.accountForm.controls['platformId'].value?.id ?? null
|
||||
};
|
||||
|
||||
if (this.data.account.id) {
|
||||
(account as UpdateAccountDto).id = this.data.account.id;
|
||||
} else {
|
||||
delete (account as CreateAccountDto).id;
|
||||
}
|
||||
try {
|
||||
if (this.data.account.id) {
|
||||
(account as UpdateAccountDto).id = this.data.account.id;
|
||||
|
||||
this.dialogRef.close({ account });
|
||||
await validateObjectForForm({
|
||||
classDto: UpdateAccountDto,
|
||||
form: this.accountForm,
|
||||
object: account
|
||||
});
|
||||
} else {
|
||||
delete (account as CreateAccountDto).id;
|
||||
|
||||
await validateObjectForForm({
|
||||
classDto: CreateAccountDto,
|
||||
form: this.accountForm,
|
||||
object: account
|
||||
});
|
||||
}
|
||||
|
||||
this.dialogRef.close({ account });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
|
||||
@ -451,7 +452,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
public async onSubmit() {
|
||||
const activity: CreateOrderDto | UpdateOrderDto = {
|
||||
accountId: this.activityForm.controls['accountId'].value,
|
||||
assetClass: this.activityForm.controls['assetClass'].value,
|
||||
@ -474,14 +475,32 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
unitPrice: this.activityForm.controls['unitPrice'].value
|
||||
};
|
||||
|
||||
if (this.data.activity.id) {
|
||||
(activity as UpdateOrderDto).id = this.data.activity.id;
|
||||
} else {
|
||||
(activity as CreateOrderDto).updateAccountBalance =
|
||||
this.activityForm.controls['updateAccountBalance'].value;
|
||||
}
|
||||
try {
|
||||
if (this.data.activity.id) {
|
||||
(activity as UpdateOrderDto).id = this.data.activity.id;
|
||||
|
||||
this.dialogRef.close({ activity });
|
||||
await validateObjectForForm({
|
||||
classDto: UpdateOrderDto,
|
||||
form: this.activityForm,
|
||||
ignoreFields: ['dataSource', 'date'],
|
||||
object: activity as UpdateOrderDto
|
||||
});
|
||||
} else {
|
||||
(activity as CreateOrderDto).updateAccountBalance =
|
||||
this.activityForm.controls['updateAccountBalance'].value;
|
||||
|
||||
await validateObjectForForm({
|
||||
classDto: CreateOrderDto,
|
||||
form: this.activityForm,
|
||||
ignoreFields: ['dataSource', 'date'],
|
||||
object: activity
|
||||
});
|
||||
}
|
||||
|
||||
this.dialogRef.close({ activity });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -411,19 +411,13 @@ export class DataService {
|
||||
|
||||
public fetchPortfolioDetails({
|
||||
filters,
|
||||
withLiabilities = false,
|
||||
withMarkets = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
withLiabilities?: boolean;
|
||||
withMarkets?: boolean;
|
||||
} = {}): Observable<PortfolioDetails> {
|
||||
let params = this.buildFiltersAsQueryParams({ filters });
|
||||
|
||||
if (withLiabilities) {
|
||||
params = params.append('withLiabilities', withLiabilities);
|
||||
}
|
||||
|
||||
if (withMarkets) {
|
||||
params = params.append('withMarkets', withMarkets);
|
||||
}
|
||||
|
38
apps/client/src/app/util/form.util.ts
Normal file
38
apps/client/src/app/util/form.util.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
|
||||
export async function validateObjectForForm<T>({
|
||||
classDto,
|
||||
form,
|
||||
ignoreFields = [],
|
||||
object
|
||||
}: {
|
||||
classDto: { new (): T };
|
||||
form: FormGroup;
|
||||
ignoreFields?: string[];
|
||||
object: T;
|
||||
}): Promise<void> {
|
||||
const objectInstance = plainToInstance(classDto, object);
|
||||
const errors = await validate(objectInstance as object);
|
||||
|
||||
const nonIgnoredErrors = errors.filter(({ property }) => {
|
||||
return !ignoreFields.includes(property);
|
||||
});
|
||||
|
||||
if (nonIgnoredErrors.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
for (const { constraints, property } of nonIgnoredErrors) {
|
||||
const formControl = form.get(property);
|
||||
|
||||
if (formControl) {
|
||||
formControl.setErrors({
|
||||
validationError: Object.values(constraints)[0]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(nonIgnoredErrors);
|
||||
}
|
@ -7,6 +7,7 @@ export interface SymbolMetrics {
|
||||
currentValuesWithCurrencyEffect: {
|
||||
[date: string]: Big;
|
||||
};
|
||||
feesWithCurrencyEffect: Big;
|
||||
grossPerformance: Big;
|
||||
grossPerformancePercentage: Big;
|
||||
grossPerformancePercentageWithCurrencyEffect: Big;
|
||||
@ -41,6 +42,12 @@ export interface SymbolMetrics {
|
||||
timeWeightedInvestmentWithCurrencyEffect: Big;
|
||||
totalDividend: Big;
|
||||
totalDividendInBaseCurrency: Big;
|
||||
totalInterest: Big;
|
||||
totalInterestInBaseCurrency: Big;
|
||||
totalInvestment: Big;
|
||||
totalInvestmentWithCurrencyEffect: Big;
|
||||
totalLiabilities: Big;
|
||||
totalLiabilitiesInBaseCurrency: Big;
|
||||
totalValuables: Big;
|
||||
totalValuablesInBaseCurrency: Big;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.72.0",
|
||||
"version": "2.73.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -132,7 +132,7 @@
|
||||
"svgmap": "2.6.0",
|
||||
"twitter-api-v2": "1.14.2",
|
||||
"uuid": "9.0.1",
|
||||
"yahoo-finance2": "2.11.1",
|
||||
"yahoo-finance2": "2.11.2",
|
||||
"zone.js": "0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -171,7 +171,7 @@
|
||||
"@types/color": "3.0.3",
|
||||
"@types/google-spreadsheet": "3.1.5",
|
||||
"@types/jest": "29.4.4",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/lodash": "4.17.0",
|
||||
"@types/node": "18.16.9",
|
||||
"@types/papaparse": "5.3.7",
|
||||
"@types/passport-google-oauth20": "2.0.11",
|
||||
|
16
yarn.lock
16
yarn.lock
@ -7314,10 +7314,10 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash@4.14.195":
|
||||
version "4.14.195"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632"
|
||||
integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==
|
||||
"@types/lodash@4.17.0":
|
||||
version "4.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3"
|
||||
integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==
|
||||
|
||||
"@types/lodash@^4.14.167":
|
||||
version "4.14.200"
|
||||
@ -19620,10 +19620,10 @@ y18n@^5.0.5:
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yahoo-finance2@2.11.1:
|
||||
version "2.11.1"
|
||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.11.1.tgz#97758d4784ef0b4efe4b370a72063929cc4c6342"
|
||||
integrity sha512-YglgpjIDithq1PG8Je/gy8nzJFqkH214x2ZGfr6Y+HV4ymTDFLluq2W9Hsvvyydv1zTv9/Ykedf0J4YIpmO2Zg==
|
||||
yahoo-finance2@2.11.2:
|
||||
version "2.11.2"
|
||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.11.2.tgz#44f35105a2500fd1da22ac4f4393106f2bfec6d1"
|
||||
integrity sha512-S5lHKqneMXMKN/rxowqErEfkvXJE6s/SPuekT7UkOVbsSyRcptea/U3Mud+ikOEXEbKXPiZrU0Jy+iF51ITuSw==
|
||||
dependencies:
|
||||
"@types/tough-cookie" "^4.0.2"
|
||||
ajv "8.10.0"
|
||||
|
Reference in New Issue
Block a user