@ -48,6 +48,7 @@ import type {
RequestWithUser ,
UserWithSettings
} from '@ghostfolio/common/types' ;
import { Inject , Injectable } from '@nestjs/common' ;
import { REQUEST } from '@nestjs/core' ;
import {
@ -73,11 +74,13 @@ import {
min ,
parseISO ,
set ,
setDayOfYear ,
startOfWeek ,
startOfMonth ,
startOfYear ,
subDays ,
subYears
} from 'date-fns' ;
import { isEmpty , last , sortBy , uniq , uniqBy } from 'lodash' ;
import { isEmpty , last , uniq , uniqBy } from 'lodash' ;
import {
HistoricalDataContainer ,
@ -285,82 +288,38 @@ export class PortfolioService {
const portfolioCalculator = new PortfolioCalculator ( {
currency : this.request.user.Settings.settings.baseCurrency ,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : portfolioOrders
} ) ;
portfolioCalculator . setTransactionPoints ( transactionPoints ) ;
let investments : InvestmentItem [ ] ;
if ( groupBy ) {
investments = portfolioCalculator
. getInvestmentsByGroup ( groupBy )
. map ( ( item ) = > {
return {
date : item.date ,
investment : item.investment.toNumber ( )
} ;
const { items } = await this . getChart ( {
dateRange ,
impersonationId ,
portfolioOrders ,
transactionPoints ,
userId ,
userCurrency : this.request.user.Settings.settings.baseCurrency ,
withDataDecimation : false
} ) ;
// Add investment of current group
const dateOfCurrentGroup = format (
set ( new Date ( ) , {
date : 1 ,
month : groupBy === 'year' ? 0 : new Date ( ) . getMonth ( )
} ) ,
DATE_FORMAT
) ;
const investmentOfCurrentGroup = investments . filter ( ( { date } ) = > {
return date === dateOfCurrentGroup ;
} ) ;
let investments : InvestmentItem [ ] ;
if ( investmentOfCurrentGroup . length <= 0 ) {
investments . push ( {
date : dateOfCurrentGroup ,
investment : 0
if ( groupBy ) {
investments = portfolioCalculator . getInvestmentsByGroup ( {
groupBy ,
data : items
} ) ;
}
} else {
investments = portfolioCalculator
. getInvestments ( )
. map ( ( { date , investment } ) = > {
investments = items . map ( ( { date , investmentValueWithCurrencyEffect } ) = > {
return {
date ,
investment : investment.toNumber ( )
investment : investmentValueWithCurrencyEffect
} ;
} ) ;
// Add investment of today
const investmentOfToday = investments . filter ( ( { date } ) = > {
return date === format ( new Date ( ) , DATE_FORMAT ) ;
} ) ;
if ( investmentOfToday . length <= 0 ) {
const pastInvestments = investments . filter ( ( { date } ) = > {
return isBefore ( parseDate ( date ) , new Date ( ) ) ;
} ) ;
const lastInvestment = pastInvestments [ pastInvestments . length - 1 ] ;
investments . push ( {
date : format ( new Date ( ) , DATE_FORMAT ) ,
investment : lastInvestment?.investment ? ? 0
} ) ;
}
}
investments = sortBy ( investments , ( investment ) = > {
return investment . date ;
} ) ;
const startDate = this . getStartDate (
dateRange ,
parseDate ( investments [ 0 ] ? . date )
) ;
investments = investments . filter ( ( { date } ) = > {
return ! isBefore ( parseDate ( date ) , startDate ) ;
} ) ;
let streaks : PortfolioInvestments [ 'streaks' ] ;
if ( savingsRate ) {
@ -407,6 +366,7 @@ export class PortfolioService {
const portfolioCalculator = new PortfolioCalculator ( {
currency : userCurrency ,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : portfolioOrders
} ) ;
@ -480,7 +440,7 @@ export class PortfolioService {
continue ;
}
const value = item . quantity . mul ( item . marketPrice ? ? 0 ) ;
const value = item . quantity . mul ( item . marketPriceInBaseCurrency ? ? 0 ) ;
const symbolProfile = symbolProfileMap [ item . symbol ] ;
const dataProviderResponse = dataProviderResponses [ item . symbol ] ;
@ -697,6 +657,7 @@ export class PortfolioService {
if ( orders . length <= 0 ) {
return {
tags ,
accounts : [ ] ,
averagePrice : undefined ,
dataProviderInfo : undefined ,
dividendInBaseCurrency : undefined ,
@ -704,6 +665,8 @@ export class PortfolioService {
firstBuyDate : undefined ,
grossPerformance : undefined ,
grossPerformancePercent : undefined ,
grossPerformancePercentWithCurrencyEffect : undefined ,
grossPerformanceWithCurrencyEffect : undefined ,
historicalData : [ ] ,
investment : undefined ,
marketPrice : undefined ,
@ -711,6 +674,8 @@ export class PortfolioService {
minPrice : undefined ,
netPerformance : undefined ,
netPerformancePercent : undefined ,
netPerformancePercentWithCurrencyEffect : undefined ,
netPerformanceWithCurrencyEffect : undefined ,
orders : [ ] ,
quantity : undefined ,
SymbolProfile : undefined ,
@ -719,7 +684,6 @@ export class PortfolioService {
} ;
}
const positionCurrency = orders [ 0 ] . SymbolProfile . currency ;
const [ SymbolProfile ] = await this . symbolProfileService . getSymbolProfiles ( [
{ dataSource : aDataSource , symbol : aSymbol }
] ) ;
@ -746,8 +710,9 @@ export class PortfolioService {
tags = uniqBy ( tags , 'id' ) ;
const portfolioCalculator = new PortfolioCalculator ( {
currency : position Currency,
currency : user Currency,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : portfolioOrders
} ) ;
@ -755,12 +720,13 @@ export class PortfolioService {
const transactionPoints = portfolioCalculator . getTransactionPoints ( ) ;
const portfolioStart = parseDate ( transactionPoints [ 0 ] . date ) ;
const currentPositions =
await portfolioCalculator . getCurrentPositions ( portfolioStart ) ;
const position = currentPositions . positions . find (
( item ) = > item . symbol === aSymbol
) ;
const position = currentPositions . positions . find ( ( { symbol } ) = > {
return symbol === aSymbol ;
} ) ;
if ( position ) {
const {
@ -774,6 +740,13 @@ export class PortfolioService {
transactionCount
} = position ;
const accounts : PortfolioPositionDetail [ 'accounts' ] = uniqBy (
orders ,
'Account.id'
) . map ( ( { Account } ) = > {
return Account ;
} ) ;
const dividendInBaseCurrency = getSum (
orders
. filter ( ( { type } ) = > {
@ -784,23 +757,6 @@ export class PortfolioService {
} )
) ;
// Convert investment, gross and net performance to currency of user
const investment = this . exchangeRateDataService . toCurrency (
position . investment ? . toNumber ( ) ,
currency ,
userCurrency
) ;
const grossPerformance = this . exchangeRateDataService . toCurrency (
position . grossPerformance ? . toNumber ( ) ,
currency ,
userCurrency
) ;
const netPerformance = this . exchangeRateDataService . toCurrency (
position . netPerformance ? . toNumber ( ) ,
currency ,
userCurrency
) ;
const historicalData = await this . dataProviderService . getHistorical (
[ { dataSource , symbol : aSymbol } ] ,
'day' ,
@ -864,13 +820,11 @@ export class PortfolioService {
}
return {
accounts ,
firstBuyDate ,
grossPerformance ,
investment ,
marketPrice ,
maxPrice ,
minPrice ,
netPerformance ,
orders ,
SymbolProfile ,
tags ,
@ -883,10 +837,21 @@ export class PortfolioService {
SymbolProfile . currency ,
userCurrency
) ,
grossPerformance : position.grossPerformance?.toNumber ( ) ,
grossPerformancePercent :
position . grossPerformancePercentage ? . toNumber ( ) ,
grossPerformancePercentWithCurrencyEffect :
position . grossPerformancePercentageWithCurrencyEffect ? . toNumber ( ) ,
grossPerformanceWithCurrencyEffect :
position . grossPerformanceWithCurrencyEffect ? . toNumber ( ) ,
historicalData : historicalDataArray ,
investment : position.investment?.toNumber ( ) ,
netPerformance : position.netPerformance?.toNumber ( ) ,
netPerformancePercent : position.netPerformancePercentage?.toNumber ( ) ,
netPerformancePercentWithCurrencyEffect :
position . netPerformancePercentageWithCurrencyEffect ? . toNumber ( ) ,
netPerformanceWithCurrencyEffect :
position . netPerformanceWithCurrencyEffect ? . toNumber ( ) ,
quantity : quantity.toNumber ( ) ,
value : this.exchangeRateDataService.toCurrency (
quantity . mul ( marketPrice ? ? 0 ) . toNumber ( ) ,
@ -938,6 +903,7 @@ export class PortfolioService {
orders ,
SymbolProfile ,
tags ,
accounts : [ ] ,
averagePrice : 0 ,
dataProviderInfo : undefined ,
dividendInBaseCurrency : 0 ,
@ -945,10 +911,14 @@ export class PortfolioService {
firstBuyDate : undefined ,
grossPerformance : undefined ,
grossPerformancePercent : undefined ,
grossPerformancePercentWithCurrencyEffect : undefined ,
grossPerformanceWithCurrencyEffect : undefined ,
historicalData : historicalDataArray ,
investment : 0 ,
netPerformance : undefined ,
netPerformancePercent : undefined ,
netPerformancePercentWithCurrencyEffect : undefined ,
netPerformanceWithCurrencyEffect : undefined ,
quantity : 0 ,
transactionCount : undefined ,
value : 0
@ -986,6 +956,7 @@ export class PortfolioService {
const portfolioCalculator = new PortfolioCalculator ( {
currency : this.request.user.Settings.settings.baseCurrency ,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : portfolioOrders
} ) ;
@ -1017,6 +988,7 @@ export class PortfolioService {
] ) ;
const symbolProfileMap : { [ symbol : string ] : EnhancedSymbolProfile } = { } ;
for ( const symbolProfile of symbolProfiles ) {
symbolProfileMap [ symbolProfile . symbol ] = symbolProfile ;
}
@ -1035,25 +1007,64 @@ export class PortfolioService {
return {
hasErrors : currentPositions.hasErrors ,
positions : positions.map ( ( position ) = > {
positions : positions.map (
( {
averagePrice ,
currency ,
dataSource ,
firstBuyDate ,
grossPerformance ,
grossPerformancePercentage ,
grossPerformancePercentageWithCurrencyEffect ,
grossPerformanceWithCurrencyEffect ,
investment ,
investmentWithCurrencyEffect ,
netPerformance ,
netPerformancePercentage ,
netPerformancePercentageWithCurrencyEffect ,
netPerformanceWithCurrencyEffect ,
quantity ,
symbol ,
timeWeightedInvestment ,
timeWeightedInvestmentWithCurrencyEffect ,
transactionCount
} ) = > {
return {
. . . position ,
assetClass : symbolProfileMap [ position . symbol ] . assetClass ,
assetSubClass : symbolProfileMap [ position . symbol ] . assetSubClass ,
averagePrice : new Big ( position . averagePrice ) . toNumber ( ) ,
grossPerformance : position.grossPerformance?.toNumber ( ) ? ? null ,
currency ,
dataSource ,
firstBuyDate ,
symbol ,
transactionCount ,
assetClass : symbolProfileMap [ symbol ] . assetClass ,
assetSubClass : symbolProfileMap [ symbol ] . assetSubClass ,
averagePrice : averagePrice.toNumber ( ) ,
grossPerformance : grossPerformance?.toNumber ( ) ? ? null ,
grossPerformancePercentage :
position . grossPerformancePercentage ? . toNumber ( ) ? ? null ,
investment : new Big ( position . investment ) . toNumber ( ) ,
grossPerformancePercentage ? . toNumber ( ) ? ? null ,
grossPerformancePercentageWithCurrencyEffect :
grossPerformancePercentageWithCurrencyEffect ? . toNumber ( ) ? ? null ,
grossPerformanceWithCurrencyEffect :
grossPerformanceWithCurrencyEffect ? . toNumber ( ) ? ? null ,
investment : investment.toNumber ( ) ,
investmentWithCurrencyEffect :
investmentWithCurrencyEffect ? . toNumber ( ) ,
marketState :
dataProviderResponses [ position . symbol ] ? . marketState ? ? 'delayed' ,
name : symbolProfileMap [ position . symbol ] . name ,
netPerformance : position.netPerformance?.toNumber ( ) ? ? null ,
dataProviderResponses [ symbol ] ? . marketState ? ? 'delayed' ,
name : symbolProfileMap [ symbol ] . name ,
netPerformance : netPerformance?.toNumber ( ) ? ? null ,
netPerformancePercentage :
position . netPerformancePercentage ? . toNumber ( ) ? ? null ,
quantity : new Big ( position . quantity ) . toNumber ( )
netPerformancePercentage ? . toNumber ( ) ? ? null ,
netPerformancePercentageWithCurrencyEffect :
netPerformancePercentageWithCurrencyEffect ? . toNumber ( ) ? ? null ,
netPerformanceWithCurrencyEffect :
netPerformanceWithCurrencyEffect ? . toNumber ( ) ? ? null ,
quantity : quantity.toNumber ( ) ,
timeWeightedInvestment : timeWeightedInvestment?.toNumber ( ) ,
timeWeightedInvestmentWithCurrencyEffect :
timeWeightedInvestmentWithCurrencyEffect ? . toNumber ( )
} ;
} )
}
)
} ;
}
@ -1109,6 +1120,7 @@ export class PortfolioService {
const portfolioCalculator = new PortfolioCalculator ( {
currency : userCurrency ,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : portfolioOrders
} ) ;
@ -1120,8 +1132,12 @@ export class PortfolioService {
performance : {
currentGrossPerformance : 0 ,
currentGrossPerformancePercent : 0 ,
currentGrossPerformancePercentWithCurrencyEffect : 0 ,
currentGrossPerformanceWithCurrencyEffect : 0 ,
currentNetPerformance : 0 ,
currentNetPerformancePercent : 0 ,
currentNetPerformancePercentWithCurrencyEffect : 0 ,
currentNetPerformanceWithCurrencyEffect : 0 ,
currentNetWorth : 0 ,
currentValue : 0 ,
totalInvestment : 0
@ -1146,17 +1162,26 @@ export class PortfolioService {
errors ,
grossPerformance ,
grossPerformancePercentage ,
grossPerformancePercentageWithCurrencyEffect ,
grossPerformanceWithCurrencyEffect ,
hasErrors ,
netPerformance ,
netPerformancePercentage ,
netPerformancePercentageWithCurrencyEffect ,
netPerformanceWithCurrencyEffect ,
totalInvestment
} = await portfolioCalculator . getCurrentPositions ( startDate ) ;
const currentGrossPerformance = grossPerformance ;
const currentGrossPerformancePercent = grossPerformancePercentage ;
let currentNetPerformance = netPerformance ;
let currentNetPerformancePercent = netPerformancePercentage ;
let currentNetPerformancePercentWithCurrencyEffect =
netPerformancePercentageWithCurrencyEffect ;
let currentNetPerformanceWithCurrencyEffect =
netPerformanceWithCurrencyEffect ;
const { items } = await this . getChart ( {
dateRange ,
impersonationId ,
@ -1172,9 +1197,18 @@ export class PortfolioService {
if ( itemOfToday ) {
currentNetPerformance = new Big ( itemOfToday . netPerformance ) ;
currentNetPerformancePercent = new Big (
itemOfToday . netPerformanceInPercentage
) . div ( 100 ) ;
currentNetPerformancePercentWithCurrencyEffect = new Big (
itemOfToday . netPerformanceInPercentageWithCurrencyEffect
) . div ( 100 ) ;
currentNetPerformanceWithCurrencyEffect = new Big (
itemOfToday . netPerformanceWithCurrencyEffect
) ;
}
accountBalanceItems = accountBalanceItems . filter ( ( { date } ) = > {
@ -1207,11 +1241,18 @@ export class PortfolioService {
firstOrderDate : parseDate ( items [ 0 ] ? . date ) ,
performance : {
currentNetWorth ,
currentGrossPerformance : currentGrossPerformance.toNumber ( ) ,
currentGrossPerformancePercent :
currentGrossPerformancePercent . toNumber ( ) ,
currentGrossPerformance : grossPerformance.toNumber ( ) ,
currentGrossPerformancePercent : grossPerformancePercentage.toNumber ( ) ,
currentGrossPerformancePercentWithCurrencyEffect :
grossPerformancePercentageWithCurrencyEffect . toNumber ( ) ,
currentGrossPerformanceWithCurrencyEffect :
grossPerformanceWithCurrencyEffect . toNumber ( ) ,
currentNetPerformance : currentNetPerformance.toNumber ( ) ,
currentNetPerformancePercent : currentNetPerformancePercent.toNumber ( ) ,
currentNetPerformancePercentWithCurrencyEffect :
currentNetPerformancePercentWithCurrencyEffect . toNumber ( ) ,
currentNetPerformanceWithCurrencyEffect :
currentNetPerformanceWithCurrencyEffect . toNumber ( ) ,
currentValue : currentValue.toNumber ( ) ,
totalInvestment : totalInvestment.toNumber ( )
}
@ -1231,6 +1272,7 @@ export class PortfolioService {
const portfolioCalculator = new PortfolioCalculator ( {
currency : userCurrency ,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : portfolioOrders
} ) ;
@ -1372,7 +1414,8 @@ export class PortfolioService {
portfolioOrders ,
transactionPoints ,
userCurrency ,
userId
userId ,
withDataDecimation = true
} : {
dateRange? : DateRange ;
impersonationId : string ;
@ -1380,6 +1423,7 @@ export class PortfolioService {
transactionPoints : TransactionPoint [ ] ;
userCurrency : string ;
userId : string ;
withDataDecimation? : boolean ;
} ) : Promise < HistoricalDataContainer > {
if ( transactionPoints . length === 0 ) {
return {
@ -1394,6 +1438,7 @@ export class PortfolioService {
const portfolioCalculator = new PortfolioCalculator ( {
currency : userCurrency ,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : portfolioOrders
} ) ;
@ -1404,16 +1449,18 @@ export class PortfolioService {
const portfolioStart = parseDate ( transactionPoints [ 0 ] . date ) ;
const startDate = this . getStartDate ( dateRange , portfolioStart ) ;
let step = 1 ;
if ( withDataDecimation ) {
const daysInMarket = differenceInDays ( new Date ( ) , startDate ) ;
const step = Math . round (
daysInMarket / Math . min ( daysInMarket , MAX_CHART_ITEMS )
) ;
step = Math . round ( daysInMarket / Math . min ( daysInMarket , MAX_CHART_ITEMS ) ) ;
}
const items = await portfolioCalculator . getChartData (
startDat e ,
endDate ,
step
) ;
const items = await portfolioCalculator . getChartData ( {
step ,
end : end Date ,
start : startDate
} ) ;
return {
items ,
@ -1574,10 +1621,25 @@ export class PortfolioService {
subDays ( new Date ( ) . setHours ( 0 , 0 , 0 , 0 ) , 1 )
] ) ;
break ;
case 'mtd' :
portfolioStart = max ( [
portfolioStart ,
subDays ( startOfMonth ( new Date ( ) . setHours ( 0 , 0 , 0 , 0 ) ) , 1 )
] ) ;
break ;
case 'wtd' :
portfolioStart = max ( [
portfolioStart ,
subDays (
startOfWeek ( new Date ( ) . setHours ( 0 , 0 , 0 , 0 ) , { weekStartsOn : 1 } ) ,
1
)
] ) ;
break ;
case 'ytd' :
portfolioStart = max ( [
portfolioStart ,
setDayOfYear ( new Date ( ) . setHours ( 0 , 0 , 0 , 0 ) , 1 )
subDays ( start OfYear ( new Date ( ) . setHours ( 0 , 0 , 0 , 0 ) ) , 1 )
] ) ;
break ;
case '1y' :
@ -1593,6 +1655,7 @@ export class PortfolioService {
] ) ;
break ;
}
return portfolioStart ;
}
@ -1738,6 +1801,7 @@ export class PortfolioService {
const annualizedPerformancePercent = new PortfolioCalculator ( {
currency : userCurrency ,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : [ ]
} )
. getAnnualizedPerformancePercent ( {
@ -1847,30 +1911,19 @@ export class PortfolioService {
currency : order.SymbolProfile.currency ,
dataSource : order.SymbolProfile.dataSource ,
date : format ( order . date , DATE_FORMAT ) ,
fee : new Big (
this . exchangeRateDataService . toCurrency (
order . fee ,
order . SymbolProfile . currency ,
userCurrency
)
) ,
fee : new Big ( order . fee ) ,
name : order.SymbolProfile?.name ,
quantity : new Big ( order . quantity ) ,
symbol : order . SymbolProfile . symbol ,
tags : order.tags ,
type : order . type ,
unitPrice : new Big (
this . exchangeRateDataService . toCurrency (
order . unitPrice ,
order . SymbolProfile . currency ,
userCurrency
)
)
unitPrice : new Big ( order . unitPrice )
} ) ) ;
const portfolioCalculator = new PortfolioCalculator ( {
currency : userCurrency ,
currentRateService : this.currentRateService ,
exchangeRateDataService : this.exchangeRateDataService ,
orders : portfolioOrders
} ) ;
@ -2006,7 +2059,8 @@ export class PortfolioService {
for ( const order of ordersByAccount ) {
let currentValueOfSymbolInBaseCurrency =
order . quantity *
( portfolioItemsNow [ order . SymbolProfile . symbol ] ? . marketPrice ? ?
( portfolioItemsNow [ order . SymbolProfile . symbol ]
? . marketPriceInBaseCurrency ? ?
order . unitPrice ? ?
0 ) ;
@ -2080,9 +2134,9 @@ export class PortfolioService {
}
) ;
historicalDataItems . sort (
( a , b ) = > new Date ( a . date ) . getTime ( ) - new Date ( b . date ) . getTime ( )
) ;
historicalDataItems . sort ( ( a , b ) = > {
return new Date ( a . date ) . getTime ( ) - new Date ( b . date ) . getTime ( ) ;
} ) ;
return historicalDataItems ;
}