Feature/add data provider errors to api response (#738)
* Add data provider error details * Update changelog
This commit is contained in:
parent
3de7d3f60e
commit
86acbf06f4
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Included data provider errors in API response
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Improved the account calculations
|
- Improved the account calculations
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
export interface CurrentPositions {
|
export interface CurrentPositions extends ResponseError {
|
||||||
hasErrors: boolean;
|
|
||||||
positions: TimelinePosition[];
|
positions: TimelinePosition[];
|
||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
|
@ -66,6 +66,7 @@ describe('PortfolioCalculatorNew', () => {
|
|||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('0'),
|
currentValue: new Big('0'),
|
||||||
|
errors: [],
|
||||||
grossPerformance: new Big('-12.6'),
|
grossPerformance: new Big('-12.6'),
|
||||||
grossPerformancePercentage: new Big('-0.0440867739678096571'),
|
grossPerformancePercentage: new Big('-0.0440867739678096571'),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
|
@ -55,6 +55,7 @@ describe('PortfolioCalculatorNew', () => {
|
|||||||
|
|
||||||
expect(currentPositions).toEqual({
|
expect(currentPositions).toEqual({
|
||||||
currentValue: new Big('297.8'),
|
currentValue: new Big('297.8'),
|
||||||
|
errors: [],
|
||||||
grossPerformance: new Big('24.6'),
|
grossPerformance: new Big('24.6'),
|
||||||
grossPerformancePercentage: new Big('0.09004392386530014641'),
|
grossPerformancePercentage: new Big('0.09004392386530014641'),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
|
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
|
||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { TimelinePosition } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
ResponseError,
|
||||||
|
TimelinePosition,
|
||||||
|
UniqueAsset
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { Type as TypeOfOrder } from '@prisma/client';
|
import { Type as TypeOfOrder } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
@ -232,6 +236,8 @@ export class PortfolioCalculatorNew {
|
|||||||
const positions: TimelinePosition[] = [];
|
const positions: TimelinePosition[] = [];
|
||||||
let hasAnySymbolMetricsErrors = false;
|
let hasAnySymbolMetricsErrors = false;
|
||||||
|
|
||||||
|
const errors: ResponseError['errors'] = [];
|
||||||
|
|
||||||
for (const item of lastTransactionPoint.items) {
|
for (const item of lastTransactionPoint.items) {
|
||||||
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
||||||
|
|
||||||
@ -272,12 +278,17 @@ export class PortfolioCalculatorNew {
|
|||||||
symbol: item.symbol,
|
symbol: item.symbol,
|
||||||
transactionCount: item.transactionCount
|
transactionCount: item.transactionCount
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const overall = this.calculateOverallPerformance(positions, initialValues);
|
const overall = this.calculateOverallPerformance(positions, initialValues);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...overall,
|
...overall,
|
||||||
|
errors,
|
||||||
positions,
|
positions,
|
||||||
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformance,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary
|
PortfolioSummary
|
||||||
@ -204,10 +204,11 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Get('performance')
|
@Get('performance')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
@Headers('impersonation-id') impersonationId: string,
|
@Headers('impersonation-id') impersonationId: string,
|
||||||
@Query('range') range
|
@Query('range') range
|
||||||
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const performanceInformation = await this.portfolioServiceStrategy
|
const performanceInformation = await this.portfolioServiceStrategy
|
||||||
.get()
|
.get()
|
||||||
.getPerformance(impersonationId, range);
|
.getPerformance(impersonationId, range);
|
||||||
|
@ -24,7 +24,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
|||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
@ -730,7 +730,7 @@ export class PortfolioServiceNew {
|
|||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const { portfolioOrders, transactionPoints } =
|
const { portfolioOrders, transactionPoints } =
|
||||||
@ -776,6 +776,7 @@ export class PortfolioServiceNew {
|
|||||||
currentPositions.netPerformancePercentage.toNumber();
|
currentPositions.netPerformancePercentage.toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
errors: currentPositions.errors,
|
||||||
hasErrors: currentPositions.hasErrors || hasErrors,
|
hasErrors: currentPositions.hasErrors || hasErrors,
|
||||||
performance: {
|
performance: {
|
||||||
currentGrossPerformance,
|
currentGrossPerformance,
|
||||||
|
@ -25,7 +25,7 @@ import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
|||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
@ -712,7 +712,7 @@ export class PortfolioService {
|
|||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
): Promise<PortfolioPerformanceResponse> {
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
|
@ -41,6 +41,14 @@ export class TransformDataSourceInResponseInterceptor<T>
|
|||||||
data.dataSource = encodeDataSource(data.dataSource);
|
data.dataSource = encodeDataSource(data.dataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.errors) {
|
||||||
|
for (const error of data.errors) {
|
||||||
|
if (error.dataSource) {
|
||||||
|
error.dataSource = encodeDataSource(error.dataSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data.holdings) {
|
if (data.holdings) {
|
||||||
for (const symbol of Object.keys(data.holdings)) {
|
for (const symbol of Object.keys(data.holdings)) {
|
||||||
if (data.holdings[symbol].dataSource) {
|
if (data.holdings[symbol].dataSource) {
|
||||||
|
@ -7,7 +7,11 @@ import {
|
|||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
||||||
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
PortfolioPerformance,
|
||||||
|
UniqueAsset,
|
||||||
|
User
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
@ -24,6 +28,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
public dateRange: DateRange;
|
public dateRange: DateRange;
|
||||||
public dateRangeOptions = defaultDateRangeOptions;
|
public dateRangeOptions = defaultDateRangeOptions;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public errors: UniqueAsset[];
|
||||||
public hasError: boolean;
|
public hasError: boolean;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
@ -126,6 +131,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
.fetchPortfolioPerformance({ range: this.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
|
this.errors = response.errors;
|
||||||
this.hasError = response.hasErrors;
|
this.hasError = response.hasErrors;
|
||||||
this.performance = response.performance;
|
this.performance = response.performance;
|
||||||
this.isLoadingPerformance = false;
|
this.isLoadingPerformance = false;
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
class="pb-4"
|
class="pb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
|
[errors]="errors"
|
||||||
[hasError]="hasError"
|
[hasError]="hasError"
|
||||||
[isAllTimeHigh]="isAllTimeHigh"
|
[isAllTimeHigh]="isAllTimeHigh"
|
||||||
[isAllTimeLow]="isAllTimeLow"
|
[isAllTimeLow]="isAllTimeLow"
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
? 'Sorry! Our data provider partner is experiencing the hiccups.'
|
? 'Sorry! Our data provider partner is experiencing the hiccups.'
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
|
(click)="errors?.length > 0 && onShowErrors()"
|
||||||
>
|
>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="hasError && !isLoading"
|
*ngIf="hasError && !isLoading"
|
||||||
|
@ -7,7 +7,10 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
PortfolioPerformance,
|
||||||
|
ResponseError
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { CountUp } from 'countup.js';
|
import { CountUp } from 'countup.js';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
@ -20,6 +23,7 @@ import { isNumber } from 'lodash';
|
|||||||
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
|
@Input() errors: ResponseError['errors'];
|
||||||
@Input() hasError: boolean;
|
@Input() hasError: boolean;
|
||||||
@Input() isAllTimeHigh: boolean;
|
@Input() isAllTimeHigh: boolean;
|
||||||
@Input() isAllTimeLow: boolean;
|
@Input() isAllTimeLow: boolean;
|
||||||
@ -69,4 +73,12 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onShowErrors() {
|
||||||
|
const errorMessageParts = this.errors.map((error) => {
|
||||||
|
return `${error.symbol} (${error.dataSource})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(errorMessageParts.join('\n'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,11 @@ import {
|
|||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
|
UniqueAsset,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
@ -188,13 +190,13 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioPerformance(aParams: { [param: string]: any }) {
|
public fetchPortfolioPerformance(params: { [param: string]: any }) {
|
||||||
return this.http.get<{
|
return this.http.get<PortfolioPerformanceResponse>(
|
||||||
hasErrors: boolean;
|
'/api/portfolio/performance',
|
||||||
performance: PortfolioPerformance;
|
{
|
||||||
}>('/api/portfolio/performance', {
|
params
|
||||||
params: aParams
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPortfolioPublic(aId: string) {
|
public fetchPortfolioPublic(aId: string) {
|
||||||
|
@ -21,6 +21,8 @@ import { PortfolioReportRule } from './portfolio-report-rule.interface';
|
|||||||
import { PortfolioReport } from './portfolio-report.interface';
|
import { PortfolioReport } from './portfolio-report.interface';
|
||||||
import { PortfolioSummary } from './portfolio-summary.interface';
|
import { PortfolioSummary } from './portfolio-summary.interface';
|
||||||
import { Position } from './position.interface';
|
import { Position } from './position.interface';
|
||||||
|
import { ResponseError } from './responses/errors.interface';
|
||||||
|
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
||||||
import { TimelinePosition } from './timeline-position.interface';
|
import { TimelinePosition } from './timeline-position.interface';
|
||||||
import { UniqueAsset } from './unique-asset.interface';
|
import { UniqueAsset } from './unique-asset.interface';
|
||||||
import { UserSettings } from './user-settings.interface';
|
import { UserSettings } from './user-settings.interface';
|
||||||
@ -43,12 +45,14 @@ export {
|
|||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
PortfolioOverview,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
|
PortfolioPerformanceResponse,
|
||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioReportRule,
|
PortfolioReportRule,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
|
ResponseError,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
UniqueAsset,
|
UniqueAsset,
|
||||||
User,
|
User,
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import { UniqueAsset } from '../unique-asset.interface';
|
||||||
|
|
||||||
|
export interface ResponseError {
|
||||||
|
errors?: UniqueAsset[];
|
||||||
|
hasErrors: boolean;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import { PortfolioPerformance } from '../portfolio-performance.interface';
|
||||||
|
import { ResponseError } from './errors.interface';
|
||||||
|
|
||||||
|
export interface PortfolioPerformanceResponse extends ResponseError {
|
||||||
|
performance: PortfolioPerformance;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user