Browse Source

Merge pull request #50 from dandevaud/feature/time-weighted-graph-and-performance-&-refactoring

Feature/time weighted graph and performance & refactoring
pull/5027/head
dandevaud 2 years ago
committed by GitHub
parent
commit
b2eb1d361f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts
  2. 1509
      apps/api/src/app/portfolio/portfolio-calculator.ts
  3. 6
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 14
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 1
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.scss
  6. 16
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  7. 53
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  8. 20
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  9. 10
      apps/client/src/app/services/data.service.ts
  10. 1
      libs/common/src/lib/interfaces/historical-data-item.interface.ts

2
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -77,6 +77,7 @@ describe('PortfolioCalculator', () => {
date: '2022-03-07',
netPerformanceInPercentage: 0,
netPerformance: 0,
timeWeightedPerformance: 0,
totalInvestment: 151.6,
value: 151.6
});
@ -85,6 +86,7 @@ describe('PortfolioCalculator', () => {
date: '2022-04-11',
netPerformanceInPercentage: 13.100263852242744,
netPerformance: 19.86,
timeWeightedPerformance: 0,
totalInvestment: 0,
value: 0
});

1509
apps/api/src/app/portfolio/portfolio-calculator.ts

File diff suppressed because it is too large

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

@ -359,7 +359,8 @@ export class PortfolioController {
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false
@Query('withExcludedAccounts') withExcludedAccounts = false,
@Query('timeWeightedPerformance') calculateTimeWeightedPerformance = false
): Promise<PortfolioPerformanceResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
@ -372,7 +373,8 @@ export class PortfolioController {
filters,
impersonationId,
withExcludedAccounts,
userId: this.request.user.id
userId: this.request.user.id,
calculateTimeWeightedPerformance
});
if (

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

@ -1153,13 +1153,15 @@ export class PortfolioService {
filters,
impersonationId,
userId,
withExcludedAccounts = false
withExcludedAccounts = false,
calculateTimeWeightedPerformance = false
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userId: string;
withExcludedAccounts?: boolean;
calculateTimeWeightedPerformance?: boolean;
}): Promise<PortfolioPerformanceResponse> {
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId });
@ -1254,7 +1256,8 @@ export class PortfolioService {
portfolioOrders,
transactionPoints,
userCurrency,
userId
userId,
calculateTimeWeightedPerformance
});
const itemOfToday = items.find(({ date }) => {
@ -1463,7 +1466,8 @@ export class PortfolioService {
portfolioOrders,
transactionPoints,
userCurrency,
userId
userId,
calculateTimeWeightedPerformance
}: {
dateRange?: DateRange;
impersonationId: string;
@ -1471,6 +1475,7 @@ export class PortfolioService {
transactionPoints: TransactionPoint[];
userCurrency: string;
userId: string;
calculateTimeWeightedPerformance: boolean;
}): Promise<HistoricalDataContainer> {
if (transactionPoints.length === 0) {
return {
@ -1503,7 +1508,8 @@ export class PortfolioService {
const items = await portfolioCalculator.getChartData(
startDate,
endDate,
step
step,
calculateTimeWeightedPerformance
);
return {

1
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.scss

@ -7,5 +7,6 @@
ngx-skeleton-loader {
height: 100%;
}
margin-bottom: 0.5rem;
}
}

16
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

@ -53,6 +53,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Input() isLoading: boolean;
@Input() locale: string;
@Input() performanceDataItems: LineChartItem[];
@Input() timeWeightedPerformanceDataItems: LineChartItem[];
@Input() user: User;
@Output() benchmarkChanged = new EventEmitter<string>();
@ -83,7 +84,10 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
permissions.accessAdminControl
);
if (this.performanceDataItems) {
if (
this.performanceDataItems ||
this.timeWeightedPerformanceDataItems?.length > 0
) {
this.initialize();
}
}
@ -108,6 +112,16 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
}),
label: $localize`Portfolio`
},
{
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: 2,
borderDash: [5, 5],
data: this.timeWeightedPerformanceDataItems.map(({ date, value }) => {
return { x: parseDate(date).getTime(), y: value };
}),
label: $localize`Portfolio (time-weighted)`
},
{
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,

53
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -19,6 +19,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
import { AssetClass, DataSource, SymbolProfile } from '@prisma/client';
import Big from 'big.js';
import { differenceInDays } from 'date-fns';
import { isNumber, sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
@ -37,6 +38,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public benchmarks: Partial<SymbolProfile>[];
public bottom3: Position[];
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public timeWeightedPerformanceOptions = [
{ label: $localize`No`, value: 'N' },
{ label: $localize`Both`, value: 'B' },
{ label: $localize`Only`, value: 'O' }
];
public selectedTimeWeightedPerformanceOption: string;
public daysInMarket: number;
public deviceType: string;
public dividendsByGroup: InvestmentItem[];
@ -56,9 +63,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
];
public performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[];
public performanceDataItemsTimeWeightedInPercentage: HistoricalDataItem[] =
[];
public placeholder = '';
public portfolioEvolutionDataLabel = $localize`Deposit`;
public streaks: PortfolioInvestments['streaks'];
public timeWeightedPerformance: string = 'N';
public top3: Position[];
public unitCurrentStreak: string;
public unitLongestStreak: string;
@ -212,6 +222,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
}
public onTimeWeightedPerformanceChanged(timeWeightedPerformance: string) {
this.timeWeightedPerformance = timeWeightedPerformance;
this.update();
}
public onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode;
this.fetchDividendsAndInvestments();
@ -252,16 +268,16 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
? translate('YEAR')
: translate('YEARS')
: this.streaks?.currentStreak === 1
? translate('MONTH')
: translate('MONTHS');
? translate('MONTH')
: translate('MONTHS');
this.unitLongestStreak =
this.mode === 'year'
? this.streaks?.longestStreak === 1
? translate('YEAR')
: translate('YEARS')
: this.streaks?.longestStreak === 1
? translate('MONTH')
: translate('MONTHS');
? translate('MONTH')
: translate('MONTHS');
this.changeDetectorRef.markForCheck();
});
@ -314,7 +330,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService
.fetchPortfolioPerformance({
filters: this.activeFilters,
range: this.user?.settings?.dateRange
range: this.user?.settings?.dateRange,
timeWeightedPerformance:
this.timeWeightedPerformance === 'N' ? false : true
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart, firstOrderDate }) => {
@ -324,6 +342,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.investments = [];
this.performanceDataItems = [];
this.performanceDataItemsInPercentage = [];
this.performanceDataItemsTimeWeightedInPercentage = [];
for (const [
index,
@ -332,7 +351,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
netPerformanceInPercentage,
totalInvestment,
value,
valueInPercentage
valueInPercentage,
timeWeightedPerformance
}
] of chart.entries()) {
if (index > 0 || this.user?.settings?.dateRange === 'max') {
@ -347,6 +367,27 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
date,
value: netPerformanceInPercentage
});
if ((this.timeWeightedPerformance ?? 'N') !== 'N') {
let lastPerformance = 0;
if (index > 0) {
lastPerformance = new Big(
chart[index - 1].timeWeightedPerformance
)
.div(100)
.plus(1)
.mul(
new Big(chart[index].timeWeightedPerformance).div(100).plus(1)
)
.minus(1)
.mul(100)
.toNumber();
}
chart[index].timeWeightedPerformance = lastPerformance;
this.performanceDataItemsTimeWeightedInPercentage.push({
date,
value: lastPerformance
});
}
}
this.isLoadingInvestmentChart = false;

20
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -17,7 +17,7 @@
<div class="mb-5 row">
<div class="col-lg">
<gf-benchmark-comparator
class="h-100"
class="h-auto"
[benchmark]="user?.settings?.benchmark"
[benchmarkDataItems]="benchmarkDataItems"
[benchmarks]="benchmarks"
@ -25,10 +25,26 @@
[daysInMarket]="daysInMarket"
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[locale]="user?.settings?.locale"
[performanceDataItems]="performanceDataItemsInPercentage"
[performanceDataItems]="timeWeightedPerformance === 'O' ? [] :performanceDataItemsInPercentage"
[timeWeightedPerformanceDataItems]="timeWeightedPerformance === 'N' ? [] :performanceDataItemsTimeWeightedInPercentage"
[user]="user"
(benchmarkChanged)="onChangeBenchmark($event)"
></gf-benchmark-comparator>
<div>
<div class="col-md-6 col-xs-12 d-flex">
<div
class="align-items-center d-flex flex-grow-1 h6 mb-0 py-2 text-truncate"
>
<span i18n>Include time-weighted performance </span>
<gf-toggle
[defaultValue]="timeWeightedPerformance"
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[options]="timeWeightedPerformanceOptions"
(change)="onTimeWeightedPerformanceChanged($event.value)"
></gf-toggle>
</div>
</div>
</div>
</div>
</div>

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

@ -398,11 +398,13 @@ export class DataService {
public fetchPortfolioPerformance({
filters,
range,
withExcludedAccounts = false
withExcludedAccounts = false,
timeWeightedPerformance = false
}: {
filters?: Filter[];
range: DateRange;
withExcludedAccounts?: boolean;
timeWeightedPerformance?: boolean;
}): Observable<PortfolioPerformanceResponse> {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range);
@ -410,6 +412,12 @@ export class DataService {
if (withExcludedAccounts) {
params = params.append('withExcludedAccounts', withExcludedAccounts);
}
if (timeWeightedPerformance) {
params = params.append(
'timeWeightedPerformance',
timeWeightedPerformance
);
}
return this.http
.get<any>(`/api/v2/portfolio/performance`, {

1
libs/common/src/lib/interfaces/historical-data-item.interface.ts

@ -12,4 +12,5 @@ export interface HistoricalDataItem {
totalInvestment?: number;
value?: number;
valueInPercentage?: number;
timeWeightedPerformance?: number;
}

Loading…
Cancel
Save