mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
96 changed files with 18441 additions and 15847 deletions
@ -0,0 +1,10 @@ |
|||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; |
|||
import { AccountBalance } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { IsArray, IsOptional } from 'class-validator'; |
|||
|
|||
export class CreateAccountWithBalancesDto extends CreateAccountDto { |
|||
@IsArray() |
|||
@IsOptional() |
|||
balances?: AccountBalance; |
|||
} |
@ -0,0 +1,969 @@ |
|||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
|||
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; |
|||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; |
|||
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; |
|||
import { DATE_FORMAT } from '@ghostfolio/common/helper'; |
|||
import { |
|||
AssetProfileIdentifier, |
|||
SymbolMetrics |
|||
} from '@ghostfolio/common/interfaces'; |
|||
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; |
|||
import { DateRange } from '@ghostfolio/common/types'; |
|||
|
|||
import { Logger } from '@nestjs/common'; |
|||
import { Big } from 'big.js'; |
|||
import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns'; |
|||
import { cloneDeep, sortBy } from 'lodash'; |
|||
|
|||
export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|||
private chartDates: string[]; |
|||
|
|||
protected calculateOverallPerformance( |
|||
positions: TimelinePosition[] |
|||
): 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 totalFeesWithCurrencyEffect = new Big(0); |
|||
const 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.feeInBaseCurrency) { |
|||
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( |
|||
currentPosition.feeInBaseCurrency |
|||
); |
|||
} |
|||
|
|||
if (currentPosition.valueInBaseCurrency) { |
|||
currentValueInBaseCurrency = currentValueInBaseCurrency.plus( |
|||
currentPosition.valueInBaseCurrency |
|||
); |
|||
} else { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.investment) { |
|||
totalInvestment = totalInvestment.plus(currentPosition.investment); |
|||
|
|||
totalInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect.plus( |
|||
currentPosition.investmentWithCurrencyEffect |
|||
); |
|||
} else { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.grossPerformance) { |
|||
grossPerformance = grossPerformance.plus( |
|||
currentPosition.grossPerformance |
|||
); |
|||
|
|||
grossPerformanceWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect.plus( |
|||
currentPosition.grossPerformanceWithCurrencyEffect |
|||
); |
|||
|
|||
netPerformance = netPerformance.plus(currentPosition.netPerformance); |
|||
} else if (!currentPosition.quantity.eq(0)) { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.timeWeightedInvestment) { |
|||
totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus( |
|||
currentPosition.timeWeightedInvestment |
|||
); |
|||
|
|||
totalTimeWeightedInvestmentWithCurrencyEffect = |
|||
totalTimeWeightedInvestmentWithCurrencyEffect.plus( |
|||
currentPosition.timeWeightedInvestmentWithCurrencyEffect |
|||
); |
|||
} else if (!currentPosition.quantity.eq(0)) { |
|||
Logger.warn( |
|||
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`, |
|||
'PortfolioCalculator' |
|||
); |
|||
|
|||
hasErrors = true; |
|||
} |
|||
} |
|||
|
|||
return { |
|||
currentValueInBaseCurrency, |
|||
hasErrors, |
|||
positions, |
|||
totalFeesWithCurrencyEffect, |
|||
totalInterestWithCurrencyEffect, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect, |
|||
activitiesCount: this.activities.filter(({ type }) => { |
|||
return ['BUY', 'SELL'].includes(type); |
|||
}).length, |
|||
createdAt: new Date(), |
|||
errors: [], |
|||
historicalData: [], |
|||
totalLiabilitiesWithCurrencyEffect: new Big(0), |
|||
totalValuablesWithCurrencyEffect: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
protected getSymbolMetrics({ |
|||
chartDateMap, |
|||
dataSource, |
|||
end, |
|||
exchangeRates, |
|||
marketSymbolMap, |
|||
start, |
|||
symbol |
|||
}: { |
|||
chartDateMap?: { [date: string]: boolean }; |
|||
end: Date; |
|||
exchangeRates: { [dateString: string]: number }; |
|||
marketSymbolMap: { |
|||
[date: string]: { [symbol: string]: Big }; |
|||
}; |
|||
start: Date; |
|||
} & AssetProfileIdentifier): SymbolMetrics { |
|||
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; |
|||
const currentValues: { [date: string]: Big } = {}; |
|||
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
let fees = new Big(0); |
|||
let feesAtStartDate = new Big(0); |
|||
let feesAtStartDateWithCurrencyEffect = new Big(0); |
|||
let feesWithCurrencyEffect = new Big(0); |
|||
let grossPerformance = new Big(0); |
|||
let grossPerformanceWithCurrencyEffect = new Big(0); |
|||
let grossPerformanceAtStartDate = new Big(0); |
|||
let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0); |
|||
let grossPerformanceFromSells = new Big(0); |
|||
let grossPerformanceFromSellsWithCurrencyEffect = new Big(0); |
|||
let initialValue: Big; |
|||
let initialValueWithCurrencyEffect: Big; |
|||
let investmentAtStartDate: Big; |
|||
let investmentAtStartDateWithCurrencyEffect: Big; |
|||
const investmentValuesAccumulated: { [date: string]: Big } = {}; |
|||
const investmentValuesAccumulatedWithCurrencyEffect: { |
|||
[date: string]: Big; |
|||
} = {}; |
|||
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
let lastAveragePrice = new Big(0); |
|||
let lastAveragePriceWithCurrencyEffect = new Big(0); |
|||
const netPerformanceValues: { [date: string]: Big } = {}; |
|||
const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
const timeWeightedInvestmentValues: { [date: string]: Big } = {}; |
|||
|
|||
const timeWeightedInvestmentValuesWithCurrencyEffect: { |
|||
[date: string]: Big; |
|||
} = {}; |
|||
|
|||
const totalAccountBalanceInBaseCurrency = new Big(0); |
|||
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; |
|||
|
|||
// Clone orders to keep the original values in this.orders
|
|||
let orders: PortfolioOrderItem[] = cloneDeep( |
|||
this.activities.filter(({ SymbolProfile }) => { |
|||
return SymbolProfile.symbol === symbol; |
|||
}) |
|||
); |
|||
|
|||
if (orders.length <= 0) { |
|||
return { |
|||
currentValues: {}, |
|||
currentValuesWithCurrencyEffect: {}, |
|||
feesWithCurrencyEffect: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: false, |
|||
initialValue: new Big(0), |
|||
initialValueWithCurrencyEffect: new Big(0), |
|||
investmentValuesAccumulated: {}, |
|||
investmentValuesAccumulatedWithCurrencyEffect: {}, |
|||
investmentValuesWithCurrencyEffect: {}, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffectMap: {}, |
|||
netPerformanceValues: {}, |
|||
netPerformanceValuesWithCurrencyEffect: {}, |
|||
netPerformanceWithCurrencyEffectMap: {}, |
|||
timeWeightedInvestment: new Big(0), |
|||
timeWeightedInvestmentValues: {}, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
|||
totalAccountBalanceInBaseCurrency: 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), |
|||
totalLiabilities: new Big(0), |
|||
totalLiabilitiesInBaseCurrency: new Big(0), |
|||
totalValuables: new Big(0), |
|||
totalValuablesInBaseCurrency: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
const dateOfFirstTransaction = new Date(orders[0].date); |
|||
|
|||
const endDateString = format(end, DATE_FORMAT); |
|||
const startDateString = format(start, DATE_FORMAT); |
|||
|
|||
const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol]; |
|||
const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol]; |
|||
|
|||
if ( |
|||
!unitPriceAtEndDate || |
|||
(!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) |
|||
) { |
|||
return { |
|||
currentValues: {}, |
|||
currentValuesWithCurrencyEffect: {}, |
|||
feesWithCurrencyEffect: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: true, |
|||
initialValue: new Big(0), |
|||
initialValueWithCurrencyEffect: new Big(0), |
|||
investmentValuesAccumulated: {}, |
|||
investmentValuesAccumulatedWithCurrencyEffect: {}, |
|||
investmentValuesWithCurrencyEffect: {}, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffectMap: {}, |
|||
netPerformanceWithCurrencyEffectMap: {}, |
|||
netPerformanceValues: {}, |
|||
netPerformanceValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestment: new Big(0), |
|||
timeWeightedInvestmentValues: {}, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
|||
totalAccountBalanceInBaseCurrency: 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), |
|||
totalLiabilities: new Big(0), |
|||
totalLiabilitiesInBaseCurrency: new Big(0), |
|||
totalValuables: new Big(0), |
|||
totalValuablesInBaseCurrency: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
// Add a synthetic order at the start and the end date
|
|||
orders.push({ |
|||
date: startDateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
itemType: 'start', |
|||
quantity: new Big(0), |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
type: 'BUY', |
|||
unitPrice: unitPriceAtStartDate |
|||
}); |
|||
|
|||
orders.push({ |
|||
date: endDateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
itemType: 'end', |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
quantity: new Big(0), |
|||
type: 'BUY', |
|||
unitPrice: unitPriceAtEndDate |
|||
}); |
|||
|
|||
let lastUnitPrice: Big; |
|||
|
|||
const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; |
|||
|
|||
for (const order of orders) { |
|||
ordersByDate[order.date] = ordersByDate[order.date] ?? []; |
|||
ordersByDate[order.date].push(order); |
|||
} |
|||
|
|||
if (!this.chartDates) { |
|||
this.chartDates = Object.keys(chartDateMap).sort(); |
|||
} |
|||
|
|||
for (const dateString of this.chartDates) { |
|||
if (dateString < startDateString) { |
|||
continue; |
|||
} else if (dateString > endDateString) { |
|||
break; |
|||
} |
|||
|
|||
if (ordersByDate[dateString]?.length > 0) { |
|||
for (const order of ordersByDate[dateString]) { |
|||
order.unitPriceFromMarketData = |
|||
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; |
|||
} |
|||
} else { |
|||
orders.push({ |
|||
date: dateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
quantity: new Big(0), |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
type: 'BUY', |
|||
unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice, |
|||
unitPriceFromMarketData: |
|||
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice |
|||
}); |
|||
} |
|||
|
|||
const lastOrder = orders.at(-1); |
|||
|
|||
lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; |
|||
} |
|||
|
|||
// Sort orders so that the start and end placeholder order are at the correct
|
|||
// position
|
|||
orders = sortBy(orders, ({ date, itemType }) => { |
|||
let sortIndex = new Date(date); |
|||
|
|||
if (itemType === 'end') { |
|||
sortIndex = addMilliseconds(sortIndex, 1); |
|||
} else if (itemType === 'start') { |
|||
sortIndex = addMilliseconds(sortIndex, -1); |
|||
} |
|||
|
|||
return sortIndex.getTime(); |
|||
}); |
|||
|
|||
const indexOfStartOrder = orders.findIndex(({ itemType }) => { |
|||
return itemType === 'start'; |
|||
}); |
|||
|
|||
const indexOfEndOrder = orders.findIndex(({ itemType }) => { |
|||
return itemType === 'end'; |
|||
}); |
|||
|
|||
let totalInvestmentDays = 0; |
|||
let sumOfTimeWeightedInvestments = new Big(0); |
|||
let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); |
|||
|
|||
for (let i = 0; i < orders.length; i += 1) { |
|||
const order = orders[i]; |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log(); |
|||
console.log(); |
|||
console.log( |
|||
i + 1, |
|||
order.date, |
|||
order.type, |
|||
order.itemType ? `(${order.itemType})` : '' |
|||
); |
|||
} |
|||
|
|||
const exchangeRateAtOrderDate = exchangeRates[order.date]; |
|||
|
|||
if (order.type === 'DIVIDEND') { |
|||
const dividend = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalDividend = totalDividend.plus(dividend); |
|||
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) |
|||
); |
|||
} |
|||
|
|||
if (order.itemType === 'start') { |
|||
// Take the unit price of the order as the market price if there are no
|
|||
// orders of this symbol before the start date
|
|||
order.unitPrice = |
|||
indexOfStartOrder === 0 |
|||
? orders[i + 1]?.unitPrice |
|||
: unitPriceAtStartDate; |
|||
} |
|||
|
|||
if (order.fee) { |
|||
order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1); |
|||
order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul( |
|||
exchangeRateAtOrderDate ?? 1 |
|||
); |
|||
} |
|||
|
|||
const unitPrice = ['BUY', 'SELL'].includes(order.type) |
|||
? order.unitPrice |
|||
: order.unitPriceFromMarketData; |
|||
|
|||
if (unitPrice) { |
|||
order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1); |
|||
|
|||
order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( |
|||
exchangeRateAtOrderDate ?? 1 |
|||
); |
|||
} |
|||
|
|||
const valueOfInvestmentBeforeTransaction = totalUnits.mul( |
|||
order.unitPriceInBaseCurrency |
|||
); |
|||
|
|||
const valueOfInvestmentBeforeTransactionWithCurrencyEffect = |
|||
totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect); |
|||
|
|||
if (!investmentAtStartDate && i >= indexOfStartOrder) { |
|||
investmentAtStartDate = totalInvestment ?? new Big(0); |
|||
|
|||
investmentAtStartDateWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect ?? new Big(0); |
|||
|
|||
valueAtStartDate = valueOfInvestmentBeforeTransaction; |
|||
|
|||
valueAtStartDateWithCurrencyEffect = |
|||
valueOfInvestmentBeforeTransactionWithCurrencyEffect; |
|||
} |
|||
|
|||
let transactionInvestment = new Big(0); |
|||
let transactionInvestmentWithCurrencyEffect = new Big(0); |
|||
|
|||
if (order.type === 'BUY') { |
|||
transactionInvestment = order.quantity |
|||
.mul(order.unitPriceInBaseCurrency) |
|||
.mul(getFactor(order.type)); |
|||
|
|||
transactionInvestmentWithCurrencyEffect = order.quantity |
|||
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) |
|||
.mul(getFactor(order.type)); |
|||
|
|||
totalQuantityFromBuyTransactions = |
|||
totalQuantityFromBuyTransactions.plus(order.quantity); |
|||
|
|||
totalInvestmentFromBuyTransactions = |
|||
totalInvestmentFromBuyTransactions.plus(transactionInvestment); |
|||
|
|||
totalInvestmentFromBuyTransactionsWithCurrencyEffect = |
|||
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( |
|||
transactionInvestmentWithCurrencyEffect |
|||
); |
|||
} else if (order.type === 'SELL') { |
|||
if (totalUnits.gt(0)) { |
|||
transactionInvestment = totalInvestment |
|||
.div(totalUnits) |
|||
.mul(order.quantity) |
|||
.mul(getFactor(order.type)); |
|||
transactionInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect |
|||
.div(totalUnits) |
|||
.mul(order.quantity) |
|||
.mul(getFactor(order.type)); |
|||
} |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log('order.quantity', order.quantity.toNumber()); |
|||
console.log('transactionInvestment', transactionInvestment.toNumber()); |
|||
|
|||
console.log( |
|||
'transactionInvestmentWithCurrencyEffect', |
|||
transactionInvestmentWithCurrencyEffect.toNumber() |
|||
); |
|||
} |
|||
|
|||
const totalInvestmentBeforeTransaction = totalInvestment; |
|||
|
|||
const totalInvestmentBeforeTransactionWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect; |
|||
|
|||
totalInvestment = totalInvestment.plus(transactionInvestment); |
|||
|
|||
totalInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect.plus( |
|||
transactionInvestmentWithCurrencyEffect |
|||
); |
|||
|
|||
if (i >= indexOfStartOrder && !initialValue) { |
|||
if ( |
|||
i === indexOfStartOrder && |
|||
!valueOfInvestmentBeforeTransaction.eq(0) |
|||
) { |
|||
initialValue = valueOfInvestmentBeforeTransaction; |
|||
|
|||
initialValueWithCurrencyEffect = |
|||
valueOfInvestmentBeforeTransactionWithCurrencyEffect; |
|||
} else if (transactionInvestment.gt(0)) { |
|||
initialValue = transactionInvestment; |
|||
|
|||
initialValueWithCurrencyEffect = |
|||
transactionInvestmentWithCurrencyEffect; |
|||
} |
|||
} |
|||
|
|||
fees = fees.plus(order.feeInBaseCurrency ?? 0); |
|||
|
|||
feesWithCurrencyEffect = feesWithCurrencyEffect.plus( |
|||
order.feeInBaseCurrencyWithCurrencyEffect ?? 0 |
|||
); |
|||
|
|||
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); |
|||
|
|||
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); |
|||
|
|||
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( |
|||
order.unitPriceInBaseCurrencyWithCurrencyEffect |
|||
); |
|||
|
|||
const grossPerformanceFromSell = |
|||
order.type === 'SELL' |
|||
? order.unitPriceInBaseCurrency |
|||
.minus(lastAveragePrice) |
|||
.mul(order.quantity) |
|||
: new Big(0); |
|||
|
|||
const grossPerformanceFromSellWithCurrencyEffect = |
|||
order.type === 'SELL' |
|||
? order.unitPriceInBaseCurrencyWithCurrencyEffect |
|||
.minus(lastAveragePriceWithCurrencyEffect) |
|||
.mul(order.quantity) |
|||
: new Big(0); |
|||
|
|||
grossPerformanceFromSells = grossPerformanceFromSells.plus( |
|||
grossPerformanceFromSell |
|||
); |
|||
|
|||
grossPerformanceFromSellsWithCurrencyEffect = |
|||
grossPerformanceFromSellsWithCurrencyEffect.plus( |
|||
grossPerformanceFromSellWithCurrencyEffect |
|||
); |
|||
|
|||
lastAveragePrice = totalQuantityFromBuyTransactions.eq(0) |
|||
? new Big(0) |
|||
: totalInvestmentFromBuyTransactions.div( |
|||
totalQuantityFromBuyTransactions |
|||
); |
|||
|
|||
lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq( |
|||
0 |
|||
) |
|||
? new Big(0) |
|||
: totalInvestmentFromBuyTransactionsWithCurrencyEffect.div( |
|||
totalQuantityFromBuyTransactions |
|||
); |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log( |
|||
'grossPerformanceFromSells', |
|||
grossPerformanceFromSells.toNumber() |
|||
); |
|||
console.log( |
|||
'grossPerformanceFromSellWithCurrencyEffect', |
|||
grossPerformanceFromSellWithCurrencyEffect.toNumber() |
|||
); |
|||
} |
|||
|
|||
const newGrossPerformance = valueOfInvestment |
|||
.minus(totalInvestment) |
|||
.plus(grossPerformanceFromSells); |
|||
|
|||
const newGrossPerformanceWithCurrencyEffect = |
|||
valueOfInvestmentWithCurrencyEffect |
|||
.minus(totalInvestmentWithCurrencyEffect) |
|||
.plus(grossPerformanceFromSellsWithCurrencyEffect); |
|||
|
|||
grossPerformance = newGrossPerformance; |
|||
|
|||
grossPerformanceWithCurrencyEffect = |
|||
newGrossPerformanceWithCurrencyEffect; |
|||
|
|||
if (order.itemType === 'start') { |
|||
feesAtStartDate = fees; |
|||
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; |
|||
grossPerformanceAtStartDate = grossPerformance; |
|||
|
|||
grossPerformanceAtStartDateWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect; |
|||
} |
|||
|
|||
if (i > indexOfStartOrder) { |
|||
// Only consider periods with an investment for the calculation of
|
|||
// the time weighted investment
|
|||
if ( |
|||
valueOfInvestmentBeforeTransaction.gt(0) && |
|||
['BUY', 'SELL'].includes(order.type) |
|||
) { |
|||
// Calculate the number of days since the previous order
|
|||
const orderDate = new Date(order.date); |
|||
const previousOrderDate = new Date(orders[i - 1].date); |
|||
|
|||
let daysSinceLastOrder = differenceInDays( |
|||
orderDate, |
|||
previousOrderDate |
|||
); |
|||
if (daysSinceLastOrder <= 0) { |
|||
// The time between two activities on the same day is unknown
|
|||
// -> Set it to the smallest floating point number greater than 0
|
|||
daysSinceLastOrder = Number.EPSILON; |
|||
} |
|||
|
|||
// Sum up the total investment days since the start date to calculate
|
|||
// the time weighted investment
|
|||
totalInvestmentDays += daysSinceLastOrder; |
|||
|
|||
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add( |
|||
valueAtStartDate |
|||
.minus(investmentAtStartDate) |
|||
.plus(totalInvestmentBeforeTransaction) |
|||
.mul(daysSinceLastOrder) |
|||
); |
|||
|
|||
sumOfTimeWeightedInvestmentsWithCurrencyEffect = |
|||
sumOfTimeWeightedInvestmentsWithCurrencyEffect.add( |
|||
valueAtStartDateWithCurrencyEffect |
|||
.minus(investmentAtStartDateWithCurrencyEffect) |
|||
.plus(totalInvestmentBeforeTransactionWithCurrencyEffect) |
|||
.mul(daysSinceLastOrder) |
|||
); |
|||
} |
|||
|
|||
currentValues[order.date] = valueOfInvestment; |
|||
|
|||
currentValuesWithCurrencyEffect[order.date] = |
|||
valueOfInvestmentWithCurrencyEffect; |
|||
|
|||
netPerformanceValues[order.date] = grossPerformance |
|||
.minus(grossPerformanceAtStartDate) |
|||
.minus(fees.minus(feesAtStartDate)); |
|||
|
|||
netPerformanceValuesWithCurrencyEffect[order.date] = |
|||
grossPerformanceWithCurrencyEffect |
|||
.minus(grossPerformanceAtStartDateWithCurrencyEffect) |
|||
.minus( |
|||
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) |
|||
); |
|||
|
|||
investmentValuesAccumulated[order.date] = totalInvestment; |
|||
|
|||
investmentValuesAccumulatedWithCurrencyEffect[order.date] = |
|||
totalInvestmentWithCurrencyEffect; |
|||
|
|||
investmentValuesWithCurrencyEffect[order.date] = ( |
|||
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) |
|||
).add(transactionInvestmentWithCurrencyEffect); |
|||
|
|||
timeWeightedInvestmentValues[order.date] = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) |
|||
: new Big(0); |
|||
|
|||
timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( |
|||
totalInvestmentDays |
|||
) |
|||
: new Big(0); |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log('totalInvestment', totalInvestment.toNumber()); |
|||
|
|||
console.log( |
|||
'totalInvestmentWithCurrencyEffect', |
|||
totalInvestmentWithCurrencyEffect.toNumber() |
|||
); |
|||
|
|||
console.log( |
|||
'totalGrossPerformance', |
|||
grossPerformance.minus(grossPerformanceAtStartDate).toNumber() |
|||
); |
|||
|
|||
console.log( |
|||
'totalGrossPerformanceWithCurrencyEffect', |
|||
grossPerformanceWithCurrencyEffect |
|||
.minus(grossPerformanceAtStartDateWithCurrencyEffect) |
|||
.toNumber() |
|||
); |
|||
} |
|||
|
|||
if (i === indexOfEndOrder) { |
|||
break; |
|||
} |
|||
} |
|||
|
|||
const totalGrossPerformance = grossPerformance.minus( |
|||
grossPerformanceAtStartDate |
|||
); |
|||
|
|||
const totalGrossPerformanceWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect.minus( |
|||
grossPerformanceAtStartDateWithCurrencyEffect |
|||
); |
|||
|
|||
const totalNetPerformance = grossPerformance |
|||
.minus(grossPerformanceAtStartDate) |
|||
.minus(fees.minus(feesAtStartDate)); |
|||
|
|||
const timeWeightedAverageInvestmentBetweenStartAndEndDate = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) |
|||
: new Big(0); |
|||
|
|||
const timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( |
|||
totalInvestmentDays |
|||
) |
|||
: new Big(0); |
|||
|
|||
const grossPerformancePercentage = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) |
|||
? totalGrossPerformance.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate |
|||
) |
|||
: new Big(0); |
|||
|
|||
const grossPerformancePercentageWithCurrencyEffect = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt( |
|||
0 |
|||
) |
|||
? totalGrossPerformanceWithCurrencyEffect.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|||
) |
|||
: new Big(0); |
|||
|
|||
const feesPerUnit = totalUnits.gt(0) |
|||
? fees.minus(feesAtStartDate).div(totalUnits) |
|||
: new Big(0); |
|||
|
|||
const feesPerUnitWithCurrencyEffect = totalUnits.gt(0) |
|||
? feesWithCurrencyEffect |
|||
.minus(feesAtStartDateWithCurrencyEffect) |
|||
.div(totalUnits) |
|||
: new Big(0); |
|||
|
|||
const netPerformancePercentage = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) |
|||
? totalNetPerformance.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate |
|||
) |
|||
: new Big(0); |
|||
|
|||
const netPerformancePercentageWithCurrencyEffectMap: { |
|||
[key: DateRange]: Big; |
|||
} = {}; |
|||
|
|||
const netPerformanceWithCurrencyEffectMap: { |
|||
[key: DateRange]: Big; |
|||
} = {}; |
|||
|
|||
for (const dateRange of [ |
|||
'1d', |
|||
'1y', |
|||
'5y', |
|||
'max', |
|||
'mtd', |
|||
'wtd', |
|||
'ytd' |
|||
// TODO:
|
|||
// ...eachYearOfInterval({ end, start })
|
|||
// .filter((date) => {
|
|||
// return !isThisYear(date);
|
|||
// })
|
|||
// .map((date) => {
|
|||
// return format(date, 'yyyy');
|
|||
// })
|
|||
] as DateRange[]) { |
|||
const dateInterval = getIntervalFromDateRange(dateRange); |
|||
const endDate = dateInterval.endDate; |
|||
let startDate = dateInterval.startDate; |
|||
|
|||
if (isBefore(startDate, start)) { |
|||
startDate = start; |
|||
} |
|||
|
|||
const rangeEndDateString = format(endDate, DATE_FORMAT); |
|||
const rangeStartDateString = format(startDate, DATE_FORMAT); |
|||
|
|||
const currentValuesAtDateRangeStartWithCurrencyEffect = |
|||
currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0); |
|||
|
|||
const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = |
|||
investmentValuesAccumulatedWithCurrencyEffect[rangeStartDateString] ?? |
|||
new Big(0); |
|||
|
|||
const grossPerformanceAtDateRangeStartWithCurrencyEffect = |
|||
currentValuesAtDateRangeStartWithCurrencyEffect.minus( |
|||
investmentValuesAccumulatedAtStartDateWithCurrencyEffect |
|||
); |
|||
|
|||
let average = new Big(0); |
|||
let dayCount = 0; |
|||
|
|||
for (let i = this.chartDates.length - 1; i >= 0; i -= 1) { |
|||
const date = this.chartDates[i]; |
|||
|
|||
if (date > rangeEndDateString) { |
|||
continue; |
|||
} else if (date < rangeStartDateString) { |
|||
break; |
|||
} |
|||
|
|||
if ( |
|||
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && |
|||
investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) |
|||
) { |
|||
average = average.add( |
|||
investmentValuesAccumulatedWithCurrencyEffect[date].add( |
|||
grossPerformanceAtDateRangeStartWithCurrencyEffect |
|||
) |
|||
); |
|||
|
|||
dayCount++; |
|||
} |
|||
} |
|||
|
|||
if (dayCount > 0) { |
|||
average = average.div(dayCount); |
|||
} |
|||
|
|||
netPerformanceWithCurrencyEffectMap[dateRange] = |
|||
netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus( |
|||
// If the date range is 'max', take 0 as a start value. Otherwise,
|
|||
// the value of the end of the day of the start date is taken which
|
|||
// differs from the buying price.
|
|||
dateRange === 'max' |
|||
? new Big(0) |
|||
: (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ?? |
|||
new Big(0)) |
|||
) ?? new Big(0); |
|||
|
|||
netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) |
|||
? netPerformanceWithCurrencyEffectMap[dateRange].div(average) |
|||
: new Big(0); |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log( |
|||
` |
|||
${symbol} |
|||
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed( |
|||
2 |
|||
)} -> ${unitPriceAtEndDate.toFixed(2)} |
|||
Total investment: ${totalInvestment.toFixed(2)} |
|||
Total investment with currency effect: ${totalInvestmentWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed( |
|||
2 |
|||
)} |
|||
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Total dividend: ${totalDividend.toFixed(2)} |
|||
Gross performance: ${totalGrossPerformance.toFixed( |
|||
2 |
|||
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}% |
|||
Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} / ${grossPerformancePercentageWithCurrencyEffect |
|||
.mul(100) |
|||
.toFixed(2)}% |
|||
Fees per unit: ${feesPerUnit.toFixed(2)} |
|||
Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Net performance: ${totalNetPerformance.toFixed( |
|||
2 |
|||
)} / ${netPerformancePercentage.mul(100).toFixed(2)}% |
|||
Net performance with currency effect: ${netPerformancePercentageWithCurrencyEffectMap[ |
|||
'max' |
|||
].toFixed(2)}%` |
|||
); |
|||
} |
|||
|
|||
return { |
|||
currentValues, |
|||
currentValuesWithCurrencyEffect, |
|||
feesWithCurrencyEffect, |
|||
grossPerformancePercentage, |
|||
grossPerformancePercentageWithCurrencyEffect, |
|||
initialValue, |
|||
initialValueWithCurrencyEffect, |
|||
investmentValuesAccumulated, |
|||
investmentValuesAccumulatedWithCurrencyEffect, |
|||
investmentValuesWithCurrencyEffect, |
|||
netPerformancePercentage, |
|||
netPerformancePercentageWithCurrencyEffectMap, |
|||
netPerformanceValues, |
|||
netPerformanceValuesWithCurrencyEffect, |
|||
netPerformanceWithCurrencyEffectMap, |
|||
timeWeightedInvestmentValues, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect, |
|||
totalAccountBalanceInBaseCurrency, |
|||
totalDividend, |
|||
totalDividendInBaseCurrency, |
|||
totalInterest, |
|||
totalInterestInBaseCurrency, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect, |
|||
totalLiabilities, |
|||
totalLiabilitiesInBaseCurrency, |
|||
totalValuables, |
|||
totalValuablesInBaseCurrency, |
|||
grossPerformance: totalGrossPerformance, |
|||
grossPerformanceWithCurrencyEffect: |
|||
totalGrossPerformanceWithCurrencyEffect, |
|||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), |
|||
netPerformance: totalNetPerformance, |
|||
timeWeightedInvestment: |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate, |
|||
timeWeightedInvestmentWithCurrencyEffect: |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|||
}; |
|||
} |
|||
} |
@ -1,969 +1,24 @@ |
|||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; |
|||
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; |
|||
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; |
|||
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; |
|||
import { DATE_FORMAT } from '@ghostfolio/common/helper'; |
|||
import { |
|||
AssetProfileIdentifier, |
|||
SymbolMetrics |
|||
} from '@ghostfolio/common/interfaces'; |
|||
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; |
|||
import { DateRange } from '@ghostfolio/common/types'; |
|||
import { PortfolioSnapshot } from '@ghostfolio/common/models'; |
|||
|
|||
import { Logger } from '@nestjs/common'; |
|||
import { Big } from 'big.js'; |
|||
import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns'; |
|||
import { cloneDeep, sortBy } from 'lodash'; |
|||
|
|||
export class TWRPortfolioCalculator extends PortfolioCalculator { |
|||
private chartDates: string[]; |
|||
|
|||
protected calculateOverallPerformance( |
|||
positions: TimelinePosition[] |
|||
): 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 totalFeesWithCurrencyEffect = new Big(0); |
|||
const 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.feeInBaseCurrency) { |
|||
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( |
|||
currentPosition.feeInBaseCurrency |
|||
); |
|||
} |
|||
|
|||
if (currentPosition.valueInBaseCurrency) { |
|||
currentValueInBaseCurrency = currentValueInBaseCurrency.plus( |
|||
currentPosition.valueInBaseCurrency |
|||
); |
|||
} else { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.investment) { |
|||
totalInvestment = totalInvestment.plus(currentPosition.investment); |
|||
|
|||
totalInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect.plus( |
|||
currentPosition.investmentWithCurrencyEffect |
|||
); |
|||
} else { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.grossPerformance) { |
|||
grossPerformance = grossPerformance.plus( |
|||
currentPosition.grossPerformance |
|||
); |
|||
|
|||
grossPerformanceWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect.plus( |
|||
currentPosition.grossPerformanceWithCurrencyEffect |
|||
); |
|||
|
|||
netPerformance = netPerformance.plus(currentPosition.netPerformance); |
|||
} else if (!currentPosition.quantity.eq(0)) { |
|||
hasErrors = true; |
|||
} |
|||
|
|||
if (currentPosition.timeWeightedInvestment) { |
|||
totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus( |
|||
currentPosition.timeWeightedInvestment |
|||
); |
|||
|
|||
totalTimeWeightedInvestmentWithCurrencyEffect = |
|||
totalTimeWeightedInvestmentWithCurrencyEffect.plus( |
|||
currentPosition.timeWeightedInvestmentWithCurrencyEffect |
|||
); |
|||
} else if (!currentPosition.quantity.eq(0)) { |
|||
Logger.warn( |
|||
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`, |
|||
'PortfolioCalculator' |
|||
); |
|||
|
|||
hasErrors = true; |
|||
} |
|||
} |
|||
|
|||
return { |
|||
currentValueInBaseCurrency, |
|||
hasErrors, |
|||
positions, |
|||
totalFeesWithCurrencyEffect, |
|||
totalInterestWithCurrencyEffect, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect, |
|||
activitiesCount: this.activities.filter(({ type }) => { |
|||
return ['BUY', 'SELL'].includes(type); |
|||
}).length, |
|||
createdAt: new Date(), |
|||
errors: [], |
|||
historicalData: [], |
|||
totalLiabilitiesWithCurrencyEffect: new Big(0), |
|||
totalValuablesWithCurrencyEffect: new Big(0) |
|||
}; |
|||
export class TwrPortfolioCalculator extends PortfolioCalculator { |
|||
protected calculateOverallPerformance(): PortfolioSnapshot { |
|||
throw new Error('Method not implemented.'); |
|||
} |
|||
|
|||
protected getSymbolMetrics({ |
|||
chartDateMap, |
|||
dataSource, |
|||
end, |
|||
exchangeRates, |
|||
marketSymbolMap, |
|||
start, |
|||
symbol |
|||
}: { |
|||
chartDateMap?: { [date: string]: boolean }; |
|||
protected getSymbolMetrics({}: { |
|||
end: Date; |
|||
exchangeRates: { [dateString: string]: number }; |
|||
marketSymbolMap: { |
|||
[date: string]: { [symbol: string]: Big }; |
|||
}; |
|||
start: Date; |
|||
step?: number; |
|||
} & AssetProfileIdentifier): SymbolMetrics { |
|||
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; |
|||
const currentValues: { [date: string]: Big } = {}; |
|||
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
let fees = new Big(0); |
|||
let feesAtStartDate = new Big(0); |
|||
let feesAtStartDateWithCurrencyEffect = new Big(0); |
|||
let feesWithCurrencyEffect = new Big(0); |
|||
let grossPerformance = new Big(0); |
|||
let grossPerformanceWithCurrencyEffect = new Big(0); |
|||
let grossPerformanceAtStartDate = new Big(0); |
|||
let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0); |
|||
let grossPerformanceFromSells = new Big(0); |
|||
let grossPerformanceFromSellsWithCurrencyEffect = new Big(0); |
|||
let initialValue: Big; |
|||
let initialValueWithCurrencyEffect: Big; |
|||
let investmentAtStartDate: Big; |
|||
let investmentAtStartDateWithCurrencyEffect: Big; |
|||
const investmentValuesAccumulated: { [date: string]: Big } = {}; |
|||
const investmentValuesAccumulatedWithCurrencyEffect: { |
|||
[date: string]: Big; |
|||
} = {}; |
|||
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
let lastAveragePrice = new Big(0); |
|||
let lastAveragePriceWithCurrencyEffect = new Big(0); |
|||
const netPerformanceValues: { [date: string]: Big } = {}; |
|||
const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {}; |
|||
const timeWeightedInvestmentValues: { [date: string]: Big } = {}; |
|||
|
|||
const timeWeightedInvestmentValuesWithCurrencyEffect: { |
|||
[date: string]: Big; |
|||
} = {}; |
|||
|
|||
const totalAccountBalanceInBaseCurrency = new Big(0); |
|||
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; |
|||
|
|||
// Clone orders to keep the original values in this.orders
|
|||
let orders: PortfolioOrderItem[] = cloneDeep( |
|||
this.activities.filter(({ SymbolProfile }) => { |
|||
return SymbolProfile.symbol === symbol; |
|||
}) |
|||
); |
|||
|
|||
if (orders.length <= 0) { |
|||
return { |
|||
currentValues: {}, |
|||
currentValuesWithCurrencyEffect: {}, |
|||
feesWithCurrencyEffect: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: false, |
|||
initialValue: new Big(0), |
|||
initialValueWithCurrencyEffect: new Big(0), |
|||
investmentValuesAccumulated: {}, |
|||
investmentValuesAccumulatedWithCurrencyEffect: {}, |
|||
investmentValuesWithCurrencyEffect: {}, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffectMap: {}, |
|||
netPerformanceValues: {}, |
|||
netPerformanceValuesWithCurrencyEffect: {}, |
|||
netPerformanceWithCurrencyEffectMap: {}, |
|||
timeWeightedInvestment: new Big(0), |
|||
timeWeightedInvestmentValues: {}, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
|||
totalAccountBalanceInBaseCurrency: 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), |
|||
totalLiabilities: new Big(0), |
|||
totalLiabilitiesInBaseCurrency: new Big(0), |
|||
totalValuables: new Big(0), |
|||
totalValuablesInBaseCurrency: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
const dateOfFirstTransaction = new Date(orders[0].date); |
|||
|
|||
const endDateString = format(end, DATE_FORMAT); |
|||
const startDateString = format(start, DATE_FORMAT); |
|||
|
|||
const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol]; |
|||
const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol]; |
|||
|
|||
if ( |
|||
!unitPriceAtEndDate || |
|||
(!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) |
|||
) { |
|||
return { |
|||
currentValues: {}, |
|||
currentValuesWithCurrencyEffect: {}, |
|||
feesWithCurrencyEffect: new Big(0), |
|||
grossPerformance: new Big(0), |
|||
grossPerformancePercentage: new Big(0), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big(0), |
|||
grossPerformanceWithCurrencyEffect: new Big(0), |
|||
hasErrors: true, |
|||
initialValue: new Big(0), |
|||
initialValueWithCurrencyEffect: new Big(0), |
|||
investmentValuesAccumulated: {}, |
|||
investmentValuesAccumulatedWithCurrencyEffect: {}, |
|||
investmentValuesWithCurrencyEffect: {}, |
|||
netPerformance: new Big(0), |
|||
netPerformancePercentage: new Big(0), |
|||
netPerformancePercentageWithCurrencyEffectMap: {}, |
|||
netPerformanceWithCurrencyEffectMap: {}, |
|||
netPerformanceValues: {}, |
|||
netPerformanceValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestment: new Big(0), |
|||
timeWeightedInvestmentValues: {}, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect: {}, |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big(0), |
|||
totalAccountBalanceInBaseCurrency: 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), |
|||
totalLiabilities: new Big(0), |
|||
totalLiabilitiesInBaseCurrency: new Big(0), |
|||
totalValuables: new Big(0), |
|||
totalValuablesInBaseCurrency: new Big(0) |
|||
}; |
|||
} |
|||
|
|||
// Add a synthetic order at the start and the end date
|
|||
orders.push({ |
|||
date: startDateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
itemType: 'start', |
|||
quantity: new Big(0), |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
type: 'BUY', |
|||
unitPrice: unitPriceAtStartDate |
|||
}); |
|||
|
|||
orders.push({ |
|||
date: endDateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
itemType: 'end', |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
quantity: new Big(0), |
|||
type: 'BUY', |
|||
unitPrice: unitPriceAtEndDate |
|||
}); |
|||
|
|||
let lastUnitPrice: Big; |
|||
|
|||
const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; |
|||
|
|||
for (const order of orders) { |
|||
ordersByDate[order.date] = ordersByDate[order.date] ?? []; |
|||
ordersByDate[order.date].push(order); |
|||
} |
|||
|
|||
if (!this.chartDates) { |
|||
this.chartDates = Object.keys(chartDateMap).sort(); |
|||
} |
|||
|
|||
for (const dateString of this.chartDates) { |
|||
if (dateString < startDateString) { |
|||
continue; |
|||
} else if (dateString > endDateString) { |
|||
break; |
|||
} |
|||
|
|||
if (ordersByDate[dateString]?.length > 0) { |
|||
for (const order of ordersByDate[dateString]) { |
|||
order.unitPriceFromMarketData = |
|||
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; |
|||
} |
|||
} else { |
|||
orders.push({ |
|||
date: dateString, |
|||
fee: new Big(0), |
|||
feeInBaseCurrency: new Big(0), |
|||
quantity: new Big(0), |
|||
SymbolProfile: { |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
type: 'BUY', |
|||
unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice, |
|||
unitPriceFromMarketData: |
|||
marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice |
|||
}); |
|||
} |
|||
|
|||
const lastOrder = orders.at(-1); |
|||
|
|||
lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; |
|||
} |
|||
|
|||
// Sort orders so that the start and end placeholder order are at the correct
|
|||
// position
|
|||
orders = sortBy(orders, ({ date, itemType }) => { |
|||
let sortIndex = new Date(date); |
|||
|
|||
if (itemType === 'end') { |
|||
sortIndex = addMilliseconds(sortIndex, 1); |
|||
} else if (itemType === 'start') { |
|||
sortIndex = addMilliseconds(sortIndex, -1); |
|||
} |
|||
|
|||
return sortIndex.getTime(); |
|||
}); |
|||
|
|||
const indexOfStartOrder = orders.findIndex(({ itemType }) => { |
|||
return itemType === 'start'; |
|||
}); |
|||
|
|||
const indexOfEndOrder = orders.findIndex(({ itemType }) => { |
|||
return itemType === 'end'; |
|||
}); |
|||
|
|||
let totalInvestmentDays = 0; |
|||
let sumOfTimeWeightedInvestments = new Big(0); |
|||
let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); |
|||
|
|||
for (let i = 0; i < orders.length; i += 1) { |
|||
const order = orders[i]; |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log(); |
|||
console.log(); |
|||
console.log( |
|||
i + 1, |
|||
order.date, |
|||
order.type, |
|||
order.itemType ? `(${order.itemType})` : '' |
|||
); |
|||
} |
|||
|
|||
const exchangeRateAtOrderDate = exchangeRates[order.date]; |
|||
|
|||
if (order.type === 'DIVIDEND') { |
|||
const dividend = order.quantity.mul(order.unitPrice); |
|||
|
|||
totalDividend = totalDividend.plus(dividend); |
|||
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) |
|||
); |
|||
} |
|||
|
|||
if (order.itemType === 'start') { |
|||
// Take the unit price of the order as the market price if there are no
|
|||
// orders of this symbol before the start date
|
|||
order.unitPrice = |
|||
indexOfStartOrder === 0 |
|||
? orders[i + 1]?.unitPrice |
|||
: unitPriceAtStartDate; |
|||
} |
|||
|
|||
if (order.fee) { |
|||
order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1); |
|||
order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul( |
|||
exchangeRateAtOrderDate ?? 1 |
|||
); |
|||
} |
|||
|
|||
const unitPrice = ['BUY', 'SELL'].includes(order.type) |
|||
? order.unitPrice |
|||
: order.unitPriceFromMarketData; |
|||
|
|||
if (unitPrice) { |
|||
order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1); |
|||
|
|||
order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( |
|||
exchangeRateAtOrderDate ?? 1 |
|||
); |
|||
} |
|||
|
|||
const valueOfInvestmentBeforeTransaction = totalUnits.mul( |
|||
order.unitPriceInBaseCurrency |
|||
); |
|||
|
|||
const valueOfInvestmentBeforeTransactionWithCurrencyEffect = |
|||
totalUnits.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect); |
|||
|
|||
if (!investmentAtStartDate && i >= indexOfStartOrder) { |
|||
investmentAtStartDate = totalInvestment ?? new Big(0); |
|||
|
|||
investmentAtStartDateWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect ?? new Big(0); |
|||
|
|||
valueAtStartDate = valueOfInvestmentBeforeTransaction; |
|||
|
|||
valueAtStartDateWithCurrencyEffect = |
|||
valueOfInvestmentBeforeTransactionWithCurrencyEffect; |
|||
} |
|||
|
|||
let transactionInvestment = new Big(0); |
|||
let transactionInvestmentWithCurrencyEffect = new Big(0); |
|||
|
|||
if (order.type === 'BUY') { |
|||
transactionInvestment = order.quantity |
|||
.mul(order.unitPriceInBaseCurrency) |
|||
.mul(getFactor(order.type)); |
|||
|
|||
transactionInvestmentWithCurrencyEffect = order.quantity |
|||
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) |
|||
.mul(getFactor(order.type)); |
|||
|
|||
totalQuantityFromBuyTransactions = |
|||
totalQuantityFromBuyTransactions.plus(order.quantity); |
|||
|
|||
totalInvestmentFromBuyTransactions = |
|||
totalInvestmentFromBuyTransactions.plus(transactionInvestment); |
|||
|
|||
totalInvestmentFromBuyTransactionsWithCurrencyEffect = |
|||
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( |
|||
transactionInvestmentWithCurrencyEffect |
|||
); |
|||
} else if (order.type === 'SELL') { |
|||
if (totalUnits.gt(0)) { |
|||
transactionInvestment = totalInvestment |
|||
.div(totalUnits) |
|||
.mul(order.quantity) |
|||
.mul(getFactor(order.type)); |
|||
transactionInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect |
|||
.div(totalUnits) |
|||
.mul(order.quantity) |
|||
.mul(getFactor(order.type)); |
|||
} |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log('order.quantity', order.quantity.toNumber()); |
|||
console.log('transactionInvestment', transactionInvestment.toNumber()); |
|||
|
|||
console.log( |
|||
'transactionInvestmentWithCurrencyEffect', |
|||
transactionInvestmentWithCurrencyEffect.toNumber() |
|||
); |
|||
} |
|||
|
|||
const totalInvestmentBeforeTransaction = totalInvestment; |
|||
|
|||
const totalInvestmentBeforeTransactionWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect; |
|||
|
|||
totalInvestment = totalInvestment.plus(transactionInvestment); |
|||
|
|||
totalInvestmentWithCurrencyEffect = |
|||
totalInvestmentWithCurrencyEffect.plus( |
|||
transactionInvestmentWithCurrencyEffect |
|||
); |
|||
|
|||
if (i >= indexOfStartOrder && !initialValue) { |
|||
if ( |
|||
i === indexOfStartOrder && |
|||
!valueOfInvestmentBeforeTransaction.eq(0) |
|||
) { |
|||
initialValue = valueOfInvestmentBeforeTransaction; |
|||
|
|||
initialValueWithCurrencyEffect = |
|||
valueOfInvestmentBeforeTransactionWithCurrencyEffect; |
|||
} else if (transactionInvestment.gt(0)) { |
|||
initialValue = transactionInvestment; |
|||
|
|||
initialValueWithCurrencyEffect = |
|||
transactionInvestmentWithCurrencyEffect; |
|||
} |
|||
} |
|||
|
|||
fees = fees.plus(order.feeInBaseCurrency ?? 0); |
|||
|
|||
feesWithCurrencyEffect = feesWithCurrencyEffect.plus( |
|||
order.feeInBaseCurrencyWithCurrencyEffect ?? 0 |
|||
); |
|||
|
|||
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); |
|||
|
|||
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); |
|||
|
|||
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( |
|||
order.unitPriceInBaseCurrencyWithCurrencyEffect |
|||
); |
|||
|
|||
const grossPerformanceFromSell = |
|||
order.type === 'SELL' |
|||
? order.unitPriceInBaseCurrency |
|||
.minus(lastAveragePrice) |
|||
.mul(order.quantity) |
|||
: new Big(0); |
|||
|
|||
const grossPerformanceFromSellWithCurrencyEffect = |
|||
order.type === 'SELL' |
|||
? order.unitPriceInBaseCurrencyWithCurrencyEffect |
|||
.minus(lastAveragePriceWithCurrencyEffect) |
|||
.mul(order.quantity) |
|||
: new Big(0); |
|||
|
|||
grossPerformanceFromSells = grossPerformanceFromSells.plus( |
|||
grossPerformanceFromSell |
|||
); |
|||
|
|||
grossPerformanceFromSellsWithCurrencyEffect = |
|||
grossPerformanceFromSellsWithCurrencyEffect.plus( |
|||
grossPerformanceFromSellWithCurrencyEffect |
|||
); |
|||
|
|||
lastAveragePrice = totalQuantityFromBuyTransactions.eq(0) |
|||
? new Big(0) |
|||
: totalInvestmentFromBuyTransactions.div( |
|||
totalQuantityFromBuyTransactions |
|||
); |
|||
|
|||
lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq( |
|||
0 |
|||
) |
|||
? new Big(0) |
|||
: totalInvestmentFromBuyTransactionsWithCurrencyEffect.div( |
|||
totalQuantityFromBuyTransactions |
|||
); |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log( |
|||
'grossPerformanceFromSells', |
|||
grossPerformanceFromSells.toNumber() |
|||
); |
|||
console.log( |
|||
'grossPerformanceFromSellWithCurrencyEffect', |
|||
grossPerformanceFromSellWithCurrencyEffect.toNumber() |
|||
); |
|||
} |
|||
|
|||
const newGrossPerformance = valueOfInvestment |
|||
.minus(totalInvestment) |
|||
.plus(grossPerformanceFromSells); |
|||
|
|||
const newGrossPerformanceWithCurrencyEffect = |
|||
valueOfInvestmentWithCurrencyEffect |
|||
.minus(totalInvestmentWithCurrencyEffect) |
|||
.plus(grossPerformanceFromSellsWithCurrencyEffect); |
|||
|
|||
grossPerformance = newGrossPerformance; |
|||
|
|||
grossPerformanceWithCurrencyEffect = |
|||
newGrossPerformanceWithCurrencyEffect; |
|||
|
|||
if (order.itemType === 'start') { |
|||
feesAtStartDate = fees; |
|||
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; |
|||
grossPerformanceAtStartDate = grossPerformance; |
|||
|
|||
grossPerformanceAtStartDateWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect; |
|||
} |
|||
|
|||
if (i > indexOfStartOrder) { |
|||
// Only consider periods with an investment for the calculation of
|
|||
// the time weighted investment
|
|||
if ( |
|||
valueOfInvestmentBeforeTransaction.gt(0) && |
|||
['BUY', 'SELL'].includes(order.type) |
|||
) { |
|||
// Calculate the number of days since the previous order
|
|||
const orderDate = new Date(order.date); |
|||
const previousOrderDate = new Date(orders[i - 1].date); |
|||
|
|||
let daysSinceLastOrder = differenceInDays( |
|||
orderDate, |
|||
previousOrderDate |
|||
); |
|||
if (daysSinceLastOrder <= 0) { |
|||
// The time between two activities on the same day is unknown
|
|||
// -> Set it to the smallest floating point number greater than 0
|
|||
daysSinceLastOrder = Number.EPSILON; |
|||
} |
|||
|
|||
// Sum up the total investment days since the start date to calculate
|
|||
// the time weighted investment
|
|||
totalInvestmentDays += daysSinceLastOrder; |
|||
|
|||
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add( |
|||
valueAtStartDate |
|||
.minus(investmentAtStartDate) |
|||
.plus(totalInvestmentBeforeTransaction) |
|||
.mul(daysSinceLastOrder) |
|||
); |
|||
|
|||
sumOfTimeWeightedInvestmentsWithCurrencyEffect = |
|||
sumOfTimeWeightedInvestmentsWithCurrencyEffect.add( |
|||
valueAtStartDateWithCurrencyEffect |
|||
.minus(investmentAtStartDateWithCurrencyEffect) |
|||
.plus(totalInvestmentBeforeTransactionWithCurrencyEffect) |
|||
.mul(daysSinceLastOrder) |
|||
); |
|||
} |
|||
|
|||
currentValues[order.date] = valueOfInvestment; |
|||
|
|||
currentValuesWithCurrencyEffect[order.date] = |
|||
valueOfInvestmentWithCurrencyEffect; |
|||
|
|||
netPerformanceValues[order.date] = grossPerformance |
|||
.minus(grossPerformanceAtStartDate) |
|||
.minus(fees.minus(feesAtStartDate)); |
|||
|
|||
netPerformanceValuesWithCurrencyEffect[order.date] = |
|||
grossPerformanceWithCurrencyEffect |
|||
.minus(grossPerformanceAtStartDateWithCurrencyEffect) |
|||
.minus( |
|||
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) |
|||
); |
|||
|
|||
investmentValuesAccumulated[order.date] = totalInvestment; |
|||
|
|||
investmentValuesAccumulatedWithCurrencyEffect[order.date] = |
|||
totalInvestmentWithCurrencyEffect; |
|||
|
|||
investmentValuesWithCurrencyEffect[order.date] = ( |
|||
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) |
|||
).add(transactionInvestmentWithCurrencyEffect); |
|||
|
|||
timeWeightedInvestmentValues[order.date] = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) |
|||
: new Big(0); |
|||
|
|||
timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( |
|||
totalInvestmentDays |
|||
) |
|||
: new Big(0); |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log('totalInvestment', totalInvestment.toNumber()); |
|||
|
|||
console.log( |
|||
'totalInvestmentWithCurrencyEffect', |
|||
totalInvestmentWithCurrencyEffect.toNumber() |
|||
); |
|||
|
|||
console.log( |
|||
'totalGrossPerformance', |
|||
grossPerformance.minus(grossPerformanceAtStartDate).toNumber() |
|||
); |
|||
|
|||
console.log( |
|||
'totalGrossPerformanceWithCurrencyEffect', |
|||
grossPerformanceWithCurrencyEffect |
|||
.minus(grossPerformanceAtStartDateWithCurrencyEffect) |
|||
.toNumber() |
|||
); |
|||
} |
|||
|
|||
if (i === indexOfEndOrder) { |
|||
break; |
|||
} |
|||
} |
|||
|
|||
const totalGrossPerformance = grossPerformance.minus( |
|||
grossPerformanceAtStartDate |
|||
); |
|||
|
|||
const totalGrossPerformanceWithCurrencyEffect = |
|||
grossPerformanceWithCurrencyEffect.minus( |
|||
grossPerformanceAtStartDateWithCurrencyEffect |
|||
); |
|||
|
|||
const totalNetPerformance = grossPerformance |
|||
.minus(grossPerformanceAtStartDate) |
|||
.minus(fees.minus(feesAtStartDate)); |
|||
|
|||
const timeWeightedAverageInvestmentBetweenStartAndEndDate = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) |
|||
: new Big(0); |
|||
|
|||
const timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect = |
|||
totalInvestmentDays > 0 |
|||
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( |
|||
totalInvestmentDays |
|||
) |
|||
: new Big(0); |
|||
|
|||
const grossPerformancePercentage = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) |
|||
? totalGrossPerformance.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate |
|||
) |
|||
: new Big(0); |
|||
|
|||
const grossPerformancePercentageWithCurrencyEffect = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt( |
|||
0 |
|||
) |
|||
? totalGrossPerformanceWithCurrencyEffect.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|||
) |
|||
: new Big(0); |
|||
|
|||
const feesPerUnit = totalUnits.gt(0) |
|||
? fees.minus(feesAtStartDate).div(totalUnits) |
|||
: new Big(0); |
|||
|
|||
const feesPerUnitWithCurrencyEffect = totalUnits.gt(0) |
|||
? feesWithCurrencyEffect |
|||
.minus(feesAtStartDateWithCurrencyEffect) |
|||
.div(totalUnits) |
|||
: new Big(0); |
|||
|
|||
const netPerformancePercentage = |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0) |
|||
? totalNetPerformance.div( |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate |
|||
) |
|||
: new Big(0); |
|||
|
|||
const netPerformancePercentageWithCurrencyEffectMap: { |
|||
[key: DateRange]: Big; |
|||
} = {}; |
|||
|
|||
const netPerformanceWithCurrencyEffectMap: { |
|||
[key: DateRange]: Big; |
|||
} = {}; |
|||
|
|||
for (const dateRange of [ |
|||
'1d', |
|||
'1y', |
|||
'5y', |
|||
'max', |
|||
'mtd', |
|||
'wtd', |
|||
'ytd' |
|||
// TODO:
|
|||
// ...eachYearOfInterval({ end, start })
|
|||
// .filter((date) => {
|
|||
// return !isThisYear(date);
|
|||
// })
|
|||
// .map((date) => {
|
|||
// return format(date, 'yyyy');
|
|||
// })
|
|||
] as DateRange[]) { |
|||
const dateInterval = getIntervalFromDateRange(dateRange); |
|||
const endDate = dateInterval.endDate; |
|||
let startDate = dateInterval.startDate; |
|||
|
|||
if (isBefore(startDate, start)) { |
|||
startDate = start; |
|||
} |
|||
|
|||
const rangeEndDateString = format(endDate, DATE_FORMAT); |
|||
const rangeStartDateString = format(startDate, DATE_FORMAT); |
|||
|
|||
const currentValuesAtDateRangeStartWithCurrencyEffect = |
|||
currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0); |
|||
|
|||
const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = |
|||
investmentValuesAccumulatedWithCurrencyEffect[rangeStartDateString] ?? |
|||
new Big(0); |
|||
|
|||
const grossPerformanceAtDateRangeStartWithCurrencyEffect = |
|||
currentValuesAtDateRangeStartWithCurrencyEffect.minus( |
|||
investmentValuesAccumulatedAtStartDateWithCurrencyEffect |
|||
); |
|||
|
|||
let average = new Big(0); |
|||
let dayCount = 0; |
|||
|
|||
for (let i = this.chartDates.length - 1; i >= 0; i -= 1) { |
|||
const date = this.chartDates[i]; |
|||
|
|||
if (date > rangeEndDateString) { |
|||
continue; |
|||
} else if (date < rangeStartDateString) { |
|||
break; |
|||
} |
|||
|
|||
if ( |
|||
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && |
|||
investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) |
|||
) { |
|||
average = average.add( |
|||
investmentValuesAccumulatedWithCurrencyEffect[date].add( |
|||
grossPerformanceAtDateRangeStartWithCurrencyEffect |
|||
) |
|||
); |
|||
|
|||
dayCount++; |
|||
} |
|||
} |
|||
|
|||
if (dayCount > 0) { |
|||
average = average.div(dayCount); |
|||
} |
|||
|
|||
netPerformanceWithCurrencyEffectMap[dateRange] = |
|||
netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus( |
|||
// If the date range is 'max', take 0 as a start value. Otherwise,
|
|||
// the value of the end of the day of the start date is taken which
|
|||
// differs from the buying price.
|
|||
dateRange === 'max' |
|||
? new Big(0) |
|||
: (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ?? |
|||
new Big(0)) |
|||
) ?? new Big(0); |
|||
|
|||
netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) |
|||
? netPerformanceWithCurrencyEffectMap[dateRange].div(average) |
|||
: new Big(0); |
|||
} |
|||
|
|||
if (PortfolioCalculator.ENABLE_LOGGING) { |
|||
console.log( |
|||
` |
|||
${symbol} |
|||
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed( |
|||
2 |
|||
)} -> ${unitPriceAtEndDate.toFixed(2)} |
|||
Total investment: ${totalInvestment.toFixed(2)} |
|||
Total investment with currency effect: ${totalInvestmentWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed( |
|||
2 |
|||
)} |
|||
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Total dividend: ${totalDividend.toFixed(2)} |
|||
Gross performance: ${totalGrossPerformance.toFixed( |
|||
2 |
|||
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}% |
|||
Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} / ${grossPerformancePercentageWithCurrencyEffect |
|||
.mul(100) |
|||
.toFixed(2)}% |
|||
Fees per unit: ${feesPerUnit.toFixed(2)} |
|||
Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed( |
|||
2 |
|||
)} |
|||
Net performance: ${totalNetPerformance.toFixed( |
|||
2 |
|||
)} / ${netPerformancePercentage.mul(100).toFixed(2)}% |
|||
Net performance with currency effect: ${netPerformancePercentageWithCurrencyEffectMap[ |
|||
'max' |
|||
].toFixed(2)}%` |
|||
); |
|||
} |
|||
|
|||
return { |
|||
currentValues, |
|||
currentValuesWithCurrencyEffect, |
|||
feesWithCurrencyEffect, |
|||
grossPerformancePercentage, |
|||
grossPerformancePercentageWithCurrencyEffect, |
|||
initialValue, |
|||
initialValueWithCurrencyEffect, |
|||
investmentValuesAccumulated, |
|||
investmentValuesAccumulatedWithCurrencyEffect, |
|||
investmentValuesWithCurrencyEffect, |
|||
netPerformancePercentage, |
|||
netPerformancePercentageWithCurrencyEffectMap, |
|||
netPerformanceValues, |
|||
netPerformanceValuesWithCurrencyEffect, |
|||
netPerformanceWithCurrencyEffectMap, |
|||
timeWeightedInvestmentValues, |
|||
timeWeightedInvestmentValuesWithCurrencyEffect, |
|||
totalAccountBalanceInBaseCurrency, |
|||
totalDividend, |
|||
totalDividendInBaseCurrency, |
|||
totalInterest, |
|||
totalInterestInBaseCurrency, |
|||
totalInvestment, |
|||
totalInvestmentWithCurrencyEffect, |
|||
totalLiabilities, |
|||
totalLiabilitiesInBaseCurrency, |
|||
totalValuables, |
|||
totalValuablesInBaseCurrency, |
|||
grossPerformance: totalGrossPerformance, |
|||
grossPerformanceWithCurrencyEffect: |
|||
totalGrossPerformanceWithCurrencyEffect, |
|||
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), |
|||
netPerformance: totalNetPerformance, |
|||
timeWeightedInvestment: |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDate, |
|||
timeWeightedInvestmentWithCurrencyEffect: |
|||
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|||
}; |
|||
throw new Error('Method not implemented.'); |
|||
} |
|||
} |
|||
|
File diff suppressed because it is too large
@ -1,18 +1,20 @@ |
|||
-----BEGIN CERTIFICATE----- |
|||
MIIC5TCCAc2gAwIBAgIJAJAMHOFnJ6oyMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV |
|||
BAMMCWxvY2FsaG9zdDAeFw0yNDAyMjcxNTQ2MzFaFw0yNDAzMjgxNTQ2MzFaMBQx |
|||
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC |
|||
ggEBAJ0hRjViikEKVIyukXR4CfuYVvFEFzB6AwAQ9Jrz2MseqpLacLTXFFAS54mp |
|||
iDuqPBzs9ta40mlSrqSBuAWKikpW5kTNnmqUnDZ6iSJezbYWx9YyULGqqb1q3C4/ |
|||
5pH9m6NHJ+2uaUNKlDxYNKbntqs3drQEdxH9yv672Z53nvogTcf9jz6zjutEQGSV |
|||
HgVkCTTQmzf3+3st+VJ5D8JeYFR+tpZ6yleqgXFaTMtPZRfKLvTkQ+KeyCJLnsUJ |
|||
BQvdCKI0PGsG6d6ygXFmSePolD9KW3VTKKDPCsndID89vAnRWDj9UhzvycxmKpcF |
|||
GrUPH5+Pis1PM1R7OnAvnFygnyECAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo |
|||
b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B |
|||
AQsFAAOCAQEAOdzrY3RTAPnBVAd3/GKIEkielYyOgKPnvd+RcOB+je8cXZY+vaxX |
|||
uEFP0526G00+kRZ2Tx9t0SmjoSpwg3lHUPMko0dIxgRlqISDAohdrEptHpcVujsD |
|||
ak88LLnAurr60JNjWX2wbEoJ18KLtqGSnATdmCgKwDPIN5a7+ssp44BGyzH6VYCg |
|||
wV3VjJl0pp4C5M0Jfu0p1FrQjzIVhgqR7JFYmvqIogWrGwYAQK/3XRXq8t48J5X3 |
|||
IsfWiLAA2ZdCoWAnZ6PAGBOoGivtkJm967pHjd/28qYY6mQo4sN2ksEOjx6/YslF |
|||
2mOJdLV/DzqoggUsTpPEG0dRhzQLTGHKDQ== |
|||
MIIDSDCCAjACCQCQ2ForVhz+uDANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJD |
|||
SDEOMAwGA1UECAwFU3RhdGUxDTALBgNVBAcMBENpdHkxFTATBgNVBAoMDE9yZ2Fu |
|||
aXphdGlvbjENMAsGA1UECwwEVW5pdDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1 |
|||
MDMwOTE2MzQxM1oXDTI2MDMwOTE2MzQxM1owZjELMAkGA1UEBhMCQ0gxDjAMBgNV |
|||
BAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRUwEwYDVQQKDAxPcmdhbml6YXRpb24x |
|||
DTALBgNVBAsMBFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN |
|||
AQEBBQADggEPADCCAQoCggEBAMkJRKPgV8NDcoIakPc7sZVXQ9VK2PGb8+lF/1Lv |
|||
NcIZpD40+p4DzuEw0bjRn17IDClyLMaLbZNtIyTPSkFaffL+rJ0JvnKdG50s+HId |
|||
YNuCwKkgHg4hTXFzOPpT3HMG3UxyEwFOm25GMFiikfT96ukMAAkanMqYKZQOClRU |
|||
Cw4LP3g0Oks58obbRy4Wltp88K8LJrR+j81+AjElTIGXHhChXzV/NjJ14TMNy5hZ |
|||
lwV4xUSwvNqOvWGMIR7J77fINF130ghTSnvzCS52dCeom2I4Lvncz3m37lDttCOs |
|||
Wm/i651ro7pwFEs/lJmrnFHPtph2ayPcHBmrQCgLc5xMUMcCAwEAATANBgkqhkiG |
|||
9w0BAQsFAAOCAQEAhRA1/+Gl2VH34yN/FvrE5cY0W4ghSCuTdK9pGeo8AcN+TScU |
|||
7O+hVsEwZDrYKuDvG8Ab//A+uv5gbfGbYPJVIdJ3Q8HKijNZmbwAgANJU/c0WwOx |
|||
XBQ9mCzWRcJxQeUUgh4DT4lZCOfR5pIvAJpKScTcF/yp5gOgrgJH1GHFEYYPoXWO |
|||
ezPPMwCNbfamUPlZZnHu74fUrFrDPI9c/YSu8Ex/LegZXJAEzA+8I0g64rjGtzJp |
|||
fkRDyQcBuT5SVa+USBlALQmdIuT/fN6R729DcGzvV8JqdoG9sLra4hrRCn3+A3c9 |
|||
izZguW1BQNQ2N7II6QCDnWkdUFSQCiQunX/xsg== |
|||
-----END CERTIFICATE----- |
|||
|
@ -1,28 +1,28 @@ |
|||
-----BEGIN PRIVATE KEY----- |
|||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdIUY1YopBClSM |
|||
rpF0eAn7mFbxRBcwegMAEPSa89jLHqqS2nC01xRQEueJqYg7qjwc7PbWuNJpUq6k |
|||
gbgFiopKVuZEzZ5qlJw2eokiXs22FsfWMlCxqqm9atwuP+aR/ZujRyftrmlDSpQ8 |
|||
WDSm57arN3a0BHcR/cr+u9med576IE3H/Y8+s47rREBklR4FZAk00Js39/t7LflS |
|||
eQ/CXmBUfraWespXqoFxWkzLT2UXyi705EPinsgiS57FCQUL3QiiNDxrBunesoFx |
|||
Zknj6JQ/Slt1UyigzwrJ3SA/PbwJ0Vg4/VIc78nMZiqXBRq1Dx+fj4rNTzNUezpw |
|||
L5xcoJ8hAgMBAAECggEAPU5YOEgELTA8oM8TjV+wdWuQsH2ilpVkSkhTR4nQkh+a |
|||
6cU0qDoqgLt/fySYNL9MyPRjso9V+SX7YdAC3paZMjwJh9q57lehQ1g33SMkG+Fz |
|||
gs0K0ucFZxQkaB8idN+ANAp1N7UO+ORGRe0cTeqmSNNRCxea5XgiFZVxaPS/IFOR |
|||
vVdXS1DulgwHh4H0ljKmkj7jc9zPBSc9ccW5Ml2q4a26Atu4IC/Mlp/DF4GNELbD |
|||
ebi9ZOZG33ip2bdhj3WX7NW9GJaaViKtVUpcpR6u+6BfvTXQ6xrqdoxXk9fnPzzf |
|||
sSoLPTt8yO4RavP1zQU22PhgIcWudhCiy/2Nv+uLqQKBgQDMPh1/xwdFl8yES8dy |
|||
f0xJyCDWPiB+xzGwcOAC90FrNZaqG0WWs3VHE4cilaWjCflvdR6aAEDEY68sZJhl |
|||
h+ChT/g61QLMOI+VIDQ1kJXKYgfS/B+BE1PZ0EkuymKFOvbNO8agHodB8CqnZaQh |
|||
bLuZaDnZ0JLK4im3KPt70+aKYwKBgQDE8s6xl0SLu+yJLo3VklCRD67Z8/jlvx2t |
|||
h3DF6NG8pB3QmvKdJWFKuLAWLGflJwbUW9Pt3hXkc0649KQrtiIYC0ZMh9KMaVCk |
|||
WmjS/n2eIUQZ7ZUlwFesi4p4iGynVBavIY8TJ6Y+K3TvsJgXP3IZ96r689PQJo8E |
|||
KbSeyYzFqwKBgGQTS4EAlJ+U8bEhMGj51veQCAbyChoUoFRD+n95h6RwbZKMKlzd |
|||
MenRt7VKfg6VJJNoX8Y1uYaBEaQ+5i1Zlsdz1718AhLu4+u+C9bzMXIo9ox63TTx |
|||
s3RWioVSxVNiwOtvDrQGQWAdvcioFPQLwyA34aDIgiTHDImimxbhjWThAoGAWOqW |
|||
Tq9QjxWk0Lpn5ohMP3GpK1VuhasnJvUDARb/uf8ORuPtrOz3Y9jGBvy9W0OnXbCn |
|||
mbiugZldbTtl8yYjdl+AuYSIlkPl2I3IzZl/9Shnqp0MvSJ9crT9KzXMeC8Knr6z |
|||
7Z30/BR6ksxTngtS5E5grzPt6Qe/gc2ich3kpEkCgYBfBHUhVIKVrDW/8a7U2679 |
|||
Wj4i9us/M3iPMxcTv7/ZAg08TEvNBQYtvVQLu0NfDKXx8iGKJJ6evIYgNXVm2sPq |
|||
VzSgwGCALi1X0443amZU+mnX+/9RnBqyM+PJU8570mM83jqKlY/BEsi0aqxTioFG |
|||
an3xbjjN+Rq9iKLzmPxIMg== |
|||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJCUSj4FfDQ3KC |
|||
GpD3O7GVV0PVStjxm/PpRf9S7zXCGaQ+NPqeA87hMNG40Z9eyAwpcizGi22TbSMk |
|||
z0pBWn3y/qydCb5ynRudLPhyHWDbgsCpIB4OIU1xczj6U9xzBt1MchMBTptuRjBY |
|||
opH0/erpDAAJGpzKmCmUDgpUVAsOCz94NDpLOfKG20cuFpbafPCvCya0fo/NfgIx |
|||
JUyBlx4QoV81fzYydeEzDcuYWZcFeMVEsLzajr1hjCEeye+3yDRdd9IIU0p78wku |
|||
dnQnqJtiOC753M95t+5Q7bQjrFpv4uuda6O6cBRLP5SZq5xRz7aYdmsj3BwZq0Ao |
|||
C3OcTFDHAgMBAAECggEBALJQqDN7QB0QbDb+fWrt5bwDJUXBF+BmZdiZn7jeOJ6r |
|||
w8TxlQIneo6/kKYQOP4HDtKMVS7eaRkFCtERlFmXfHPWdSDtjaF3vRCS3OPLLyhF |
|||
N8JLnJ0H6PsiKn3PeJAGnK+71yOnp7IOS7+yoyfdOUnwvO9WTZBdmzOZqIvX595R |
|||
g7R5yjSYjzFMmaCpyab6kiD7b4bHzDIrB0XuouT2W/fS/i1srwc3eDk78ZyioyiB |
|||
g6GDuOwqDfPmkUqKo2oXSoSR8yCwSSdlClc7aOowoNxbsksTDjXf1k02n0lG5MHU |
|||
ldCX02WdA0JFW8Os0Ig+YBq7wSkB5oNVt7gEek/MB0ECgYEA+ATkyfX9/5X2kUMY |
|||
MatUqKOvLUtyIfHeYUy/Dm+9JlZlrxD0dRAKWhnhRR16v5cwN2RVtEvMDsV1AGHN |
|||
e/fh315aAq+I6/eY6syXfkeHHs5UIRPrOlIcp1Ogfg99xpOT0/TZy9bB7lKvtYes |
|||
GmKO8n7md1TptdxilSNORI60KvECgYEAz4FX6vH76HgV/seY+vePrj5nFCZnDru7 |
|||
16w5LYoHaX0hABJ0qZCqZozdPf9mqM5Ldc8PUbvVsFqXyaHwBAKUsH44a3aLxcXU |
|||
JMsQanU5I87SWP/S8Xu2Yxc10L66Oc5VdAeraZvb+wJqTkYKhDYOJVMjyuI/vkAw |
|||
fqMPI6wShzcCgYEAndYnb6uf4Eakap9jR0C8mLHKaq3nzVhqaEt6DwrnOf2jqnzE |
|||
xbbWj66GoQB4vHLP2YB91kaibwgURJD5PxpqYUdfSvRA08J3S322L0P/5ofyHDbb |
|||
7PqSh539thvPtE74tdvNux5Jvoxai9Dyorv0Mri1nF2qefTtu/GC/rg+SlECgYB2 |
|||
FaYhhomTVls1/QIat6zlPI/OULhPExineFOljaoAJvwTnW0UXcYKy9jPgjs6jwM0 |
|||
TJvsKFdHn5ZHYUdEEO/qrDmRNgn+h0Ddm02BN6pHrVfY2+SAFaXKKBgw7YjugnPw |
|||
rrimRdLeuhYi6wrrCBPuu6xftXcO3lp6hnKEG1UD6wKBgEh/C7HQ6cjb7Rr15eRq |
|||
2VOgeuz7o2v/OC+jO6yFGRrs2VKoBuJpw/6jx806Cbi2jLEwim21iNYW/2McOWP3 |
|||
YUvni7qHXfll8d4sSAuCTA4K/N0MJ/3XbGBPDm/83J2o7uz2GFkQRruruaERvDMF |
|||
x26H2i3DOUFzdgbkoNB0ifHd |
|||
-----END PRIVATE KEY----- |
|||
|
@ -1,3 +1,17 @@ |
|||
:host { |
|||
display: block; |
|||
|
|||
.icon-container { |
|||
background-color: rgba(var(--palette-foreground-base), 0.02); |
|||
border-radius: 0.25rem; |
|||
height: 2rem; |
|||
|
|||
&.okay { |
|||
color: var(--success); |
|||
} |
|||
|
|||
&.warn { |
|||
color: var(--danger); |
|||
} |
|||
} |
|||
} |
|||
|
@ -1,19 +1,58 @@ |
|||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; |
|||
import { DataService } from '@ghostfolio/client/services/data.service'; |
|||
|
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { MatStepper } from '@angular/material/stepper'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'gf-show-access-token-dialog', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
selector: 'gf-show-access-token-dialog', |
|||
standalone: false, |
|||
styleUrls: ['./show-access-token-dialog.scss'], |
|||
templateUrl: 'show-access-token-dialog.html', |
|||
standalone: false |
|||
templateUrl: 'show-access-token-dialog.html' |
|||
}) |
|||
export class ShowAccessTokenDialog { |
|||
public isAgreeButtonDisabled = true; |
|||
@ViewChild(MatStepper) stepper!: MatStepper; |
|||
|
|||
public accessToken: string; |
|||
public authToken: string; |
|||
public isCreateAccountButtonDisabled = true; |
|||
public isDisclaimerChecked = false; |
|||
public role: string; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor( |
|||
private changeDetectorRef: ChangeDetectorRef, |
|||
private dataService: DataService |
|||
) {} |
|||
|
|||
public constructor(@Inject(MAT_DIALOG_DATA) public data: any) {} |
|||
public createAccount() { |
|||
this.dataService |
|||
.postUser() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(({ accessToken, authToken, role }) => { |
|||
this.accessToken = accessToken; |
|||
this.authToken = authToken; |
|||
this.role = role; |
|||
|
|||
this.stepper.next(); |
|||
|
|||
this.changeDetectorRef.markForCheck(); |
|||
}); |
|||
} |
|||
|
|||
public enableCreateAccountButton() { |
|||
this.isCreateAccountButtonDisabled = false; |
|||
} |
|||
|
|||
public enableAgreeButton() { |
|||
this.isAgreeButtonDisabled = false; |
|||
public onChangeDislaimerChecked() { |
|||
this.isDisclaimerChecked = !this.isDisclaimerChecked; |
|||
} |
|||
} |
|||
|
@ -1,48 +1,93 @@ |
|||
<h1 mat-dialog-title> |
|||
<span i18n>Create Account</span> |
|||
@if (data.role === 'ADMIN') { |
|||
<span class="badge badge-light ml-2">{{ data.role }}</span> |
|||
@if (role === 'ADMIN') { |
|||
<span class="badge badge-light ml-2">{{ role }}</span> |
|||
} |
|||
</h1> |
|||
<div class="py-3" mat-dialog-content> |
|||
<div> |
|||
<mat-form-field appearance="outline" class="w-100"> |
|||
<mat-label i18n>Security Token</mat-label> |
|||
<textarea |
|||
cdkTextareaAutosize |
|||
matInput |
|||
readonly |
|||
type="text" |
|||
[(value)]="data.accessToken" |
|||
></textarea> |
|||
<div class="float-right mt-3"> |
|||
<button |
|||
color="secondary" |
|||
mat-flat-button |
|||
[cdkCopyToClipboard]="data.accessToken" |
|||
(click)="enableAgreeButton()" |
|||
<div class="px-0" mat-dialog-content> |
|||
<mat-stepper #stepper animationDuration="0ms" linear> |
|||
<mat-step editable="false" [completed]="isDisclaimerChecked"> |
|||
<ng-template i18n matStepLabel>Terms and Conditions</ng-template> |
|||
<div class="pt-2"> |
|||
<ng-container i18n |
|||
>Please keep your security token safe. If you lose it, you will not be |
|||
able to recover your account.</ng-container |
|||
> |
|||
<ion-icon class="mr-1" name="copy-outline" /><span i18n |
|||
>Copy to clipboard</span |
|||
</div> |
|||
<mat-checkbox |
|||
class="pt-2" |
|||
color="primary" |
|||
(change)="onChangeDislaimerChecked()" |
|||
> |
|||
<ng-container i18n |
|||
>I understand that if I lose my security token, I cannot recover my |
|||
account.</ng-container |
|||
> |
|||
</mat-checkbox> |
|||
<div class="mt-3" mat-dialog-actions> |
|||
<div class="flex-grow-1"> |
|||
<button i18n mat-button [mat-dialog-close]="undefined">Cancel</button> |
|||
</div> |
|||
<div> |
|||
<button |
|||
color="primary" |
|||
mat-flat-button |
|||
[disabled]="!isDisclaimerChecked" |
|||
(click)="createAccount()" |
|||
> |
|||
</button> |
|||
<ng-container i18n>Continue</ng-container> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</mat-form-field> |
|||
</div> |
|||
<p i18n> |
|||
I agree to have stored my <i>Security Token</i> from above in a secure |
|||
place. If I lose it, I cannot get my account back. |
|||
</p> |
|||
</div> |
|||
<div class="justify-content-end" mat-dialog-actions> |
|||
<button i18n mat-flat-button [mat-dialog-close]="undefined">Cancel</button> |
|||
<button |
|||
color="primary" |
|||
mat-flat-button |
|||
[disabled]="isAgreeButtonDisabled" |
|||
[mat-dialog-close]="data" |
|||
> |
|||
<span i18n>Agree and continue</span> |
|||
<ion-icon class="ml-1" name="arrow-forward-outline" /> |
|||
</button> |
|||
</mat-step> |
|||
<mat-step editable="false"> |
|||
<ng-template i18n matStepLabel>Security Token</ng-template> |
|||
<div class="pt-2"> |
|||
<ng-container i18n |
|||
>Here is your security token. It is only visible once, please store |
|||
and keep it in a safe place.</ng-container |
|||
> |
|||
</div> |
|||
<mat-form-field appearance="outline" class="pt-3 w-100 without-hint"> |
|||
<mat-label i18n>Security Token</mat-label> |
|||
<textarea |
|||
cdkTextareaAutosize |
|||
matInput |
|||
readonly |
|||
type="text" |
|||
[(value)]="accessToken" |
|||
></textarea> |
|||
<div class="float-right mt-1"> |
|||
<button |
|||
color="secondary" |
|||
mat-flat-button |
|||
[cdkCopyToClipboard]="accessToken" |
|||
(click)="enableCreateAccountButton()" |
|||
> |
|||
<ion-icon class="mr-1" name="copy-outline" /> |
|||
<span i18n>Copy to clipboard</span> |
|||
</button> |
|||
</div> |
|||
</mat-form-field> |
|||
<div align="end" class="mt-1" mat-dialog-actions> |
|||
<div> |
|||
<button |
|||
color="primary" |
|||
mat-flat-button |
|||
matStepperNext |
|||
[disabled]="isCreateAccountButtonDisabled" |
|||
[mat-dialog-close]="authToken" |
|||
> |
|||
<span i18n>Create Account</span> |
|||
<ion-icon class="ml-1" name="arrow-forward-outline" /> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</mat-step> |
|||
|
|||
<ng-template matStepperIcon="done"> |
|||
<ion-icon name="checkmark-outline"></ion-icon> |
|||
</ng-template> |
|||
</mat-stepper> |
|||
<div></div> |
|||
</div> |
|||
|
@ -1,2 +1,6 @@ |
|||
:host { |
|||
.mat-mdc-dialog-actions { |
|||
padding-left: 0 !important; |
|||
padding-right: 0 !important; |
|||
} |
|||
} |
|||
|
@ -0,0 +1,4 @@ |
|||
export interface AccountBalance { |
|||
date: string; |
|||
value: number; |
|||
} |
@ -0,0 +1,4 @@ |
|||
import { SymbolProfile } from '@prisma/client'; |
|||
|
|||
export interface DataProviderGhostfolioAssetProfileResponse |
|||
extends Partial<SymbolProfile> {} |
@ -0,0 +1 @@ |
|||
export type AiPromptMode = 'analysis' | 'portfolio'; |
File diff suppressed because it is too large
Loading…
Reference in new issue