@ -19,13 +19,14 @@ import {
import {
AssetProfileIdentifier ,
DataProviderInfo ,
Filter ,
HistoricalDataItem ,
InvestmentItem ,
ResponseError ,
SymbolMetrics
} from '@ghostfolio/common/interfaces' ;
import { PortfolioSnapshot , TimelinePosition } from '@ghostfolio/common/models' ;
import { DateRange, GroupBy } from '@ghostfolio/common/types' ;
import { GroupBy } from '@ghostfolio/common/types' ;
import { Logger } from '@nestjs/common' ;
import { Big } from 'big.js' ;
@ -37,12 +38,10 @@ import {
format ,
isAfter ,
isBefore ,
isSameDay ,
max ,
min ,
subDays
} from 'date-fns' ;
import { first , last, uniq , uniqBy } from 'lodash' ;
import { first , isNumber, last, sortBy , sum , uniq , uniqBy } from 'lodash' ;
export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false ;
@ -54,15 +53,14 @@ export abstract class PortfolioCalculator {
private currency : string ;
private currentRateService : CurrentRateService ;
private dataProviderInfos : DataProviderInfo [ ] ;
private dateRange : DateRange ;
private endDate : Date ;
private exchangeRateDataService : ExchangeRateDataService ;
private filters : Filter [ ] ;
private redisCacheService : RedisCacheService ;
private snapshot : PortfolioSnapshot ;
private snapshotPromise : Promise < void > ;
private startDate : Date ;
private transactionPoints : TransactionPoint [ ] ;
private useCache : boolean ;
private userId : string ;
public constructor ( {
@ -71,10 +69,9 @@ export abstract class PortfolioCalculator {
configurationService ,
currency ,
currentRateService ,
dateRange ,
exchangeRateDataService ,
filters ,
redisCacheService ,
useCache ,
userId
} : {
accountBalanceItems : HistoricalDataItem [ ] ;
@ -82,18 +79,19 @@ export abstract class PortfolioCalculator {
configurationService : ConfigurationService ;
currency : string ;
currentRateService : CurrentRateService ;
dateRange : DateRange ;
exchangeRateDataService : ExchangeRateDataService ;
filters : Filter [ ] ;
redisCacheService : RedisCacheService ;
useCache : boolean ;
userId : string ;
} ) {
this . accountBalanceItems = accountBalanceItems ;
this . configurationService = configurationService ;
this . currency = currency ;
this . currentRateService = currentRateService ;
this . dateRange = dateRange ;
this . exchangeRateDataService = exchangeRateDataService ;
this . filters = filters ;
let dateOfFirstActivity = new Date ( ) ;
this . activities = activities
. map (
@ -106,10 +104,14 @@ export abstract class PortfolioCalculator {
type ,
unitPrice
} ) = > {
if ( isAfter ( date , new Date ( Date . now ( ) ) ) ) {
if ( isBefore ( date , dateOfFirstActivity ) ) {
dateOfFirstActivity = date ;
}
if ( isAfter ( date , new Date ( ) ) ) {
// Adapt date to today if activity is in future (e.g. liability)
// to include it in the interval
date = endOfDay ( new Date ( Date . now ( ) ) ) ;
date = endOfDay ( new Date ( ) ) ;
}
return {
@ -128,10 +130,12 @@ export abstract class PortfolioCalculator {
} ) ;
this . redisCacheService = redisCacheService ;
this . useCache = useCache ;
this . userId = userId ;
const { endDate , startDate } = getIntervalFromDateRange ( dateRange ) ;
const { endDate , startDate } = getIntervalFromDateRange (
'max' ,
subDays ( dateOfFirstActivity , 1 )
) ;
this . endDate = endDate ;
this . startDate = startDate ;
@ -145,38 +149,18 @@ export abstract class PortfolioCalculator {
positions : TimelinePosition [ ]
) : PortfolioSnapshot ;
public async computeSnapshot (
start : Date ,
end? : Date
) : Promise < PortfolioSnapshot > {
private async computeSnapshot ( ) : 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 ) ;
return isBefore ( parseDate ( date ) , this . 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 ) ,
historicalData : [ ] ,
positions : [ ] ,
totalFeesWithCurrencyEffect : new Big ( 0 ) ,
totalInterestWithCurrencyEffect : new Big ( 0 ) ,
@ -189,15 +173,12 @@ export abstract class PortfolioCalculator {
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 ) {
@ -211,47 +192,19 @@ export abstract class PortfolioCalculator {
for ( let i = 0 ; i < transactionPoints . length ; i ++ ) {
if (
! isBefore ( parseDate ( transactionPoints [ i ] . date ) , start ) &&
! isBefore ( parseDate ( transactionPoints [ i ] . date ) , this . start Date ) &&
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( ) ,
endDate : endOfDay ( this . endDate ) ,
startDate : this.startDate ,
targetCurrency : this.currency
} ) ;
@ -262,7 +215,8 @@ export abstract class PortfolioCalculator {
} = await this . currentRateService . getValues ( {
dataGatheringItems ,
dateQuery : {
in : dates
gte : this.startDate ,
lt : this.endDate
}
} ) ;
@ -286,7 +240,19 @@ export abstract class PortfolioCalculator {
}
}
const endDateString = format ( endDate , DATE_FORMAT ) ;
const endDateString = format ( this . endDate , DATE_FORMAT ) ;
const daysInMarket = differenceInDays ( this . endDate , this . startDate ) ;
let chartDateMap = this . getChartDateMap ( {
endDate : this.endDate ,
startDate : this.startDate ,
step : Math.round ( daysInMarket / Math . min ( daysInMarket , MAX_CHART_ITEMS ) )
} ) ;
const chartDates = sortBy ( Object . keys ( chartDateMap ) , ( chartDate ) = > {
return chartDate ;
} ) ;
if ( firstIndex > 0 ) {
firstIndex -- ;
@ -297,6 +263,35 @@ export abstract class PortfolioCalculator {
const errors : ResponseError [ 'errors' ] = [ ] ;
const accumulatedValuesByDate : {
[ date : string ] : {
investmentValueWithCurrencyEffect : Big ;
totalAccountBalanceWithCurrencyEffect : Big ;
totalCurrentValue : Big ;
totalCurrentValueWithCurrencyEffect : Big ;
totalInvestmentValue : Big ;
totalInvestmentValueWithCurrencyEffect : Big ;
totalNetPerformanceValue : Big ;
totalNetPerformanceValueWithCurrencyEffect : Big ;
totalTimeWeightedInvestmentValue : Big ;
totalTimeWeightedInvestmentValueWithCurrencyEffect : Big ;
} ;
} = { } ;
const valuesBySymbol : {
[ symbol : string ] : {
currentValues : { [ date : string ] : Big } ;
currentValuesWithCurrencyEffect : { [ date : string ] : Big } ;
investmentValuesAccumulated : { [ date : string ] : Big } ;
investmentValuesAccumulatedWithCurrencyEffect : { [ date : string ] : Big } ;
investmentValuesWithCurrencyEffect : { [ date : string ] : Big } ;
netPerformanceValues : { [ date : string ] : Big } ;
netPerformanceValuesWithCurrencyEffect : { [ date : string ] : Big } ;
timeWeightedInvestmentValues : { [ date : string ] : Big } ;
timeWeightedInvestmentValuesWithCurrencyEffect : { [ date : string ] : Big } ;
} ;
} = { } ;
for ( const item of lastTransactionPoint . items ) {
const feeInBaseCurrency = item . fee . mul (
exchangeRatesByCurrency [ ` ${ item . currency } ${ this . currency } ` ] ? . [
@ -313,16 +308,25 @@ export abstract class PortfolioCalculator {
) ;
const {
currentValues ,
currentValuesWithCurrencyEffect ,
grossPerformance ,
grossPerformancePercentage ,
grossPerformancePercentageWithCurrencyEffect ,
grossPerformanceWithCurrencyEffect ,
hasErrors ,
investmentValuesAccumulated ,
investmentValuesAccumulatedWithCurrencyEffect ,
investmentValuesWithCurrencyEffect ,
netPerformance ,
netPerformancePercentage ,
netPerformancePercentageWithCurrencyEffect ,
netPerformanceWithCurrencyEffect ,
netPerformancePercentageWithCurrencyEffectMap ,
netPerformanceValues ,
netPerformanceValuesWithCurrencyEffect ,
netPerformanceWithCurrencyEffectMap ,
timeWeightedInvestment ,
timeWeightedInvestmentValues ,
timeWeightedInvestmentValuesWithCurrencyEffect ,
timeWeightedInvestmentWithCurrencyEffect ,
totalDividend ,
totalDividendInBaseCurrency ,
@ -332,17 +336,30 @@ export abstract class PortfolioCalculator {
totalLiabilitiesInBaseCurrency ,
totalValuablesInBaseCurrency
} = this . getSymbolMetrics ( {
chartDateMap ,
marketSymbolMap ,
start ,
dataSource : item.dataSource ,
end : endDate,
end : this. endDate,
exchangeRates :
exchangeRatesByCurrency [ ` ${ item . currency } ${ this . currency } ` ] ,
start : this.startDate ,
symbol : item . symbol
} ) ;
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors ;
valuesBySymbol [ item . symbol ] = {
currentValues ,
currentValuesWithCurrencyEffect ,
investmentValuesAccumulated ,
investmentValuesAccumulatedWithCurrencyEffect ,
investmentValuesWithCurrencyEffect ,
netPerformanceValues ,
netPerformanceValuesWithCurrencyEffect ,
timeWeightedInvestmentValues ,
timeWeightedInvestmentValuesWithCurrencyEffect
} ;
positions . push ( {
feeInBaseCurrency ,
timeWeightedInvestment ,
@ -374,11 +391,11 @@ export abstract class PortfolioCalculator {
netPerformancePercentage : ! hasErrors
? ( netPerformancePercentage ? ? null )
: null ,
netPerformancePercentageWithCurrencyEffect : ! hasErrors
? ( netPerformancePercentageWithCurrencyEffect ? ? null )
netPerformancePercentageWithCurrencyEffect Map : ! hasErrors
? ( netPerformancePercentageWithCurrencyEffect Map ? ? null )
: null ,
netPerformanceWithCurrencyEffect : ! hasErrors
? ( netPerformanceWithCurrencyEffect ? ? null )
netPerformanceWithCurrencyEffect Map : ! hasErrors
? ( netPerformanceWithCurrencyEffect Map ? ? null )
: null ,
quantity : item.quantity ,
symbol : item . symbol ,
@ -411,205 +428,9 @@ export abstract class PortfolioCalculator {
}
}
const overall = this . calculateOverallPerformance ( positions ) ;
return {
. . . overall ,
errors ,
positions ,
totalInterestWithCurrencyEffect ,
totalLiabilitiesWithCurrencyEffect ,
totalValuablesWithCurrencyEffect ,
hasErrors : hasAnySymbolMetricsErrors || overall . hasErrors
} ;
}
public async getChart ( {
dateRange = 'max' ,
withDataDecimation = true
} : {
dateRange? : DateRange ;
withDataDecimation? : boolean ;
} ) : Promise < HistoricalDataItem [ ] > {
const { endDate , startDate } = getIntervalFromDateRange (
dateRange ,
this . getStartDate ( )
) ;
const daysInMarket = differenceInDays ( endDate , startDate ) + 1 ;
const step = withDataDecimation
? Math . round ( daysInMarket / Math . min ( daysInMarket , MAX_CHART_ITEMS ) )
: 1 ;
return this . getChartData ( {
step ,
end : endDate ,
start : startDate
} ) ;
}
public async getChartData ( {
end = new Date ( Date . now ( ) ) ,
start ,
step = 1
} : {
end? : Date ;
start : Date ;
step? : number ;
} ) : Promise < HistoricalDataItem [ ] > {
const symbols : { [ symbol : string ] : boolean } = { } ;
const transactionPointsBeforeEndDate =
this . transactionPoints ? . filter ( ( transactionPoint ) = > {
return isBefore ( parseDate ( transactionPoint . date ) , end ) ;
} ) ? ? [ ] ;
const currencies : { [ symbol : string ] : string } = { } ;
const dataGatheringItems : IDataGatheringItem [ ] = [ ] ;
const firstIndex = transactionPointsBeforeEndDate . length ;
let dates = eachDayOfInterval ( { start , end } , { step } ) . map ( ( date ) = > {
return resetHours ( date ) ;
} ) ;
const includesEndDate = isSameDay ( last ( dates ) , end ) ;
if ( ! includesEndDate ) {
dates . push ( resetHours ( end ) ) ;
}
if ( transactionPointsBeforeEndDate . length > 0 ) {
for ( const {
currency ,
dataSource ,
symbol
} of transactionPointsBeforeEndDate [ firstIndex - 1 ] . items ) {
dataGatheringItems . push ( {
dataSource ,
symbol
} ) ;
currencies [ symbol ] = currency ;
symbols [ symbol ] = true ;
}
}
const { dataProviderInfos , values : marketSymbols } =
await this . currentRateService . getValues ( {
dataGatheringItems ,
dateQuery : {
in : dates
}
} ) ;
this . dataProviderInfos = dataProviderInfos ;
const marketSymbolMap : {
[ date : string ] : { [ symbol : string ] : Big } ;
} = { } ;
let exchangeRatesByCurrency =
await this . exchangeRateDataService . getExchangeRatesByCurrency ( {
currencies : uniq ( Object . values ( currencies ) ) ,
endDate : endOfDay ( end ) ,
startDate : this.getStartDate ( ) ,
targetCurrency : this.currency
} ) ;
for ( const marketSymbol of marketSymbols ) {
const dateString = format ( marketSymbol . date , DATE_FORMAT ) ;
if ( ! marketSymbolMap [ dateString ] ) {
marketSymbolMap [ dateString ] = { } ;
}
if ( marketSymbol . marketPrice ) {
marketSymbolMap [ dateString ] [ marketSymbol . symbol ] = new Big (
marketSymbol . marketPrice
) ;
}
}
const accumulatedValuesByDate : {
[ date : string ] : {
investmentValueWithCurrencyEffect : Big ;
totalCurrentValue : Big ;
totalCurrentValueWithCurrencyEffect : Big ;
totalAccountBalanceWithCurrencyEffect : Big ;
totalInvestmentValue : Big ;
totalInvestmentValueWithCurrencyEffect : Big ;
totalNetPerformanceValue : Big ;
totalNetPerformanceValueWithCurrencyEffect : Big ;
totalTimeWeightedInvestmentValue : Big ;
totalTimeWeightedInvestmentValueWithCurrencyEffect : Big ;
} ;
} = { } ;
const valuesBySymbol : {
[ symbol : string ] : {
currentValues : { [ date : string ] : Big } ;
currentValuesWithCurrencyEffect : { [ date : string ] : Big } ;
investmentValuesAccumulated : { [ date : string ] : Big } ;
investmentValuesAccumulatedWithCurrencyEffect : { [ date : string ] : Big } ;
investmentValuesWithCurrencyEffect : { [ date : string ] : Big } ;
netPerformanceValues : { [ date : string ] : Big } ;
netPerformanceValuesWithCurrencyEffect : { [ date : string ] : Big } ;
timeWeightedInvestmentValues : { [ date : string ] : Big } ;
timeWeightedInvestmentValuesWithCurrencyEffect : { [ date : string ] : Big } ;
} ;
} = { } ;
for ( const symbol of Object . keys ( symbols ) ) {
const {
currentValues ,
currentValuesWithCurrencyEffect ,
investmentValuesAccumulated ,
investmentValuesAccumulatedWithCurrencyEffect ,
investmentValuesWithCurrencyEffect ,
netPerformanceValues ,
netPerformanceValuesWithCurrencyEffect ,
timeWeightedInvestmentValues ,
timeWeightedInvestmentValuesWithCurrencyEffect
} = this . getSymbolMetrics ( {
end ,
marketSymbolMap ,
start ,
step ,
symbol ,
dataSource : null ,
exchangeRates :
exchangeRatesByCurrency [ ` ${ currencies [ symbol ] } ${ this . currency } ` ] ,
isChartMode : true
} ) ;
valuesBySymbol [ symbol ] = {
currentValues ,
currentValuesWithCurrencyEffect ,
investmentValuesAccumulated ,
investmentValuesAccumulatedWithCurrencyEffect ,
investmentValuesWithCurrencyEffect ,
netPerformanceValues ,
netPerformanceValuesWithCurrencyEffect ,
timeWeightedInvestmentValues ,
timeWeightedInvestmentValuesWithCurrencyEffect
} ;
}
let lastDate = format ( this . startDate , DATE_FORMAT ) ;
for ( const currentDate of dates ) {
const dateString = format ( currentDate , DATE_FORMAT ) ;
accumulatedValuesByDate [ dateString ] = {
investmentValueWithCurrencyEffect : new Big ( 0 ) ,
totalAccountBalanceWithCurrencyEffect : new Big ( 0 ) ,
totalCurrentValue : new Big ( 0 ) ,
totalCurrentValueWithCurrencyEffect : new Big ( 0 ) ,
totalInvestmentValue : new Big ( 0 ) ,
totalInvestmentValueWithCurrencyEffect : new Big ( 0 ) ,
totalNetPerformanceValue : new Big ( 0 ) ,
totalNetPerformanceValueWithCurrencyEffect : new Big ( 0 ) ,
totalTimeWeightedInvestmentValue : new Big ( 0 ) ,
totalTimeWeightedInvestmentValueWithCurrencyEffect : new Big ( 0 )
} ;
let lastDate = chartDates [ 0 ] ;
for ( const dateString of chartDates ) {
for ( const symbol of Object . keys ( valuesBySymbol ) ) {
const symbolValues = valuesBySymbol [ symbol ] ;
@ -647,91 +468,63 @@ export abstract class PortfolioCalculator {
dateString
] ? ? new Big ( 0 ) ;
accumulatedValuesByDate [ dateString ] . investmentValueWithCurrencyEffect =
accumulatedValuesByDate [
dateString
] . investmentValueWithCurrencyEffect . add (
investmentValueWithCurrencyEffect
) ;
accumulatedValuesByDate [ dateString ] . totalCurrentValue =
accumulatedValuesByDate [ dateString ] . totalCurrentValue . add (
currentValue
) ;
accumulatedValuesByDate [
dateString
] . totalCurrentValueWithCurrencyEffect = accumulatedValuesByDate [
dateString
] . totalCurrentValueWithCurrencyEffect . add (
currentValueWithCurrencyEffect
) ;
accumulatedValuesByDate [ dateString ] . totalInvestmentValue =
accumulatedValuesByDate [ dateString ] . totalInvestmentValue . add (
investmentValueAccumulated
) ;
accumulatedValuesByDate [
dateString
] . totalInvestmentValueWithCurrencyEffect = accumulatedValuesByDate [
dateString
] . totalInvestmentValueWithCurrencyEffect . add (
investmentValueAccumulatedWithCurrencyEffect
) ;
accumulatedValuesByDate [ dateString ] . totalNetPerformanceValue =
accumulatedValuesByDate [ dateString ] . totalNetPerformanceValue . add (
netPerformanceValue
) ;
accumulatedValuesByDate [
dateString
] . totalNetPerformanceValueWithCurrencyEffect = accumulatedValuesByDate [
dateString
] . totalNetPerformanceValueWithCurrencyEffect . add (
netPerformanceValueWithCurrencyEffect
) ;
accumulatedValuesByDate [ dateString ] . totalTimeWeightedInvestmentValue =
accumulatedValuesByDate [
dateString
] . totalTimeWeightedInvestmentValue . add ( timeWeightedInvestmentValue ) ;
accumulatedValuesByDate [
dateString
] . totalTimeWeightedInvestmentValueWithCurrencyEffect =
accumulatedValuesByDate [
dateString
] . totalTimeWeightedInvestmentValueWithCurrencyEffect . add (
timeWeightedInvestmentValueWithCurrencyEffect
) ;
}
if (
this . accountBalanceItems . some ( ( { date } ) = > {
accumulatedValuesByDate [ dateString ] = {
investmentValueWithCurrencyEffect : (
accumulatedValuesByDate [ dateString ]
? . investmentValueWithCurrencyEffect ? ? new Big ( 0 )
) . add ( investmentValueWithCurrencyEffect ) ,
totalAccountBalanceWithCurrencyEffect : this.accountBalanceItems.some (
( { date } ) = > {
return date === dateString ;
} )
) {
accumulatedValuesByDate [
dateString
] . totalAccountBalanceWithCurrencyEffect = new Big (
}
)
? new Big (
this . accountBalanceItems . find ( ( { date } ) = > {
return date === dateString ;
} ) . value
) ;
} else {
accumulatedValuesByDate [
dateString
] . totalAccountBalanceWithCurrencyEffect =
accumulatedValuesByDate [ lastDate ]
? . totalAccountBalanceWithCurrencyEffect ? ? new Big ( 0 ) ;
)
: ( accumulatedValuesByDate [ lastDate ]
? . totalAccountBalanceWithCurrencyEffect ? ? new Big ( 0 ) ) ,
totalCurrentValue : (
accumulatedValuesByDate [ dateString ] ? . totalCurrentValue ? ? new Big ( 0 )
) . add ( currentValue ) ,
totalCurrentValueWithCurrencyEffect : (
accumulatedValuesByDate [ dateString ]
? . totalCurrentValueWithCurrencyEffect ? ? new Big ( 0 )
) . add ( currentValueWithCurrencyEffect ) ,
totalInvestmentValue : (
accumulatedValuesByDate [ dateString ] ? . totalInvestmentValue ? ?
new Big ( 0 )
) . add ( investmentValueAccumulated ) ,
totalInvestmentValueWithCurrencyEffect : (
accumulatedValuesByDate [ dateString ]
? . totalInvestmentValueWithCurrencyEffect ? ? new Big ( 0 )
) . add ( investmentValueAccumulatedWithCurrencyEffect ) ,
totalNetPerformanceValue : (
accumulatedValuesByDate [ dateString ] ? . totalNetPerformanceValue ? ?
new Big ( 0 )
) . add ( netPerformanceValue ) ,
totalNetPerformanceValueWithCurrencyEffect : (
accumulatedValuesByDate [ dateString ]
? . totalNetPerformanceValueWithCurrencyEffect ? ? new Big ( 0 )
) . add ( netPerformanceValueWithCurrencyEffect ) ,
totalTimeWeightedInvestmentValue : (
accumulatedValuesByDate [ dateString ]
? . totalTimeWeightedInvestmentValue ? ? new Big ( 0 )
) . add ( timeWeightedInvestmentValue ) ,
totalTimeWeightedInvestmentValueWithCurrencyEffect : (
accumulatedValuesByDate [ dateString ]
? . totalTimeWeightedInvestmentValueWithCurrencyEffect ? ? new Big ( 0 )
) . add ( timeWeightedInvestmentValueWithCurrencyEffect )
} ;
}
lastDate = dateString ;
}
return Object . entries ( accumulatedValuesByDate ) . map ( ( [ date , values ] ) = > {
const historicalData : HistoricalDataItem [ ] = Object . entries (
accumulatedValuesByDate
) . map ( ( [ date , values ] ) = > {
const {
investmentValueWithCurrencyEffect ,
totalAccountBalanceWithCurrencyEffect ,
@ -749,7 +542,6 @@ export abstract class PortfolioCalculator {
? 0
: totalNetPerformanceValue
. div ( totalTimeWeightedInvestmentValue )
. mul ( 100 )
. toNumber ( ) ;
const netPerformanceInPercentageWithCurrencyEffect =
@ -757,7 +549,6 @@ export abstract class PortfolioCalculator {
? 0
: totalNetPerformanceValueWithCurrencyEffect
. div ( totalTimeWeightedInvestmentValueWithCurrencyEffect )
. mul ( 100 )
. toNumber ( ) ;
return {
@ -781,6 +572,19 @@ export abstract class PortfolioCalculator {
valueWithCurrencyEffect : totalCurrentValueWithCurrencyEffect.toNumber ( )
} ;
} ) ;
const overall = this . calculateOverallPerformance ( positions ) ;
return {
. . . overall ,
errors ,
historicalData ,
positions ,
totalInterestWithCurrencyEffect ,
totalLiabilitiesWithCurrencyEffect ,
totalValuablesWithCurrencyEffect ,
hasErrors : hasAnySymbolMetricsErrors || overall . hasErrors
} ;
}
public getDataProviderInfos() {
@ -861,6 +665,70 @@ export abstract class PortfolioCalculator {
return this . snapshot ;
}
public async getPerformance ( { end , start } ) {
await this . snapshotPromise ;
const { historicalData } = this . snapshot ;
const chart : HistoricalDataItem [ ] = [ ] ;
let netPerformanceAtStartDate : number ;
let netPerformanceWithCurrencyEffectAtStartDate : number ;
let totalInvestmentValuesWithCurrencyEffect : number [ ] = [ ] ;
for ( let historicalDataItem of historicalData ) {
const date = resetHours ( parseDate ( historicalDataItem . date ) ) ;
if ( ! isBefore ( date , start ) && ! isAfter ( date , end ) ) {
if ( ! isNumber ( netPerformanceAtStartDate ) ) {
netPerformanceAtStartDate = historicalDataItem . netPerformance ;
netPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem . netPerformanceWithCurrencyEffect ;
}
const netPerformanceSinceStartDate =
historicalDataItem . netPerformance - netPerformanceAtStartDate ;
const netPerformanceWithCurrencyEffectSinceStartDate =
historicalDataItem . netPerformanceWithCurrencyEffect -
netPerformanceWithCurrencyEffectAtStartDate ;
if ( historicalDataItem . totalInvestmentValueWithCurrencyEffect > 0 ) {
totalInvestmentValuesWithCurrencyEffect . push (
historicalDataItem . totalInvestmentValueWithCurrencyEffect
) ;
}
const timeWeightedInvestmentValue =
totalInvestmentValuesWithCurrencyEffect . length > 0
? sum ( totalInvestmentValuesWithCurrencyEffect ) /
totalInvestmentValuesWithCurrencyEffect . length
: 0 ;
chart . push ( {
. . . historicalDataItem ,
netPerformance :
historicalDataItem . netPerformance - netPerformanceAtStartDate ,
netPerformanceWithCurrencyEffect :
netPerformanceWithCurrencyEffectSinceStartDate ,
netPerformanceInPercentage :
netPerformanceSinceStartDate / timeWeightedInvestmentValue ,
netPerformanceInPercentageWithCurrencyEffect :
netPerformanceWithCurrencyEffectSinceStartDate /
timeWeightedInvestmentValue ,
// TODO: Add net worth with valuables
// netWorth: totalCurrentValueWithCurrencyEffect
// .plus(totalAccountBalanceWithCurrencyEffect)
// .toNumber()
netWorth : 0
} ) ;
}
}
return { chart } ;
}
public getStartDate() {
let firstAccountBalanceDate : Date ;
let firstActivityDate : Date ;
@ -889,23 +757,21 @@ export abstract class PortfolioCalculator {
}
protected abstract getSymbolMetrics ( {
chartDateMap ,
dataSource ,
end ,
exchangeRates ,
isChartMode ,
marketSymbolMap ,
start ,
step ,
symbol
} : {
chartDateMap : { [ date : string ] : boolean } ;
end : Date ;
exchangeRates : { [ dateString : string ] : number } ;
isChartMode? : boolean ;
marketSymbolMap : {
[ date : string ] : { [ symbol : string ] : Big } ;
} ;
start : Date ;
step? : number ;
} & AssetProfileIdentifier ) : SymbolMetrics ;
public getTransactionPoints() {
@ -918,6 +784,66 @@ export abstract class PortfolioCalculator {
return this . snapshot . totalValuablesWithCurrencyEffect ;
}
private getChartDateMap ( {
endDate ,
startDate ,
step
} : {
endDate : Date ;
startDate : Date ;
step : number ;
} ) {
// Create a map of all relevant chart dates:
// 1. Add transaction point dates
let chartDateMap = this . transactionPoints . reduce ( ( result , { date } ) = > {
result [ date ] = true ;
return result ;
} , { } ) ;
// 2. Add dates between transactions respecting the specified step size
for ( let date of eachDayOfInterval (
{ end : endDate , start : startDate } ,
{ step }
) ) {
chartDateMap [ format ( date , DATE_FORMAT ) ] = true ;
}
if ( step > 1 ) {
// Reduce the step size of recent dates
for ( let date of eachDayOfInterval (
{ end : endDate , start : subDays ( endDate , 90 ) } ,
{ step : 1 }
) ) {
chartDateMap [ format ( date , DATE_FORMAT ) ] = true ;
}
}
// Make sure the end date is present
chartDateMap [ format ( endDate , DATE_FORMAT ) ] = true ;
// Make sure some key dates are present
for ( let dateRange of [ '1d' , '1y' , '5y' , 'max' , 'mtd' , 'wtd' , 'ytd' ] ) {
const { endDate : dateRangeEnd , startDate : dateRangeStart } =
getIntervalFromDateRange ( dateRange ) ;
if (
! isBefore ( dateRangeStart , startDate ) &&
! isAfter ( dateRangeStart , endDate )
) {
chartDateMap [ format ( dateRangeStart , DATE_FORMAT ) ] = true ;
}
if (
! isBefore ( dateRangeEnd , startDate ) &&
! isAfter ( dateRangeEnd , endDate )
) {
chartDateMap [ format ( dateRangeEnd , DATE_FORMAT ) ] = true ;
}
}
return chartDateMap ;
}
private computeTransactionPoints() {
this . transactionPoints = [ ] ;
const symbols : { [ symbol : string ] : TransactionPointSymbol } = { } ;
@ -1057,11 +983,11 @@ export abstract class PortfolioCalculator {
}
private async initialize() {
if ( this . useCache ) {
const startTimeTotal = performance . now ( ) ;
const cachedSnapshot = await this . redisCacheService . get (
this . redisCacheService . getPortfolioSnapshotKey ( {
filters : this.filters ,
userId : this.userId
} )
) ;
@ -1080,13 +1006,11 @@ export abstract class PortfolioCalculator {
'PortfolioCalculator'
) ;
} else {
this . snapshot = await this . computeSnapshot (
this . startDate ,
this . endDate
) ;
this . snapshot = await this . computeSnapshot ( ) ;
this . redisCacheService . set (
this . redisCacheService . getPortfolioSnapshotKey ( {
filters : this.filters ,
userId : this.userId
} ) ,
JSON . stringify ( this . snapshot ) ,
@ -1101,8 +1025,5 @@ export abstract class PortfolioCalculator {
'PortfolioCalculator'
) ;
}
} else {
this . snapshot = await this . computeSnapshot ( this . startDate , this . endDate ) ;
}
}
}