Browse Source

Added Time Weighted Performance calculation

Not correct yet
pull/5027/head
Daniel Devaud 2 years ago
parent
commit
5d28c9e6e8
  1. 36
      apps/api/src/app/portfolio/portfolio-calculator.ts
  2. 4
      apps/api/src/app/user/update-user-setting.dto.ts
  3. 1
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.scss
  4. 15
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  5. 60
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  6. 18
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  7. 10
      apps/client/src/app/services/data.service.ts
  8. 1
      libs/common/src/lib/interfaces/historical-data-item.interface.ts
  9. 1
      libs/common/src/lib/interfaces/user-settings.interface.ts

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

@ -1585,26 +1585,38 @@ export class PortfolioCalculator {
const previousOrder = orders[i - 1];
if (order.unitPrice.toNumber() && previousOrder.unitPrice.toNumber()) {
netPerformanceValuesPercentage[order.date] = previousOrder.unitPrice
.div(order.unitPrice)
netPerformanceValuesPercentage[order.date] = order.unitPrice
.div(previousOrder.unitPrice)
.minus(1);
} else if (
order.type === 'STAKE' &&
marketSymbolMap[order.date][order.symbol]
marketSymbolMap[order.date][order.symbol] &&
((marketSymbolMap[previousOrder.date][
previousOrder.symbol
]?.toNumber() &&
previousOrder.type === 'STAKE') ||
(previousOrder.type !== 'STAKE' &&
previousOrder.unitPrice.toNumber()))
) {
netPerformanceValuesPercentage[order.date] =
let previousUnitPrice =
previousOrder.type === 'STAKE'
? marketSymbolMap[previousOrder.date][previousOrder.symbol]
: previousOrder.unitPrice
.div(marketSymbolMap[order.date][order.symbol])
.minus(1);
: previousOrder.unitPrice;
netPerformanceValuesPercentage[order.date] = marketSymbolMap[
order.date
][order.symbol]
? marketSymbolMap[order.date][order.symbol]
.div(previousUnitPrice)
.minus(1)
: new Big(0);
} else if (previousOrder.unitPrice.toNumber()) {
netPerformanceValuesPercentage[order.date] = new Big(-1);
} else if (previousOrder.type === 'STAKE' && order.unitPrice.toNumber()) {
netPerformanceValuesPercentage[order.date] = marketSymbolMap[
previousOrder.date
][previousOrder.symbol]
.div(order.unitPrice)
} else if (
previousOrder.type === 'STAKE' &&
marketSymbolMap[previousOrder.date][previousOrder.symbol]?.toNumber()
) {
netPerformanceValuesPercentage[order.date] = order.unitPrice
.div(marketSymbolMap[previousOrder.date][previousOrder.symbol])
.minus(1);
} else {
netPerformanceValuesPercentage[order.date] = new Big(0);

4
apps/api/src/app/user/update-user-setting.dto.ts

@ -68,4 +68,8 @@ export class UpdateUserSettingDto {
@IsIn(<ViewMode[]>['DEFAULT', 'ZEN'])
@IsOptional()
viewMode?: ViewMode;
@IsIn(<string[]>['N', 'B', 'O'])
@IsOptional()
timeWeightedPerformance?: string;
}

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;
}
}

15
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,15 @@ 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,
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})`,

60
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,6 +63,8 @@ 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'];
@ -212,6 +221,24 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
}
public onTimeWeightedPerformanceChanged(timeWeightedPerformance: string) {
this.dataService
.putUserSetting({ timeWeightedPerformance })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode;
this.fetchDividendsAndInvestments();
@ -252,16 +279,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 +341,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.user?.settings?.timeWeightedPerformance === 'N' ? false : true
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart, firstOrderDate }) => {
@ -324,6 +353,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.investments = [];
this.performanceDataItems = [];
this.performanceDataItemsInPercentage = [];
this.performanceDataItemsTimeWeightedInPercentage = [];
for (const [
index,
@ -332,7 +362,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 +378,23 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
date,
value: netPerformanceInPercentage
});
if ((this.user?.settings?.timeWeightedPerformance ?? 'N') !== 'N') {
let lastPerformance = 0;
if (index > 0) {
lastPerformance = new Big(
chart[index - 1].timeWeightedPerformance
)
.plus(1)
.mul(new Big(chart[index].timeWeightedPerformance).plus(1))
.minus(1)
.toNumber();
}
chart[index].timeWeightedPerformance = lastPerformance;
this.performanceDataItemsTimeWeightedInPercentage.push({
date,
value: lastPerformance
});
}
}
this.isLoadingInvestmentChart = false;

18
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"
@ -26,9 +26,25 @@
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[locale]="user?.settings?.locale"
[performanceDataItems]="performanceDataItemsInPercentage"
[timeWeightedPerformanceDataItems]="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]="user?.settings?.timeWeightedPerformance ?? 'N'"
[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;
}

1
libs/common/src/lib/interfaces/user-settings.interface.ts

@ -15,4 +15,5 @@ export interface UserSettings {
retirementDate?: string;
savingsRate?: number;
viewMode?: ViewMode;
timeWeightedPerformance?: string;
}

Loading…
Cancel
Save