|
|
@ -45,6 +45,7 @@ import type { |
|
|
|
AccountWithValue, |
|
|
|
DateRange, |
|
|
|
GroupBy, |
|
|
|
Market, |
|
|
|
RequestWithUser, |
|
|
|
UserWithSettings |
|
|
|
} from '@ghostfolio/common/types'; |
|
|
@ -581,6 +582,17 @@ export class PortfolioService { |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
let markets: { |
|
|
|
[key in Market]: { |
|
|
|
name: string; |
|
|
|
value: number; |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
if (withMarkets) { |
|
|
|
markets = this.getAggregatedMarkets(holdings); |
|
|
|
} |
|
|
|
|
|
|
|
let summary: PortfolioSummary; |
|
|
|
|
|
|
|
if (withSummary) { |
|
|
@ -602,6 +614,7 @@ export class PortfolioService { |
|
|
|
accounts, |
|
|
|
hasErrors, |
|
|
|
holdings, |
|
|
|
markets, |
|
|
|
platforms, |
|
|
|
summary |
|
|
|
}; |
|
|
@ -1148,74 +1161,49 @@ export class PortfolioService { |
|
|
|
|
|
|
|
public async getReport(impersonationId: string): Promise<PortfolioReport> { |
|
|
|
const userId = await this.getUserId(impersonationId, this.request.user.id); |
|
|
|
const user = await this.userService.user({ id: userId }); |
|
|
|
const userCurrency = this.getUserCurrency(user); |
|
|
|
|
|
|
|
const { activities } = |
|
|
|
await this.orderService.getOrdersForPortfolioCalculator({ |
|
|
|
userCurrency, |
|
|
|
userId |
|
|
|
}); |
|
|
|
const userSettings = <UserSettings>this.request.user.Settings.settings; |
|
|
|
|
|
|
|
const portfolioCalculator = this.calculatorFactory.createCalculator({ |
|
|
|
activities, |
|
|
|
const { accounts, holdings, summary } = await this.getDetails({ |
|
|
|
impersonationId, |
|
|
|
userId, |
|
|
|
calculationType: PerformanceCalculationType.TWR, |
|
|
|
currency: this.request.user.Settings.settings.baseCurrency |
|
|
|
}); |
|
|
|
|
|
|
|
let { totalFeesWithCurrencyEffect, positions, totalInvestment } = |
|
|
|
await portfolioCalculator.getSnapshot(); |
|
|
|
|
|
|
|
positions = positions.filter((item) => !item.quantity.eq(0)); |
|
|
|
|
|
|
|
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; |
|
|
|
|
|
|
|
for (const position of positions) { |
|
|
|
portfolioItemsNow[position.symbol] = position; |
|
|
|
} |
|
|
|
|
|
|
|
const { accounts } = await this.getValueOfAccountsAndPlatforms({ |
|
|
|
activities, |
|
|
|
portfolioItemsNow, |
|
|
|
userCurrency, |
|
|
|
userId |
|
|
|
withMarkets: true, |
|
|
|
withSummary: true |
|
|
|
}); |
|
|
|
|
|
|
|
const userSettings = <UserSettings>this.request.user.Settings.settings; |
|
|
|
|
|
|
|
return { |
|
|
|
rules: { |
|
|
|
accountClusterRisk: isEmpty(activities) |
|
|
|
? undefined |
|
|
|
: await this.rulesService.evaluate( |
|
|
|
[ |
|
|
|
new AccountClusterRiskCurrentInvestment( |
|
|
|
this.exchangeRateDataService, |
|
|
|
accounts |
|
|
|
), |
|
|
|
new AccountClusterRiskSingleAccount( |
|
|
|
this.exchangeRateDataService, |
|
|
|
accounts |
|
|
|
) |
|
|
|
], |
|
|
|
userSettings |
|
|
|
), |
|
|
|
currencyClusterRisk: isEmpty(activities) |
|
|
|
? undefined |
|
|
|
: await this.rulesService.evaluate( |
|
|
|
[ |
|
|
|
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( |
|
|
|
this.exchangeRateDataService, |
|
|
|
positions |
|
|
|
), |
|
|
|
new CurrencyClusterRiskCurrentInvestment( |
|
|
|
this.exchangeRateDataService, |
|
|
|
positions |
|
|
|
) |
|
|
|
], |
|
|
|
userSettings |
|
|
|
), |
|
|
|
accountClusterRisk: |
|
|
|
summary.ordersCount > 0 |
|
|
|
? await this.rulesService.evaluate( |
|
|
|
[ |
|
|
|
new AccountClusterRiskCurrentInvestment( |
|
|
|
this.exchangeRateDataService, |
|
|
|
accounts |
|
|
|
), |
|
|
|
new AccountClusterRiskSingleAccount( |
|
|
|
this.exchangeRateDataService, |
|
|
|
accounts |
|
|
|
) |
|
|
|
], |
|
|
|
userSettings |
|
|
|
) |
|
|
|
: undefined, |
|
|
|
currencyClusterRisk: |
|
|
|
summary.ordersCount > 0 |
|
|
|
? await this.rulesService.evaluate( |
|
|
|
[ |
|
|
|
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( |
|
|
|
this.exchangeRateDataService, |
|
|
|
Object.values(holdings) |
|
|
|
), |
|
|
|
new CurrencyClusterRiskCurrentInvestment( |
|
|
|
this.exchangeRateDataService, |
|
|
|
Object.values(holdings) |
|
|
|
) |
|
|
|
], |
|
|
|
userSettings |
|
|
|
) |
|
|
|
: undefined, |
|
|
|
emergencyFund: await this.rulesService.evaluate( |
|
|
|
[ |
|
|
|
new EmergencyFundSetup( |
|
|
@ -1229,8 +1217,8 @@ export class PortfolioService { |
|
|
|
[ |
|
|
|
new FeeRatioInitialInvestment( |
|
|
|
this.exchangeRateDataService, |
|
|
|
totalInvestment.toNumber(), |
|
|
|
totalFeesWithCurrencyEffect.toNumber() |
|
|
|
summary.committedFunds, |
|
|
|
summary.fees |
|
|
|
) |
|
|
|
], |
|
|
|
userSettings |
|
|
@ -1257,6 +1245,62 @@ export class PortfolioService { |
|
|
|
await this.orderService.assignTags({ dataSource, symbol, tags, userId }); |
|
|
|
} |
|
|
|
|
|
|
|
private getAggregatedMarkets(holdings: { |
|
|
|
[symbol: string]: PortfolioPosition; |
|
|
|
}): { |
|
|
|
[key in Market]: { name: string; value: number }; |
|
|
|
} { |
|
|
|
const markets = { |
|
|
|
[UNKNOWN_KEY]: { |
|
|
|
name: UNKNOWN_KEY, |
|
|
|
value: 0 |
|
|
|
}, |
|
|
|
developedMarkets: { |
|
|
|
name: 'developedMarkets', |
|
|
|
value: 0 |
|
|
|
}, |
|
|
|
emergingMarkets: { |
|
|
|
name: 'emergingMarkets', |
|
|
|
value: 0 |
|
|
|
}, |
|
|
|
otherMarkets: { |
|
|
|
name: 'otherMarkets', |
|
|
|
value: 0 |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
for (const [symbol, position] of Object.entries(holdings)) { |
|
|
|
const value = position.valueInBaseCurrency; |
|
|
|
|
|
|
|
if (position.assetClass !== AssetClass.LIQUIDITY) { |
|
|
|
if (position.countries.length > 0) { |
|
|
|
markets.developedMarkets.value += |
|
|
|
position.markets.developedMarkets * value; |
|
|
|
markets.emergingMarkets.value += |
|
|
|
position.markets.emergingMarkets * value; |
|
|
|
markets.otherMarkets.value += position.markets.otherMarkets * value; |
|
|
|
} else { |
|
|
|
markets[UNKNOWN_KEY].value += value; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const marketsTotal = |
|
|
|
markets.developedMarkets.value + |
|
|
|
markets.emergingMarkets.value + |
|
|
|
markets.otherMarkets.value + |
|
|
|
markets[UNKNOWN_KEY].value; |
|
|
|
|
|
|
|
markets.developedMarkets.value = |
|
|
|
markets.developedMarkets.value / marketsTotal; |
|
|
|
markets.emergingMarkets.value = |
|
|
|
markets.emergingMarkets.value / marketsTotal; |
|
|
|
markets.otherMarkets.value = markets.otherMarkets.value / marketsTotal; |
|
|
|
markets[UNKNOWN_KEY].value = markets[UNKNOWN_KEY].value / marketsTotal; |
|
|
|
|
|
|
|
return markets; |
|
|
|
} |
|
|
|
|
|
|
|
private async getCashPositions({ |
|
|
|
cashDetails, |
|
|
|
userCurrency, |
|
|
|