Optimize details endpoint (#3123)
* Make summary optional * Introduce dedicated holdings endpoint * Update changelog
This commit is contained in:
parent
6d2a897366
commit
eb75be8535
@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Optimized the calculation of the accounts table
|
||||||
|
- Optimized the calculation of the portfolio holdings
|
||||||
- Integrated dividend into the transaction point concept in the portfolio service
|
- Integrated dividend into the transaction point concept in the portfolio service
|
||||||
- Removed the environment variable `WEB_AUTH_RP_ID`
|
- Removed the environment variable `WEB_AUTH_RP_ID`
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioDividends,
|
PortfolioDividends,
|
||||||
|
PortfolioHoldingsResponse,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformanceResponse,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
@ -95,21 +96,15 @@ export class PortfolioController {
|
|||||||
filterByTags
|
filterByTags
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { accounts, hasErrors, holdings, platforms, summary } =
|
||||||
accounts,
|
await this.portfolioService.getDetails({
|
||||||
filteredValueInBaseCurrency,
|
dateRange,
|
||||||
filteredValueInPercentage,
|
filters,
|
||||||
hasErrors,
|
impersonationId,
|
||||||
holdings,
|
userId: this.request.user.id,
|
||||||
platforms,
|
withLiabilities: true,
|
||||||
summary,
|
withSummary: true
|
||||||
totalValueInBaseCurrency
|
});
|
||||||
} = await this.portfolioService.getDetails({
|
|
||||||
dateRange,
|
|
||||||
filters,
|
|
||||||
impersonationId,
|
|
||||||
userId: this.request.user.id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||||
hasError = true;
|
hasError = true;
|
||||||
@ -164,19 +159,21 @@ export class PortfolioController {
|
|||||||
'currentGrossPerformanceWithCurrencyEffect',
|
'currentGrossPerformanceWithCurrencyEffect',
|
||||||
'currentNetPerformance',
|
'currentNetPerformance',
|
||||||
'currentNetPerformanceWithCurrencyEffect',
|
'currentNetPerformanceWithCurrencyEffect',
|
||||||
|
'currentNetWorth',
|
||||||
'currentValue',
|
'currentValue',
|
||||||
'dividendInBaseCurrency',
|
'dividendInBaseCurrency',
|
||||||
'emergencyFund',
|
'emergencyFund',
|
||||||
'excludedAccountsAndActivities',
|
'excludedAccountsAndActivities',
|
||||||
'fees',
|
'fees',
|
||||||
|
'filteredValueInBaseCurrency',
|
||||||
'fireWealth',
|
'fireWealth',
|
||||||
'interest',
|
'interest',
|
||||||
'items',
|
'items',
|
||||||
'liabilities',
|
'liabilities',
|
||||||
'netWorth',
|
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
'totalInvestment',
|
'totalInvestment',
|
||||||
'totalSell'
|
'totalSell',
|
||||||
|
'totalValueInBaseCurrency'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,12 +200,9 @@ export class PortfolioController {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
filteredValueInBaseCurrency,
|
|
||||||
filteredValueInPercentage,
|
|
||||||
hasError,
|
hasError,
|
||||||
holdings,
|
holdings,
|
||||||
platforms,
|
platforms,
|
||||||
totalValueInBaseCurrency,
|
|
||||||
summary: portfolioSummary
|
summary: portfolioSummary
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -279,6 +273,33 @@ export class PortfolioController {
|
|||||||
return { dividends };
|
return { dividends };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('holdings')
|
||||||
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
|
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
public async getHoldings(
|
||||||
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
||||||
|
@Query('accounts') filterByAccounts?: string,
|
||||||
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
|
@Query('query') filterBySearchQuery?: string,
|
||||||
|
@Query('tags') filterByTags?: string
|
||||||
|
): Promise<PortfolioHoldingsResponse> {
|
||||||
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
|
filterByAccounts,
|
||||||
|
filterByAssetClasses,
|
||||||
|
filterBySearchQuery,
|
||||||
|
filterByTags
|
||||||
|
});
|
||||||
|
|
||||||
|
const { holdings } = await this.portfolioService.getDetails({
|
||||||
|
filters,
|
||||||
|
impersonationId,
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return { holdings: Object.values(holdings) };
|
||||||
|
}
|
||||||
|
|
||||||
@Get('investments')
|
@Get('investments')
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
public async getInvestments(
|
public async getInvestments(
|
||||||
@ -502,7 +523,6 @@ export class PortfolioController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { holdings } = await this.portfolioService.getDetails({
|
const { holdings } = await this.portfolioService.getDetails({
|
||||||
dateRange: 'max',
|
|
||||||
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
||||||
impersonationId: access.userId,
|
impersonationId: access.userId,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
|
@ -24,7 +24,12 @@ import {
|
|||||||
MAX_CHART_ITEMS,
|
MAX_CHART_ITEMS,
|
||||||
UNKNOWN_KEY
|
UNKNOWN_KEY
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
|
getAllActivityTypes,
|
||||||
|
getSum,
|
||||||
|
parseDate
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
EnhancedSymbolProfile,
|
EnhancedSymbolProfile,
|
||||||
@ -141,7 +146,8 @@ export class PortfolioService {
|
|||||||
filters,
|
filters,
|
||||||
withExcludedAccounts,
|
withExcludedAccounts,
|
||||||
impersonationId: userId,
|
impersonationId: userId,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id,
|
||||||
|
withLiabilities: true
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -332,13 +338,17 @@ export class PortfolioService {
|
|||||||
filters,
|
filters,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false
|
withExcludedAccounts = false,
|
||||||
|
withLiabilities = false,
|
||||||
|
withSummary = false
|
||||||
}: {
|
}: {
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
withExcludedAccounts?: boolean;
|
withExcludedAccounts?: boolean;
|
||||||
|
withLiabilities?: boolean;
|
||||||
|
withSummary?: boolean;
|
||||||
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
@ -352,7 +362,12 @@ export class PortfolioService {
|
|||||||
await this.getTransactionPoints({
|
await this.getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts
|
withExcludedAccounts,
|
||||||
|
types: withLiabilities
|
||||||
|
? undefined
|
||||||
|
: getAllActivityTypes().filter((activityType) => {
|
||||||
|
return activityType !== 'LIABILITY';
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
@ -625,29 +640,29 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = await this.getSummary({
|
let summary: PortfolioSummary;
|
||||||
holdings,
|
|
||||||
impersonationId,
|
if (withSummary) {
|
||||||
userCurrency,
|
summary = await this.getSummary({
|
||||||
userId,
|
filteredValueInBaseCurrency,
|
||||||
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
holdings,
|
||||||
emergencyFundPositionsValueInBaseCurrency:
|
impersonationId,
|
||||||
this.getEmergencyFundPositionsValueInBaseCurrency({
|
userCurrency,
|
||||||
holdings
|
userId,
|
||||||
})
|
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
|
||||||
});
|
emergencyFundPositionsValueInBaseCurrency:
|
||||||
|
this.getEmergencyFundPositionsValueInBaseCurrency({
|
||||||
|
holdings
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
holdings,
|
holdings,
|
||||||
platforms,
|
platforms,
|
||||||
summary,
|
summary,
|
||||||
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
hasErrors: currentPositions.hasErrors
|
||||||
filteredValueInPercentage: summary.netWorth
|
|
||||||
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
|
|
||||||
: 0,
|
|
||||||
hasErrors: currentPositions.hasErrors,
|
|
||||||
totalValueInBaseCurrency: summary.netWorth
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1705,6 +1720,7 @@ export class PortfolioService {
|
|||||||
private async getSummary({
|
private async getSummary({
|
||||||
balanceInBaseCurrency,
|
balanceInBaseCurrency,
|
||||||
emergencyFundPositionsValueInBaseCurrency,
|
emergencyFundPositionsValueInBaseCurrency,
|
||||||
|
filteredValueInBaseCurrency,
|
||||||
holdings,
|
holdings,
|
||||||
impersonationId,
|
impersonationId,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
@ -1712,6 +1728,7 @@ export class PortfolioService {
|
|||||||
}: {
|
}: {
|
||||||
balanceInBaseCurrency: number;
|
balanceInBaseCurrency: number;
|
||||||
emergencyFundPositionsValueInBaseCurrency: number;
|
emergencyFundPositionsValueInBaseCurrency: number;
|
||||||
|
filteredValueInBaseCurrency: Big;
|
||||||
holdings: PortfolioDetails['holdings'];
|
holdings: PortfolioDetails['holdings'];
|
||||||
impersonationId: string;
|
impersonationId: string;
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
@ -1893,7 +1910,6 @@ export class PortfolioService {
|
|||||||
interest,
|
interest,
|
||||||
items,
|
items,
|
||||||
liabilities,
|
liabilities,
|
||||||
netWorth,
|
|
||||||
totalBuy,
|
totalBuy,
|
||||||
totalSell,
|
totalSell,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
@ -1905,12 +1921,17 @@ export class PortfolioService {
|
|||||||
.toNumber(),
|
.toNumber(),
|
||||||
total: emergencyFund.toNumber()
|
total: emergencyFund.toNumber()
|
||||||
},
|
},
|
||||||
|
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
||||||
|
filteredValueInPercentage: netWorth
|
||||||
|
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
||||||
|
: undefined,
|
||||||
fireWealth: new Big(performanceInformation.performance.currentValue)
|
fireWealth: new Big(performanceInformation.performance.currentValue)
|
||||||
.minus(emergencyFundPositionsValueInBaseCurrency)
|
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
ordersCount: activities.filter(({ type }) => {
|
ordersCount: activities.filter(({ type }) => {
|
||||||
return type === 'BUY' || type === 'SELL';
|
return type === 'BUY' || type === 'SELL';
|
||||||
}).length
|
}).length,
|
||||||
|
totalValueInBaseCurrency: netWorth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1943,7 +1964,7 @@ export class PortfolioService {
|
|||||||
private async getTransactionPoints({
|
private async getTransactionPoints({
|
||||||
filters,
|
filters,
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
types = ['BUY', 'DIVIDEND', 'ITEM', 'LIABILITY', 'SELL'],
|
types = getAllActivityTypes(),
|
||||||
userId,
|
userId,
|
||||||
withExcludedAccounts = false
|
withExcludedAccounts = false
|
||||||
}: {
|
}: {
|
||||||
|
@ -49,7 +49,6 @@ export class RedactValuesInResponseInterceptor<T>
|
|||||||
'dividendInBaseCurrency',
|
'dividendInBaseCurrency',
|
||||||
'fee',
|
'fee',
|
||||||
'feeInBaseCurrency',
|
'feeInBaseCurrency',
|
||||||
'filteredValueInBaseCurrency',
|
|
||||||
'grossPerformance',
|
'grossPerformance',
|
||||||
'grossPerformanceWithCurrencyEffect',
|
'grossPerformanceWithCurrencyEffect',
|
||||||
'investment',
|
'investment',
|
||||||
@ -58,7 +57,6 @@ export class RedactValuesInResponseInterceptor<T>
|
|||||||
'quantity',
|
'quantity',
|
||||||
'symbolMapping',
|
'symbolMapping',
|
||||||
'totalBalanceInBaseCurrency',
|
'totalBalanceInBaseCurrency',
|
||||||
'totalValueInBaseCurrency',
|
|
||||||
'unitPrice',
|
'unitPrice',
|
||||||
'value',
|
'value',
|
||||||
'valueInBaseCurrency'
|
'valueInBaseCurrency'
|
||||||
|
@ -115,7 +115,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioDetails({
|
.fetchPortfolioHoldings({
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
type: 'ACCOUNT',
|
type: 'ACCOUNT',
|
||||||
@ -125,11 +125,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
|||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ holdings }) => {
|
.subscribe(({ holdings }) => {
|
||||||
this.holdings = [];
|
this.holdings = holdings;
|
||||||
|
|
||||||
for (const [symbol, holding] of Object.entries(holdings)) {
|
|
||||||
this.holdings.push(holding);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -282,7 +282,7 @@
|
|||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[unit]="baseCurrency"
|
[unit]="baseCurrency"
|
||||||
[value]="isLoading ? undefined : summary?.netWorth"
|
[value]="isLoading ? undefined : summary?.totalValueInBaseCurrency"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -281,7 +281,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.platforms = {};
|
this.platforms = {};
|
||||||
this.portfolioDetails = {
|
this.portfolioDetails = {
|
||||||
accounts: {},
|
accounts: {},
|
||||||
filteredValueInPercentage: 0,
|
|
||||||
holdings: {},
|
holdings: {},
|
||||||
platforms: {},
|
platforms: {},
|
||||||
summary: undefined
|
summary: undefined
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoading
|
isLoading
|
||||||
? undefined
|
? undefined
|
||||||
: portfolioDetails?.filteredValueInPercentage
|
: portfolioDetails?.summary?.filteredValueInPercentage
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
@ -26,10 +26,11 @@
|
|||||||
<mat-progress-bar
|
<mat-progress-bar
|
||||||
mode="determinate"
|
mode="determinate"
|
||||||
[title]="
|
[title]="
|
||||||
(portfolioDetails?.filteredValueInPercentage * 100).toFixed(2) +
|
(
|
||||||
'%'
|
portfolioDetails?.summary?.filteredValueInPercentage * 100
|
||||||
|
).toFixed(2) + '%'
|
||||||
"
|
"
|
||||||
[value]="portfolioDetails?.filteredValueInPercentage * 100"
|
[value]="portfolioDetails?.summary?.filteredValueInPercentage * 100"
|
||||||
/>
|
/>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -3,11 +3,7 @@ import { PositionDetailDialog } from '@ghostfolio/client/components/position/pos
|
|||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
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 {
|
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
|
||||||
PortfolioDetails,
|
|
||||||
PortfolioPosition,
|
|
||||||
User
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
@ -28,8 +24,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public holdings: PortfolioPosition[];
|
public holdings: PortfolioPosition[];
|
||||||
public isLoading = false;
|
|
||||||
public portfolioDetails: PortfolioDetails;
|
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -83,12 +77,10 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.holdings = undefined;
|
this.holdings = undefined;
|
||||||
|
|
||||||
this.fetchPortfolioDetails()
|
this.fetchHoldings()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((portfolioDetails) => {
|
.subscribe(({ holdings }) => {
|
||||||
this.portfolioDetails = portfolioDetails;
|
this.holdings = holdings;
|
||||||
|
|
||||||
this.initialize();
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
@ -103,22 +95,12 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchPortfolioDetails() {
|
private fetchHoldings() {
|
||||||
return this.dataService.fetchPortfolioDetails({
|
return this.dataService.fetchPortfolioHoldings({
|
||||||
filters: this.userService.getFilters()
|
filters: this.userService.getFilters()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private initialize() {
|
|
||||||
this.holdings = [];
|
|
||||||
|
|
||||||
for (const [symbol, holding] of Object.entries(
|
|
||||||
this.portfolioDetails.holdings
|
|
||||||
)) {
|
|
||||||
this.holdings.push(holding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private openPositionDialog({
|
private openPositionDialog({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
OAuthResponse,
|
OAuthResponse,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioDividends,
|
PortfolioDividends,
|
||||||
|
PortfolioHoldingsResponse,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformanceResponse,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
@ -434,6 +435,46 @@ export class DataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchPortfolioHoldings({
|
||||||
|
filters
|
||||||
|
}: {
|
||||||
|
filters?: Filter[];
|
||||||
|
} = {}) {
|
||||||
|
return this.http
|
||||||
|
.get<PortfolioHoldingsResponse>('/api/v1/portfolio/holdings', {
|
||||||
|
params: this.buildFiltersAsQueryParams({ filters })
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
map((response) => {
|
||||||
|
if (response.holdings) {
|
||||||
|
for (const symbol of Object.keys(response.holdings)) {
|
||||||
|
response.holdings[symbol].assetClassLabel = translate(
|
||||||
|
response.holdings[symbol].assetClass
|
||||||
|
);
|
||||||
|
|
||||||
|
response.holdings[symbol].assetSubClassLabel = translate(
|
||||||
|
response.holdings[symbol].assetSubClass
|
||||||
|
);
|
||||||
|
|
||||||
|
response.holdings[symbol].dateOfFirstActivity = response.holdings[
|
||||||
|
symbol
|
||||||
|
].dateOfFirstActivity
|
||||||
|
? parseISO(response.holdings[symbol].dateOfFirstActivity)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
response.holdings[symbol].value = isNumber(
|
||||||
|
response.holdings[symbol].value
|
||||||
|
)
|
||||||
|
? response.holdings[symbol].value
|
||||||
|
: response.holdings[symbol].valueInPercentage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public fetchPortfolioPerformance({
|
public fetchPortfolioPerformance({
|
||||||
filters,
|
filters,
|
||||||
range,
|
range,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as currencies from '@dinero.js/currencies';
|
import * as currencies from '@dinero.js/currencies';
|
||||||
import { NumberParser } from '@internationalized/number';
|
import { NumberParser } from '@internationalized/number';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData, Type as ActivityType } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
getDate,
|
getDate,
|
||||||
@ -138,6 +138,10 @@ export function extractNumberFromString({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllActivityTypes(): ActivityType[] {
|
||||||
|
return ['BUY', 'DIVIDEND', 'FEE', 'ITEM', 'LIABILITY', 'SELL'];
|
||||||
|
}
|
||||||
|
|
||||||
export function getAssetProfileIdentifier({ dataSource, symbol }: UniqueAsset) {
|
export function getAssetProfileIdentifier({ dataSource, symbol }: UniqueAsset) {
|
||||||
return `${dataSource}-${symbol}`;
|
return `${dataSource}-${symbol}`;
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ import type { BenchmarkResponse } from './responses/benchmark-response.interface
|
|||||||
import type { ResponseError } from './responses/errors.interface';
|
import type { ResponseError } from './responses/errors.interface';
|
||||||
import type { ImportResponse } from './responses/import-response.interface';
|
import type { ImportResponse } from './responses/import-response.interface';
|
||||||
import type { OAuthResponse } from './responses/oauth-response.interface';
|
import type { OAuthResponse } from './responses/oauth-response.interface';
|
||||||
|
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
|
||||||
import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
||||||
import type { ScraperConfiguration } from './scraper-configuration.interface';
|
import type { ScraperConfiguration } from './scraper-configuration.interface';
|
||||||
import type { Statistics } from './statistics.interface';
|
import type { Statistics } from './statistics.interface';
|
||||||
@ -81,6 +82,7 @@ export {
|
|||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioDividends,
|
PortfolioDividends,
|
||||||
|
PortfolioHoldingsResponse,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
PortfolioOverview,
|
||||||
|
@ -13,8 +13,6 @@ export interface PortfolioDetails {
|
|||||||
valueInPercentage?: number;
|
valueInPercentage?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
filteredValueInBaseCurrency?: number;
|
|
||||||
filteredValueInPercentage: number;
|
|
||||||
holdings: { [symbol: string]: PortfolioPosition };
|
holdings: { [symbol: string]: PortfolioPosition };
|
||||||
platforms: {
|
platforms: {
|
||||||
[id: string]: {
|
[id: string]: {
|
||||||
@ -25,6 +23,5 @@ export interface PortfolioDetails {
|
|||||||
valueInPercentage?: number;
|
valueInPercentage?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
summary: PortfolioSummary;
|
summary?: PortfolioSummary;
|
||||||
totalValueInBaseCurrency?: number;
|
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,15 @@ export interface PortfolioSummary extends PortfolioPerformance {
|
|||||||
};
|
};
|
||||||
excludedAccountsAndActivities: number;
|
excludedAccountsAndActivities: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
|
filteredValueInBaseCurrency?: number;
|
||||||
|
filteredValueInPercentage?: number;
|
||||||
fireWealth: number;
|
fireWealth: number;
|
||||||
firstOrderDate: Date;
|
firstOrderDate: Date;
|
||||||
interest: number;
|
interest: number;
|
||||||
items: number;
|
items: number;
|
||||||
liabilities: number;
|
liabilities: number;
|
||||||
netWorth: number;
|
|
||||||
ordersCount: number;
|
ordersCount: number;
|
||||||
totalBuy: number;
|
totalBuy: number;
|
||||||
totalSell: number;
|
totalSell: number;
|
||||||
|
totalValueInBaseCurrency?: number;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
export interface PortfolioHoldingsResponse {
|
||||||
|
holdings: PortfolioPosition[];
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user