Browse Source

Add investment streaks

* Current streak
* Longest streak
pull/2042/head
Thomas 2 years ago
parent
commit
7a36408b57
  1. 34
      apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  2. 38
      apps/api/src/app/portfolio/portfolio-calculator.ts
  3. 11
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 64
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 15
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  6. 20
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  7. 1
      libs/common/src/lib/interfaces/portfolio-investments.interface.ts
  8. 2
      libs/ui/src/lib/i18n.ts

34
apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -105,6 +105,40 @@ describe('PortfolioCalculator', () => {
expect(investmentsByMonth).toEqual([
{ date: '2015-01-01', investment: new Big('640.86') },
{ date: '2015-02-01', investment: new Big('0') },
{ date: '2015-03-01', investment: new Big('0') },
{ date: '2015-04-01', investment: new Big('0') },
{ date: '2015-05-01', investment: new Big('0') },
{ date: '2015-06-01', investment: new Big('0') },
{ date: '2015-07-01', investment: new Big('0') },
{ date: '2015-08-01', investment: new Big('0') },
{ date: '2015-09-01', investment: new Big('0') },
{ date: '2015-10-01', investment: new Big('0') },
{ date: '2015-11-01', investment: new Big('0') },
{ date: '2015-12-01', investment: new Big('0') },
{ date: '2016-01-01', investment: new Big('0') },
{ date: '2016-02-01', investment: new Big('0') },
{ date: '2016-03-01', investment: new Big('0') },
{ date: '2016-04-01', investment: new Big('0') },
{ date: '2016-05-01', investment: new Big('0') },
{ date: '2016-06-01', investment: new Big('0') },
{ date: '2016-07-01', investment: new Big('0') },
{ date: '2016-08-01', investment: new Big('0') },
{ date: '2016-09-01', investment: new Big('0') },
{ date: '2016-10-01', investment: new Big('0') },
{ date: '2016-11-01', investment: new Big('0') },
{ date: '2016-12-01', investment: new Big('0') },
{ date: '2017-01-01', investment: new Big('0') },
{ date: '2017-02-01', investment: new Big('0') },
{ date: '2017-03-01', investment: new Big('0') },
{ date: '2017-04-01', investment: new Big('0') },
{ date: '2017-05-01', investment: new Big('0') },
{ date: '2017-06-01', investment: new Big('0') },
{ date: '2017-07-01', investment: new Big('0') },
{ date: '2017-08-01', investment: new Big('0') },
{ date: '2017-09-01', investment: new Big('0') },
{ date: '2017-10-01', investment: new Big('0') },
{ date: '2017-11-01', investment: new Big('0') },
{ date: '2017-12-01', investment: new Big('-14156.4') }
]);
});

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

@ -544,7 +544,7 @@ export class PortfolioCalculator {
return [];
}
const investments = [];
const investments: { date: string; investment: Big }[] = [];
let currentDate: Date;
let investmentByGroup = new Big(0);
@ -554,13 +554,11 @@ export class PortfolioCalculator {
(groupBy === 'year' || isSameMonth(parseDate(order.date), currentDate))
) {
// Same group: Add up investments
investmentByGroup = investmentByGroup.plus(
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
);
} else {
// New group: Store previous group and reset
if (currentDate) {
investments.push({
date: format(
@ -595,7 +593,39 @@ export class PortfolioCalculator {
}
}
return investments;
// Fill in the missing dates with investment = 0
const startDate = parseDate(first(this.orders).date);
const endDate = parseDate(last(this.orders).date);
const allDates: string[] = [];
currentDate = startDate;
while (currentDate <= endDate) {
allDates.push(
format(
set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
)
);
currentDate.setMonth(currentDate.getMonth() + 1);
}
for (const date of allDates) {
const existingInvestment = investments.find((investment) => {
return investment.date === date;
});
if (!existingInvestment) {
investments.push({ date, investment: new Big(0) });
}
}
return sortBy(investments, (investment) => {
return investment.date;
});
}
public async calculateTimeline(

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

@ -258,11 +258,12 @@ export class PortfolioController {
filterByTags
});
let investments = await this.portfolioService.getInvestments({
let { investments, streaks } = await this.portfolioService.getInvestments({
dateRange,
filters,
groupBy,
impersonationId
impersonationId,
savingsRate: this.request.user?.Settings?.settings.savingsRate
});
if (
@ -278,6 +279,8 @@ export class PortfolioController {
date: item.date,
investment: item.investment / maxInvestment
}));
streaks = nullifyValuesInObject(streaks, ['current', 'longest']);
}
if (
@ -287,9 +290,11 @@ export class PortfolioController {
investments = investments.map((item) => {
return nullifyValuesInObject(item, ['investment']);
});
streaks = nullifyValuesInObject(streaks, ['current', 'longest']);
}
return { investments };
return { investments, streaks };
}
@Get('performance')

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

@ -28,6 +28,7 @@ import {
Filter,
HistoricalDataItem,
PortfolioDetails,
PortfolioInvestments,
PortfolioPerformanceResponse,
PortfolioPosition,
PortfolioReport,
@ -252,13 +253,15 @@ export class PortfolioService {
dateRange,
filters,
groupBy,
impersonationId
impersonationId,
savingsRate
}: {
dateRange: DateRange;
filters?: Filter[];
groupBy?: GroupBy;
impersonationId: string;
}): Promise<InvestmentItem[]> {
savingsRate: number;
}): Promise<PortfolioInvestments> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
@ -276,7 +279,10 @@ export class PortfolioService {
portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) {
return [];
return {
investments: [],
streaks: { currentStreak: 0, longestStreak: 0 }
};
}
let investments: InvestmentItem[];
@ -346,9 +352,23 @@ export class PortfolioService {
parseDate(investments[0]?.date)
);
return investments.filter(({ date }) => {
investments = investments.filter(({ date }) => {
return !isBefore(parseDate(date), startDate);
});
let streaks: PortfolioInvestments['streaks'];
if (savingsRate) {
streaks = this.getStreaks({
investments,
savingsRate: groupBy === 'year' ? 12 * savingsRate : savingsRate
});
}
return {
investments,
streaks
};
}
public async getChart({
@ -1510,6 +1530,28 @@ export class PortfolioService {
return portfolioStart;
}
private getStreaks({
investments,
savingsRate
}: {
investments: InvestmentItem[];
savingsRate: number;
}) {
let currentStreak = 0;
let longestStreak = 0;
for (const { investment } of investments) {
if (investment >= savingsRate) {
currentStreak++;
longestStreak = Math.max(longestStreak, currentStreak);
} else {
currentStreak = 0;
}
}
return { currentStreak, longestStreak };
}
private async getSummary({
balanceInBaseCurrency,
emergencyFundPositionsValueInBaseCurrency,
@ -1841,13 +1883,6 @@ export class PortfolioService {
return { accounts, platforms };
}
private async getUserId(aImpersonationId: string, aUserId: string) {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(aImpersonationId);
return impersonationUserId || aUserId;
}
private getTotalByType(
orders: OrderWithAccount[],
currency: string,
@ -1874,4 +1909,11 @@ export class PortfolioService {
this.baseCurrency
);
}
private async getUserId(aImpersonationId: string, aUserId: string) {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(aImpersonationId);
return impersonationUserId || aUserId;
}
}

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

@ -10,6 +10,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
Filter,
HistoricalDataItem,
PortfolioInvestments,
Position,
User
} from '@ghostfolio/common/interfaces';
@ -58,6 +59,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public performanceDataItemsInPercentage: HistoricalDataItem[];
public placeholder = '';
public portfolioEvolutionDataLabel = $localize`Deposit`;
public streaks: PortfolioInvestments['streaks'];
public subLabelCurrentStreak: string;
public subLabelLongestStreak: string;
public top3: Position[];
public user: User;
@ -242,8 +246,17 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ investments }) => {
.subscribe(({ investments, streaks }) => {
this.investmentsByGroup = investments;
this.streaks = streaks;
this.subLabelCurrentStreak =
this.mode === 'year'
? `(${translate('YEARS')})`
: `(${translate('MONTHS')})`;
this.subLabelLongestStreak =
this.mode === 'year'
? `(${translate('YEARS')})`
: `(${translate('MONTHS')})`;
this.changeDetectorRef.markForCheck();
});

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

@ -177,6 +177,26 @@
(change)="onChangeGroupBy($event.value)"
></gf-toggle>
</div>
<div *ngIf="streaks" class="row">
<div class="col-md-6 col-xs-12 my-2">
<gf-value
i18n
size="large"
[subLabel]="subLabelCurrentStreak"
[value]="streaks?.currentStreak"
>Current Streak</gf-value
>
</div>
<div class="col-md-6 col-xs-12 my-2">
<gf-value
i18n
size="large"
[subLabel]="subLabelLongestStreak"
[value]="streaks?.longestStreak"
>Longest Streak</gf-value
>
</div>
</div>
<div class="chart-container">
<gf-investment-chart
class="h-100"

1
libs/common/src/lib/interfaces/portfolio-investments.interface.ts

@ -2,4 +2,5 @@ import { InvestmentItem } from './investment-item.interface';
export interface PortfolioInvestments {
investments: InvestmentItem[];
streaks: { currentStreak: number; longestStreak: number };
}

2
libs/ui/src/lib/i18n.ts

@ -13,12 +13,14 @@ const locales = {
HIGHER_RISK: $localize`Higher Risk`,
IMPORT_ACTIVITY_ERROR_IS_DUPLICATE: $localize`This activity already exists.`,
LOWER_RISK: $localize`Lower Risk`,
MONTHS: $localize`Months`,
OTHER: $localize`Other`,
RETIREMENT_PROVISION: $localize`Retirement Provision`,
SATELLITE: $localize`Satellite`,
SECURITIES: $localize`Securities`,
SYMBOL: $localize`Symbol`,
TAG: $localize`Tag`,
YEARS: $localize`Years`,
// enum AssetClass
CASH: $localize`Cash`,

Loading…
Cancel
Save