Browse Source

New endpoint for Dividends aggregated per month

pull/1403/head
João Pereira 3 years ago
parent
commit
539bd624b8
No known key found for this signature in database GPG Key ID: D6CBCE84DEA64F2E
  1. 18
      apps/api/src/app/portfolio/portfolio-calculator.ts
  2. 53
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 13
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 17
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  5. 36
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  6. 14
      apps/client/src/app/services/data.service.ts
  7. 2
      libs/common/src/lib/interfaces/index.ts
  8. 5
      libs/common/src/lib/interfaces/portfolio-dividends.interface.ts

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

@ -63,7 +63,7 @@ export class PortfolioCalculator {
this.orders.sort((a, b) => a.date?.localeCompare(b.date));
}
public computeTransactionPoints() {
public computeTransactionPoints(types: TypeOfOrder[] = ['BUY', 'SELL']) {
this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
@ -85,17 +85,21 @@ export class PortfolioCalculator {
let investment = new Big(0);
if (newQuantity.gt(0)) {
if (order.type === 'BUY') {
if (order.type === 'BUY' && types.includes('BUY')) {
investment = oldAccumulatedSymbol.investment.plus(
order.quantity.mul(unitPrice)
);
} else if (order.type === 'SELL') {
} else if (order.type === 'SELL' && types.includes('SELL')) {
const averagePrice = oldAccumulatedSymbol.investment.div(
oldAccumulatedSymbol.quantity
);
investment = oldAccumulatedSymbol.investment.minus(
order.quantity.mul(averagePrice)
);
} else if (order.type === 'DIVIDEND' && types.includes('DIVIDEND')) {
investment = oldAccumulatedSymbol.investment.plus(
order.quantity.mul(unitPrice)
);
}
}
@ -492,9 +496,10 @@ export class PortfolioCalculator {
}
currentDate = parseDate(order.date);
investmentByMonth = order.quantity
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
}
if (index === this.orders.length - 1) {
@ -816,6 +821,9 @@ export class PortfolioCalculator {
case 'SELL':
factor = -1;
break;
case 'DIVIDEND':
factor = 1;
break;
default:
factor = 0;
break;

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

@ -14,6 +14,7 @@ import { parseDate } from '@ghostfolio/common/helper';
import {
PortfolioDetails,
PortfolioInvestments,
PortfolioDividends,
PortfolioPerformanceResponse,
PortfolioPublicDetails,
PortfolioReport
@ -230,6 +231,58 @@ export class PortfolioController {
return { investments };
}
@Get('dividends')
@UseGuards(AuthGuard('jwt'))
public async getDividends(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange: DateRange = 'max',
@Query('groupBy') groupBy?: GroupBy
): Promise<PortfolioDividends> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
let dividends: InvestmentItem[];
if (groupBy === 'month') {
dividends = await this.portfolioService.getInvestments({
dateRange,
impersonationId,
groupBy: 'month',
orderTypes: ['DIVIDEND']
});
} else {
dividends = await this.portfolioService.getInvestments({
dateRange,
impersonationId,
orderTypes: ['DIVIDEND']
});
}
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
const maxInvestment = dividends.reduce(
(investment, item) => Math.max(investment, item.investment),
1
);
dividends = dividends.map((item) => ({
date: item.date,
investment: item.investment / maxInvestment
}));
}
return { dividends };
}
@Get('performance')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)

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

@ -210,18 +210,21 @@ export class PortfolioService {
public async getInvestments({
dateRange,
impersonationId,
groupBy
groupBy,
orderTypes = ['BUY', 'SELL']
}: {
dateRange: DateRange;
impersonationId: string;
groupBy?: GroupBy;
orderTypes?: TypeOfOrder[]
}): Promise<InvestmentItem[]> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId,
includeDrafts: true
includeDrafts: true,
orderTypes: orderTypes
});
const portfolioCalculator = new PortfolioCalculator({
@ -1370,12 +1373,14 @@ export class PortfolioService {
filters,
includeDrafts = false,
userId,
withExcludedAccounts
withExcludedAccounts,
orderTypes = ['BUY', 'SELL']
}: {
filters?: Filter[];
includeDrafts?: boolean;
userId: string;
withExcludedAccounts?: boolean;
orderTypes?: TypeOfOrder[]
}): Promise<{
transactionPoints: TransactionPoint[];
orders: OrderWithAccount[];
@ -1390,7 +1395,7 @@ export class PortfolioService {
userCurrency,
userId,
withExcludedAccounts,
types: ['BUY', 'SELL']
types: orderTypes
});
if (orders.length <= 0) {

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

@ -34,6 +34,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean;
public investments: InvestmentItem[];
public investmentsByMonth: InvestmentItem[];
public dividendsByMonth: InvestmentItem[];
public isLoadingBenchmarkComparator: boolean;
public isLoadingInvestmentChart: boolean;
public mode: GroupBy = 'month';
@ -165,7 +166,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
});
this.dataService
this.dataService
.fetchInvestments({
groupBy: 'month',
range: this.user?.settings?.dateRange
@ -177,7 +178,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
});
this.dataService
this.dataService
.fetchPositions({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ positions }) => {
@ -197,6 +198,18 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchDividends({
groupBy: 'month',
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ dividends }) => {
this.dividendsByMonth = dividends;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}

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

@ -168,4 +168,40 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg">
<div class="align-items-center d-flex mb-4">
<div
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
>
<span i18n>Dividends Timeline</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator>
</div>
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="mode"
[isLoading]="false"
[options]="modeOptions"
(change)="onChangeGroupBy($event.value)"
></gf-toggle>
</div>
<div class="chart-container">
<gf-investment-chart
class="h-100"
groupBy="month"
[benchmarkDataItems]="dividendsByMonth"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange"
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
></gf-investment-chart>
</div>
</div>
</div>
</div>

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

@ -26,6 +26,7 @@ import {
InfoItem,
OAuthResponse,
PortfolioDetails,
PortfolioDividends,
PortfolioInvestments,
PortfolioPerformanceResponse,
PortfolioPublicDetails,
@ -182,6 +183,19 @@ export class DataService {
);
}
public fetchDividends({
groupBy,
range
}: {
groupBy?: 'month';
range: DateRange;
}) {
return this.http.get<PortfolioDividends>(
'/api/v1/portfolio/dividends',
{ params: { groupBy, range } }
);
}
public fetchSymbolItem({
dataSource,
includeHistoricalData,

2
libs/common/src/lib/interfaces/index.ts

@ -19,6 +19,7 @@ import { InfoItem } from './info-item.interface';
import { LineChartItem } from './line-chart-item.interface';
import { PortfolioChart } from './portfolio-chart.interface';
import { PortfolioDetails } from './portfolio-details.interface';
import { PortfolioDividends } from './portfolio-dividends.interface';
import { PortfolioInvestments } from './portfolio-investments.interface';
import { PortfolioItem } from './portfolio-item.interface';
import { PortfolioOverview } from './portfolio-overview.interface';
@ -63,6 +64,7 @@ export {
PortfolioChart,
PortfolioDetails,
PortfolioInvestments,
PortfolioDividends,
PortfolioItem,
PortfolioOverview,
PortfolioPerformance,

5
libs/common/src/lib/interfaces/portfolio-dividends.interface.ts

@ -0,0 +1,5 @@
import { InvestmentItem } from './investment-item.interface';
export interface PortfolioDividends {
dividends: InvestmentItem[];
}
Loading…
Cancel
Save