|
|
@ -524,11 +524,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; |
|
|
@ -544,21 +542,7 @@ export class PortfolioService { |
|
|
|
otherMarkets: 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(); |
|
|
|
} |
|
|
|
} |
|
|
|
this.calculateMarketsAllocation(symbolProfile, markets); |
|
|
|
|
|
|
|
holdings[item.symbol] = { |
|
|
|
markets, |
|
|
@ -589,6 +573,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({ |
|
|
|
activities: orders |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
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'; |
|
|
|
}); |
|
|
@ -604,16 +650,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 && |
|
|
@ -648,30 +704,32 @@ export class PortfolioService { |
|
|
|
valueInBaseCurrency: emergencyFundInCash |
|
|
|
}; |
|
|
|
} |
|
|
|
return filteredValueInBaseCurrency; |
|
|
|
} |
|
|
|
|
|
|
|
const summary = await this.getSummary({ |
|
|
|
impersonationId, |
|
|
|
userCurrency, |
|
|
|
userId, |
|
|
|
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, |
|
|
|
emergencyFundPositionsValueInBaseCurrency: |
|
|
|
this.getEmergencyFundPositionsValueInBaseCurrency({ |
|
|
|
activities: orders |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
return { |
|
|
|
accounts, |
|
|
|
holdings, |
|
|
|
platforms, |
|
|
|
summary, |
|
|
|
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), |
|
|
|
filteredValueInPercentage: summary.netWorth |
|
|
|
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() |
|
|
|
: 0, |
|
|
|
hasErrors: currentPositions.hasErrors, |
|
|
|
totalValueInBaseCurrency: summary.netWorth |
|
|
|
}; |
|
|
|
private calculateMarketsAllocation( |
|
|
|
symbolProfile: EnhancedSymbolProfile, |
|
|
|
markets: { |
|
|
|
developedMarkets: number; |
|
|
|
emergingMarkets: number; |
|
|
|
otherMarkets: number; |
|
|
|
} |
|
|
|
) { |
|
|
|
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(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public async getPosition( |
|
|
@ -704,6 +762,7 @@ export class PortfolioService { |
|
|
|
averagePrice: undefined, |
|
|
|
dataProviderInfo: undefined, |
|
|
|
dividendInBaseCurrency: undefined, |
|
|
|
stakeRewards: undefined, |
|
|
|
feeInBaseCurrency: undefined, |
|
|
|
firstBuyDate: undefined, |
|
|
|
grossPerformance: undefined, |
|
|
@ -732,7 +791,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, |
|
|
@ -788,6 +851,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(), |
|
|
@ -882,6 +955,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, |
|
|
@ -945,6 +1019,7 @@ export class PortfolioService { |
|
|
|
averagePrice: 0, |
|
|
|
dataProviderInfo: undefined, |
|
|
|
dividendInBaseCurrency: 0, |
|
|
|
stakeRewards: 0, |
|
|
|
feeInBaseCurrency: 0, |
|
|
|
firstBuyDate: undefined, |
|
|
|
grossPerformance: undefined, |
|
|
@ -1407,7 +1482,7 @@ export class PortfolioService { |
|
|
|
let valueInBaseCurrencyOfEmergencyFundPositions = new Big(0); |
|
|
|
|
|
|
|
for (const order of emergencyFundOrders) { |
|
|
|
if (order.type === 'BUY') { |
|
|
|
if (order.type === 'BUY' || order.type === 'STAKE') { |
|
|
|
valueInBaseCurrencyOfEmergencyFundPositions = |
|
|
|
valueInBaseCurrencyOfEmergencyFundPositions.plus( |
|
|
|
order.valueInBaseCurrency |
|
|
@ -1594,38 +1669,63 @@ 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 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; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const dividend = this.getDividend({ |
|
|
|
activities, |
|
|
|
userCurrency |
|
|
|
}).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 items = this.getItems(activities).toNumber(); |
|
|
|
const liabilities = this.getLiabilities(activities).toNumber(); |
|
|
|
|
|
|
|
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY'); |
|
|
|
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL'); |
|
|
|
const firstOrderDate = activities[0]?.date; |
|
|
|
|
|
|
|
const cash = new Big(balanceInBaseCurrency) |
|
|
|
.minus(emergencyFund) |
|
|
@ -1718,7 +1818,7 @@ export class PortfolioService { |
|
|
|
userCurrency, |
|
|
|
userId, |
|
|
|
withExcludedAccounts, |
|
|
|
types: ['BUY', 'SELL'] |
|
|
|
types: ['BUY', 'SELL', 'STAKE'] |
|
|
|
}); |
|
|
|
|
|
|
|
if (orders.length <= 0) { |
|
|
|