Browse Source

Improve allocations by account

pull/308/head
Thomas 4 years ago
parent
commit
66b23f0129
  1. 20
      apps/api/src/app/portfolio/portfolio.controller.ts
  2. 16
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 77
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  4. 15
      apps/client/src/app/services/data.service.ts
  5. 2
      libs/common/src/lib/interfaces/index.ts
  6. 8
      libs/common/src/lib/interfaces/portfolio-details.interface.ts

20
apps/api/src/app/portfolio/portfolio.controller.ts

@ -5,8 +5,8 @@ import {
} from '@ghostfolio/api/helper/object.helper'; } from '@ghostfolio/api/helper/object.helper';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { import {
PortfolioDetails,
PortfolioPerformance, PortfolioPerformance,
PortfolioPosition,
PortfolioReport, PortfolioReport,
PortfolioSummary PortfolioSummary
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -124,13 +124,11 @@ export class PortfolioController {
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId,
@Query('range') range, @Query('range') range,
@Res() res: Response @Res() res: Response
): Promise<{ [symbol: string]: PortfolioPosition }> { ): Promise<PortfolioDetails> {
const { details, hasErrors } = await this.portfolioService.getDetails( const { accounts, holdings, hasErrors } =
impersonationId, await this.portfolioService.getDetails(impersonationId, range);
range
);
if (hasErrors || hasNotDefinedValuesInObject(details)) { if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
res.status(StatusCodes.ACCEPTED); res.status(StatusCodes.ACCEPTED);
} }
@ -138,13 +136,13 @@ export class PortfolioController {
impersonationId || impersonationId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(details) const totalInvestment = Object.values(holdings)
.map((portfolioPosition) => { .map((portfolioPosition) => {
return portfolioPosition.investment; return portfolioPosition.investment;
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
const totalValue = Object.values(details) const totalValue = Object.values(holdings)
.map((portfolioPosition) => { .map((portfolioPosition) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice, portfolioPosition.quantity * portfolioPosition.marketPrice,
@ -154,7 +152,7 @@ export class PortfolioController {
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(details)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPosition.grossPerformance = null; portfolioPosition.grossPerformance = null;
portfolioPosition.investment = portfolioPosition.investment =
portfolioPosition.investment / totalInvestment; portfolioPosition.investment / totalInvestment;
@ -171,7 +169,7 @@ export class PortfolioController {
} }
} }
return <any>res.json(details); return <any>res.json({ accounts, holdings });
} }
@Get('performance') @Get('performance')

16
apps/api/src/app/portfolio/portfolio.service.ts

@ -24,6 +24,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config'; import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { import {
PortfolioDetails,
PortfolioPerformance, PortfolioPerformance,
PortfolioPosition, PortfolioPosition,
PortfolioReport, PortfolioReport,
@ -154,10 +155,7 @@ export class PortfolioService {
public async getDetails( public async getDetails(
aImpersonationId: string, aImpersonationId: string,
aDateRange: DateRange = 'max' aDateRange: DateRange = 'max'
): Promise<{ ): Promise<PortfolioDetails & { hasErrors: boolean }> {
details: { [symbol: string]: PortfolioPosition };
hasErrors: boolean;
}> {
const userId = await this.getUserId(aImpersonationId); const userId = await this.getUserId(aImpersonationId);
const userCurrency = this.request.user.Settings.currency; const userCurrency = this.request.user.Settings.currency;
@ -171,7 +169,7 @@ export class PortfolioService {
}); });
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
return { details: {}, hasErrors: false }; return { accounts: {}, holdings: {}, hasErrors: false };
} }
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
@ -187,7 +185,7 @@ export class PortfolioService {
userCurrency userCurrency
); );
const details: { [symbol: string]: PortfolioPosition } = {}; const holdings: PortfolioDetails['holdings'] = {};
const totalInvestment = currentPositions.totalInvestment.plus( const totalInvestment = currentPositions.totalInvestment.plus(
cashDetails.balance cashDetails.balance
); );
@ -217,7 +215,7 @@ export class PortfolioService {
const value = item.quantity.mul(item.marketPrice); const value = item.quantity.mul(item.marketPrice);
const symbolProfile = symbolProfileMap[item.symbol]; const symbolProfile = symbolProfileMap[item.symbol];
const dataProviderResponse = dataProviderResponses[item.symbol]; const dataProviderResponse = dataProviderResponses[item.symbol];
details[item.symbol] = { holdings[item.symbol] = {
accounts, accounts,
allocationCurrent: value.div(totalValue).toNumber(), allocationCurrent: value.div(totalValue).toNumber(),
allocationInvestment: item.investment.div(totalInvestment).toNumber(), allocationInvestment: item.investment.div(totalInvestment).toNumber(),
@ -241,13 +239,13 @@ export class PortfolioService {
} }
// TODO: Add a cash position for each currency // TODO: Add a cash position for each currency
details[ghostfolioCashSymbol] = await this.getCashPosition({ holdings[ghostfolioCashSymbol] = await this.getCashPosition({
cashDetails, cashDetails,
investment: totalInvestment, investment: totalInvestment,
value: totalValue value: totalValue
}); });
return { details, hasErrors: currentPositions.hasErrors }; return { accounts, holdings, hasErrors: currentPositions.hasErrors };
} }
public async getPosition( public async getPosition(

77
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -4,7 +4,11 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; import {
PortfolioDetails,
PortfolioPosition,
User
} from '@ghostfolio/common/interfaces';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -31,7 +35,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
{ label: 'Initial', value: 'original' }, { label: 'Initial', value: 'original' },
{ label: 'Current', value: 'current' } { label: 'Current', value: 'current' }
]; ];
public portfolioPositions: { [symbol: string]: PortfolioPosition }; public portfolioDetails: PortfolioDetails;
public positions: { [symbol: string]: any }; public positions: { [symbol: string]: any };
public positionsArray: PortfolioPosition[]; public positionsArray: PortfolioPosition[];
public sectors: { public sectors: {
@ -66,11 +70,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}); });
this.dataService this.dataService
.fetchPortfolioPositions({}) .fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response = {}) => { .subscribe((portfolioDetails) => {
this.portfolioPositions = response; this.portfolioDetails = portfolioDetails;
this.initializeAnalysisData(this.portfolioPositions, this.period);
this.initializeAnalysisData(this.period);
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -86,12 +91,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}); });
} }
public initializeAnalysisData( public initializeAnalysisData(aPeriod: string) {
aPortfolioPositions: {
[symbol: string]: PortfolioPosition;
},
aPeriod: string
) {
this.accounts = {}; this.accounts = {};
this.continents = { this.continents = {
[UNKNOWN_KEY]: { [UNKNOWN_KEY]: {
@ -114,7 +114,18 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
} }
}; };
for (const [symbol, position] of Object.entries(aPortfolioPositions)) { for (const [name, { current, original }] of Object.entries(
this.portfolioDetails.accounts
)) {
this.accounts[name] = {
name,
value: aPeriod === 'original' ? original : current
};
}
for (const [symbol, position] of Object.entries(
this.portfolioDetails.holdings
)) {
this.positions[symbol] = { this.positions[symbol] = {
assetClass: position.assetClass, assetClass: position.assetClass,
currency: position.currency, currency: position.currency,
@ -126,20 +137,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}; };
this.positionsArray.push(position); this.positionsArray.push(position);
for (const [account, { current, original }] of Object.entries(
position.accounts
)) {
if (this.accounts[account]?.value) {
this.accounts[account].value +=
aPeriod === 'original' ? original : current;
} else {
this.accounts[account] = {
name: account,
value: aPeriod === 'original' ? original : current
};
}
}
if (position.countries.length > 0) { if (position.countries.length > 0) {
for (const country of position.countries) { for (const country of position.countries) {
const { code, continent, name, weight } = country; const { code, continent, name, weight } = country;
@ -152,8 +149,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: value:
weight * weight *
(aPeriod === 'original' (aPeriod === 'original'
? this.portfolioPositions[symbol].investment ? this.portfolioDetails.holdings[symbol].investment
: this.portfolioPositions[symbol].value) : this.portfolioDetails.holdings[symbol].value)
}; };
} }
@ -165,21 +162,21 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: value:
weight * weight *
(aPeriod === 'original' (aPeriod === 'original'
? this.portfolioPositions[symbol].investment ? this.portfolioDetails.holdings[symbol].investment
: this.portfolioPositions[symbol].value) : this.portfolioDetails.holdings[symbol].value)
}; };
} }
} }
} else { } else {
this.continents[UNKNOWN_KEY].value += this.continents[UNKNOWN_KEY].value +=
aPeriod === 'original' aPeriod === 'original'
? this.portfolioPositions[symbol].investment ? this.portfolioDetails.holdings[symbol].investment
: this.portfolioPositions[symbol].value; : this.portfolioDetails.holdings[symbol].value;
this.countries[UNKNOWN_KEY].value += this.countries[UNKNOWN_KEY].value +=
aPeriod === 'original' aPeriod === 'original'
? this.portfolioPositions[symbol].investment ? this.portfolioDetails.holdings[symbol].investment
: this.portfolioPositions[symbol].value; : this.portfolioDetails.holdings[symbol].value;
} }
if (position.sectors.length > 0) { if (position.sectors.length > 0) {
@ -194,16 +191,16 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: value:
weight * weight *
(aPeriod === 'original' (aPeriod === 'original'
? this.portfolioPositions[symbol].investment ? this.portfolioDetails.holdings[symbol].investment
: this.portfolioPositions[symbol].value) : this.portfolioDetails.holdings[symbol].value)
}; };
} }
} }
} else { } else {
this.sectors[UNKNOWN_KEY].value += this.sectors[UNKNOWN_KEY].value +=
aPeriod === 'original' aPeriod === 'original'
? this.portfolioPositions[symbol].investment ? this.portfolioDetails.holdings[symbol].investment
: this.portfolioPositions[symbol].value; : this.portfolioDetails.holdings[symbol].value;
} }
} }
} }
@ -211,7 +208,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
public onChangePeriod(aValue: string) { public onChangePeriod(aValue: string) {
this.period = aValue; this.period = aValue;
this.initializeAnalysisData(this.portfolioPositions, this.period); this.initializeAnalysisData(this.period);
} }
public ngOnDestroy() { public ngOnDestroy() {

15
apps/client/src/app/services/data.service.ts

@ -20,8 +20,8 @@ import {
AdminData, AdminData,
Export, Export,
InfoItem, InfoItem,
PortfolioDetails,
PortfolioPerformance, PortfolioPerformance,
PortfolioPosition,
PortfolioReport, PortfolioReport,
PortfolioSummary, PortfolioSummary,
User User
@ -148,17 +148,16 @@ export class DataService {
return this.http.get<InvestmentItem[]>('/api/portfolio/investments'); return this.http.get<InvestmentItem[]>('/api/portfolio/investments');
} }
public fetchPortfolioPerformance(aParams: { [param: string]: any }) { public fetchPortfolioDetails(aParams: { [param: string]: any }) {
return this.http.get<PortfolioPerformance>('/api/portfolio/performance', { return this.http.get<PortfolioDetails>('/api/portfolio/details', {
params: aParams params: aParams
}); });
} }
public fetchPortfolioPositions(aParams: { [param: string]: any }) { public fetchPortfolioPerformance(aParams: { [param: string]: any }) {
return this.http.get<{ [symbol: string]: PortfolioPosition }>( return this.http.get<PortfolioPerformance>('/api/portfolio/performance', {
'/api/portfolio/details', params: aParams
{ params: aParams } });
);
} }
public fetchPortfolioReport() { public fetchPortfolioReport() {

2
libs/common/src/lib/interfaces/index.ts

@ -2,6 +2,7 @@ import { Access } from './access.interface';
import { AdminData } from './admin-data.interface'; import { AdminData } from './admin-data.interface';
import { Export } from './export.interface'; import { Export } from './export.interface';
import { InfoItem } from './info-item.interface'; import { InfoItem } from './info-item.interface';
import { PortfolioDetails } from './portfolio-details.interface';
import { PortfolioItem } from './portfolio-item.interface'; import { PortfolioItem } from './portfolio-item.interface';
import { PortfolioOverview } from './portfolio-overview.interface'; import { PortfolioOverview } from './portfolio-overview.interface';
import { PortfolioPerformance } from './portfolio-performance.interface'; import { PortfolioPerformance } from './portfolio-performance.interface';
@ -20,6 +21,7 @@ export {
AdminData, AdminData,
Export, Export,
InfoItem, InfoItem,
PortfolioDetails,
PortfolioItem, PortfolioItem,
PortfolioOverview, PortfolioOverview,
PortfolioPerformance, PortfolioPerformance,

8
libs/common/src/lib/interfaces/portfolio-details.interface.ts

@ -0,0 +1,8 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export interface PortfolioDetails {
accounts: {
[name: string]: { current: number; original: number };
};
holdings: { [symbol: string]: PortfolioPosition };
}
Loading…
Cancel
Save