|
|
@ -75,6 +75,7 @@ import { |
|
|
|
set, |
|
|
|
setDayOfYear, |
|
|
|
subDays, |
|
|
|
subMonths, |
|
|
|
subYears |
|
|
|
} from 'date-fns'; |
|
|
|
import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash'; |
|
|
@ -470,11 +471,9 @@ export class PortfolioService { |
|
|
|
} |
|
|
|
|
|
|
|
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; |
|
|
|
for (const position of currentPositions.positions) { |
|
|
|
portfolioItemsNow[position.symbol] = position; |
|
|
|
} |
|
|
|
|
|
|
|
for (const item of currentPositions.positions) { |
|
|
|
portfolioItemsNow[item.symbol] = item; |
|
|
|
if (item.quantity.lte(0)) { |
|
|
|
// Ignore positions without any quantity
|
|
|
|
continue; |
|
|
@ -500,59 +499,12 @@ export class PortfolioService { |
|
|
|
otherMarkets: 0 |
|
|
|
}; |
|
|
|
|
|
|
|
if (symbolProfile.countries.length > 0) { |
|
|
|
for (const country of symbolProfile.countries) { |
|
|
|
if (developedMarkets.includes(country.code)) { |
|
|
|
markets.developedMarkets = new Big(markets.developedMarkets) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (emergingMarkets.includes(country.code)) { |
|
|
|
markets.emergingMarkets = new Big(markets.emergingMarkets) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else { |
|
|
|
markets.otherMarkets = new Big(markets.otherMarkets) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} |
|
|
|
|
|
|
|
if (country.code === 'JP') { |
|
|
|
marketsAdvanced.japan = new Big(marketsAdvanced.japan) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (country.code === 'CA' || country.code === 'US') { |
|
|
|
marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (asiaPacificMarkets.includes(country.code)) { |
|
|
|
marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (emergingMarkets.includes(country.code)) { |
|
|
|
marketsAdvanced.emergingMarkets = new Big( |
|
|
|
marketsAdvanced.emergingMarkets |
|
|
|
) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (europeMarkets.includes(country.code)) { |
|
|
|
marketsAdvanced.europe = new Big(marketsAdvanced.europe) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else { |
|
|
|
marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY]) |
|
|
|
.plus(value) |
|
|
|
.toNumber(); |
|
|
|
|
|
|
|
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) |
|
|
|
.plus(value) |
|
|
|
.toNumber(); |
|
|
|
} |
|
|
|
this.calculateMarketsAllocation( |
|
|
|
symbolProfile, |
|
|
|
markets, |
|
|
|
marketsAdvanced, |
|
|
|
value |
|
|
|
); |
|
|
|
|
|
|
|
holdings[item.symbol] = { |
|
|
|
markets, |
|
|
@ -585,6 +537,68 @@ export class PortfolioService { |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
await this.handleCashPosition( |
|
|
|
filters, |
|
|
|
isFilteredByAccount, |
|
|
|
cashDetails, |
|
|
|
userCurrency, |
|
|
|
filteredValueInBaseCurrency, |
|
|
|
holdings |
|
|
|
); |
|
|
|
|
|
|
|
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({ |
|
|
|
filters, |
|
|
|
orders, |
|
|
|
portfolioItemsNow, |
|
|
|
userCurrency, |
|
|
|
userId, |
|
|
|
withExcludedAccounts |
|
|
|
}); |
|
|
|
|
|
|
|
filteredValueInBaseCurrency = await this.handleEmergencyFunds( |
|
|
|
filters, |
|
|
|
cashDetails, |
|
|
|
userCurrency, |
|
|
|
filteredValueInBaseCurrency, |
|
|
|
emergencyFund, |
|
|
|
orders, |
|
|
|
accounts, |
|
|
|
holdings |
|
|
|
); |
|
|
|
|
|
|
|
const summary = await this.getSummary({ |
|
|
|
impersonationId, |
|
|
|
userCurrency, |
|
|
|
userId, |
|
|
|
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, |
|
|
|
emergencyFundPositionsValueInBaseCurrency: |
|
|
|
this.getEmergencyFundPositionsValueInBaseCurrency({ |
|
|
|
holdings |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
return { |
|
|
|
accounts, |
|
|
|
holdings, |
|
|
|
platforms, |
|
|
|
summary, |
|
|
|
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), |
|
|
|
filteredValueInPercentage: summary.netWorth |
|
|
|
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() |
|
|
|
: 0, |
|
|
|
hasErrors: currentPositions.hasErrors, |
|
|
|
totalValueInBaseCurrency: summary.netWorth |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
private async handleCashPosition( |
|
|
|
filters: Filter[], |
|
|
|
isFilteredByAccount: boolean, |
|
|
|
cashDetails: CashDetails, |
|
|
|
userCurrency: string, |
|
|
|
filteredValueInBaseCurrency: Big, |
|
|
|
holdings: { [symbol: string]: PortfolioPosition } |
|
|
|
) { |
|
|
|
const isFilteredByCash = filters?.some((filter) => { |
|
|
|
return filter.type === 'ASSET_CLASS' && filter.id === 'CASH'; |
|
|
|
}); |
|
|
@ -600,16 +614,26 @@ export class PortfolioService { |
|
|
|
holdings[symbol] = cashPositions[symbol]; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({ |
|
|
|
filters, |
|
|
|
orders, |
|
|
|
portfolioItemsNow, |
|
|
|
userCurrency, |
|
|
|
userId, |
|
|
|
withExcludedAccounts |
|
|
|
}); |
|
|
|
|
|
|
|
private async handleEmergencyFunds( |
|
|
|
filters: Filter[], |
|
|
|
cashDetails: CashDetails, |
|
|
|
userCurrency: string, |
|
|
|
filteredValueInBaseCurrency: Big, |
|
|
|
emergencyFund: Big, |
|
|
|
orders: Activity[], |
|
|
|
accounts: { |
|
|
|
[id: string]: { |
|
|
|
balance: number; |
|
|
|
currency: string; |
|
|
|
name: string; |
|
|
|
valueInBaseCurrency: number; |
|
|
|
valueInPercentage?: number; |
|
|
|
}; |
|
|
|
}, |
|
|
|
holdings: { [symbol: string]: PortfolioPosition } |
|
|
|
) { |
|
|
|
if ( |
|
|
|
filters?.length === 1 && |
|
|
|
filters[0].id === EMERGENCY_FUND_TAG_ID && |
|
|
@ -644,30 +668,79 @@ export class PortfolioService { |
|
|
|
valueInBaseCurrency: emergencyFundInCash |
|
|
|
}; |
|
|
|
} |
|
|
|
return filteredValueInBaseCurrency; |
|
|
|
} |
|
|
|
|
|
|
|
const summary = await this.getSummary({ |
|
|
|
impersonationId, |
|
|
|
userCurrency, |
|
|
|
userId, |
|
|
|
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, |
|
|
|
emergencyFundPositionsValueInBaseCurrency: |
|
|
|
this.getEmergencyFundPositionsValueInBaseCurrency({ |
|
|
|
holdings |
|
|
|
}) |
|
|
|
}); |
|
|
|
private calculateMarketsAllocation( |
|
|
|
symbolProfile: EnhancedSymbolProfile, |
|
|
|
markets: { |
|
|
|
developedMarkets: number; |
|
|
|
emergingMarkets: number; |
|
|
|
otherMarkets: number; |
|
|
|
}, |
|
|
|
marketsAdvanced: { |
|
|
|
asiaPacific: number; |
|
|
|
emergingMarkets: number; |
|
|
|
europe: number; |
|
|
|
japan: number; |
|
|
|
northAmerica: number; |
|
|
|
otherMarkets: number; |
|
|
|
}, |
|
|
|
value: Big |
|
|
|
) { |
|
|
|
if (symbolProfile.countries.length > 0) { |
|
|
|
for (const country of symbolProfile.countries) { |
|
|
|
if (developedMarkets.includes(country.code)) { |
|
|
|
markets.developedMarkets = new Big(markets.developedMarkets) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (emergingMarkets.includes(country.code)) { |
|
|
|
markets.emergingMarkets = new Big(markets.emergingMarkets) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else { |
|
|
|
markets.otherMarkets = new Big(markets.otherMarkets) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
accounts, |
|
|
|
holdings, |
|
|
|
platforms, |
|
|
|
summary, |
|
|
|
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), |
|
|
|
filteredValueInPercentage: summary.netWorth |
|
|
|
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() |
|
|
|
: 0, |
|
|
|
hasErrors: currentPositions.hasErrors, |
|
|
|
totalValueInBaseCurrency: summary.netWorth |
|
|
|
}; |
|
|
|
if (country.code === 'JP') { |
|
|
|
marketsAdvanced.japan = new Big(marketsAdvanced.japan) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (country.code === 'CA' || country.code === 'US') { |
|
|
|
marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (asiaPacificMarkets.includes(country.code)) { |
|
|
|
marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (emergingMarkets.includes(country.code)) { |
|
|
|
marketsAdvanced.emergingMarkets = new Big( |
|
|
|
marketsAdvanced.emergingMarkets |
|
|
|
) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else if (europeMarkets.includes(country.code)) { |
|
|
|
marketsAdvanced.europe = new Big(marketsAdvanced.europe) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} else { |
|
|
|
marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets) |
|
|
|
.plus(country.weight) |
|
|
|
.toNumber(); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY]) |
|
|
|
.plus(value) |
|
|
|
.toNumber(); |
|
|
|
|
|
|
|
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) |
|
|
|
.plus(value) |
|
|
|
.toNumber(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public async getPosition( |
|
|
@ -700,6 +773,7 @@ export class PortfolioService { |
|
|
|
averagePrice: undefined, |
|
|
|
dataProviderInfo: undefined, |
|
|
|
dividendInBaseCurrency: undefined, |
|
|
|
stakeRewards: undefined, |
|
|
|
feeInBaseCurrency: undefined, |
|
|
|
firstBuyDate: undefined, |
|
|
|
grossPerformance: undefined, |
|
|
@ -728,7 +802,11 @@ export class PortfolioService { |
|
|
|
.filter((order) => { |
|
|
|
tags = tags.concat(order.tags); |
|
|
|
|
|
|
|
return order.type === 'BUY' || order.type === 'SELL'; |
|
|
|
return ( |
|
|
|
order.type === 'BUY' || |
|
|
|
order.type === 'SELL' || |
|
|
|
order.type === 'STAKE' |
|
|
|
); |
|
|
|
}) |
|
|
|
.map((order) => ({ |
|
|
|
currency: order.SymbolProfile.currency, |
|
|
@ -784,6 +862,16 @@ export class PortfolioService { |
|
|
|
}) |
|
|
|
); |
|
|
|
|
|
|
|
const stakeRewards = getSum( |
|
|
|
orders |
|
|
|
.filter(({ type }) => { |
|
|
|
return type === 'STAKE'; |
|
|
|
}) |
|
|
|
.map(({ quantity }) => { |
|
|
|
return new Big(quantity); |
|
|
|
}) |
|
|
|
); |
|
|
|
|
|
|
|
// Convert investment, gross and net performance to currency of user
|
|
|
|
const investment = this.exchangeRateDataService.toCurrency( |
|
|
|
position.investment?.toNumber(), |
|
|
@ -878,6 +966,7 @@ export class PortfolioService { |
|
|
|
averagePrice: averagePrice.toNumber(), |
|
|
|
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], |
|
|
|
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), |
|
|
|
stakeRewards: stakeRewards.toNumber(), |
|
|
|
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( |
|
|
|
fee.toNumber(), |
|
|
|
SymbolProfile.currency, |
|
|
@ -941,6 +1030,7 @@ export class PortfolioService { |
|
|
|
averagePrice: 0, |
|
|
|
dataProviderInfo: undefined, |
|
|
|
dividendInBaseCurrency: 0, |
|
|
|
stakeRewards: 0, |
|
|
|
feeInBaseCurrency: 0, |
|
|
|
firstBuyDate: undefined, |
|
|
|
grossPerformance: undefined, |
|
|
@ -1049,6 +1139,7 @@ export class PortfolioService { |
|
|
|
dataProviderResponses[position.symbol]?.marketState ?? 'delayed', |
|
|
|
name: symbolProfileMap[position.symbol].name, |
|
|
|
netPerformance: position.netPerformance?.toNumber() ?? null, |
|
|
|
tags: symbolProfileMap[position.symbol].tags, |
|
|
|
netPerformancePercentage: |
|
|
|
position.netPerformancePercentage?.toNumber() ?? null, |
|
|
|
quantity: new Big(position.quantity).toNumber() |
|
|
@ -1580,6 +1671,28 @@ export class PortfolioService { |
|
|
|
setDayOfYear(new Date().setHours(0, 0, 0, 0), 1) |
|
|
|
]); |
|
|
|
break; |
|
|
|
|
|
|
|
case '1w': |
|
|
|
portfolioStart = max([ |
|
|
|
portfolioStart, |
|
|
|
subDays(new Date().setHours(0, 0, 0, 0), 7) |
|
|
|
]); |
|
|
|
break; |
|
|
|
|
|
|
|
case '1m': |
|
|
|
portfolioStart = max([ |
|
|
|
portfolioStart, |
|
|
|
subMonths(new Date().setHours(0, 0, 0, 0), 1) |
|
|
|
]); |
|
|
|
break; |
|
|
|
|
|
|
|
case '3m': |
|
|
|
portfolioStart = max([ |
|
|
|
portfolioStart, |
|
|
|
subMonths(new Date().setHours(0, 0, 0, 0), 3) |
|
|
|
]); |
|
|
|
break; |
|
|
|
|
|
|
|
case '1y': |
|
|
|
portfolioStart = max([ |
|
|
|
portfolioStart, |
|
|
@ -1639,60 +1752,68 @@ export class PortfolioService { |
|
|
|
userId |
|
|
|
}); |
|
|
|
|
|
|
|
const activities = await this.orderService.getOrders({ |
|
|
|
const ordersRaw = await this.orderService.getOrders({ |
|
|
|
userCurrency, |
|
|
|
userId |
|
|
|
}); |
|
|
|
|
|
|
|
const excludedActivities = ( |
|
|
|
await this.orderService.getOrders({ |
|
|
|
userCurrency, |
|
|
|
userId, |
|
|
|
withExcludedAccounts: true |
|
|
|
}) |
|
|
|
).filter(({ Account: account }) => { |
|
|
|
return account?.isExcluded ?? false; |
|
|
|
userId, |
|
|
|
withExcludedAccounts: true |
|
|
|
}); |
|
|
|
const activities: Activity[] = []; |
|
|
|
const excludedActivities: Activity[] = []; |
|
|
|
let dividend = 0; |
|
|
|
let fees = 0; |
|
|
|
let items = 0; |
|
|
|
let interest = 0; |
|
|
|
|
|
|
|
let liabilities = 0; |
|
|
|
|
|
|
|
let totalBuy = 0; |
|
|
|
let totalSell = 0; |
|
|
|
for (let order of ordersRaw) { |
|
|
|
if (order.Account?.isExcluded ?? false) { |
|
|
|
excludedActivities.push(order); |
|
|
|
} else { |
|
|
|
activities.push(order); |
|
|
|
fees += this.exchangeRateDataService.toCurrency( |
|
|
|
order.fee, |
|
|
|
order.SymbolProfile.currency, |
|
|
|
userCurrency |
|
|
|
); |
|
|
|
let amount = this.exchangeRateDataService.toCurrency( |
|
|
|
new Big(order.quantity).mul(order.unitPrice).toNumber(), |
|
|
|
order.SymbolProfile.currency, |
|
|
|
userCurrency |
|
|
|
); |
|
|
|
switch (order.type) { |
|
|
|
case 'DIVIDEND': |
|
|
|
dividend += amount; |
|
|
|
break; |
|
|
|
case 'ITEM': |
|
|
|
items += amount; |
|
|
|
break; |
|
|
|
case 'SELL': |
|
|
|
totalSell += amount; |
|
|
|
break; |
|
|
|
case 'BUY': |
|
|
|
totalBuy += amount; |
|
|
|
break; |
|
|
|
case 'LIABILITY': |
|
|
|
liabilities += amount; |
|
|
|
break; |
|
|
|
case 'INTEREST': |
|
|
|
interest += amount; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const dividend = this.getSumOfActivityType({ |
|
|
|
activities, |
|
|
|
userCurrency, |
|
|
|
activityType: 'DIVIDEND' |
|
|
|
}).toNumber(); |
|
|
|
const emergencyFund = new Big( |
|
|
|
Math.max( |
|
|
|
emergencyFundPositionsValueInBaseCurrency, |
|
|
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 |
|
|
|
) |
|
|
|
); |
|
|
|
const fees = this.getFees({ activities, userCurrency }).toNumber(); |
|
|
|
const firstOrderDate = activities[0]?.date; |
|
|
|
const interest = this.getSumOfActivityType({ |
|
|
|
activities, |
|
|
|
userCurrency, |
|
|
|
activityType: 'INTEREST' |
|
|
|
}).toNumber(); |
|
|
|
const items = this.getSumOfActivityType({ |
|
|
|
activities, |
|
|
|
userCurrency, |
|
|
|
activityType: 'ITEM' |
|
|
|
}).toNumber(); |
|
|
|
const liabilities = this.getSumOfActivityType({ |
|
|
|
activities, |
|
|
|
userCurrency, |
|
|
|
activityType: 'LIABILITY' |
|
|
|
}).toNumber(); |
|
|
|
|
|
|
|
const totalBuy = this.getSumOfActivityType({ |
|
|
|
activities, |
|
|
|
userCurrency, |
|
|
|
activityType: 'BUY' |
|
|
|
}).toNumber(); |
|
|
|
const totalSell = this.getSumOfActivityType({ |
|
|
|
activities, |
|
|
|
userCurrency, |
|
|
|
activityType: 'SELL' |
|
|
|
}).toNumber(); |
|
|
|
const firstOrderDate = activities[0]?.date; |
|
|
|
|
|
|
|
const cash = new Big(balanceInBaseCurrency) |
|
|
|
.minus(emergencyFund) |
|
|
@ -1836,7 +1957,7 @@ export class PortfolioService { |
|
|
|
userCurrency, |
|
|
|
userId, |
|
|
|
withExcludedAccounts, |
|
|
|
types: ['BUY', 'SELL'] |
|
|
|
types: ['BUY', 'SELL', 'STAKE'] |
|
|
|
}); |
|
|
|
|
|
|
|
if (orders.length <= 0) { |
|
|
|