Browse Source

Feature/dividend (#547)

* Add dividend to order type

* Support dividend in transactions table

* Support dividend in transaction dialog

* Extend import file with dividend

* Add dividend to portfolio summary

* Update changelog

Co-authored-by: Fly Man <fly.man.opensim@gmail.com>
pull/592/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
9e1a7fc981
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 14
      apps/api/src/app/order/order.service.ts
  3. 1
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 87
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 14
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  6. 8
      apps/client/src/app/components/transactions-table/transactions-table.component.html
  7. 4
      apps/client/src/app/components/transactions-table/transactions-table.component.scss
  8. 3
      apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html
  9. 9
      apps/client/src/app/services/import-transactions.service.ts
  10. 1
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  11. 2
      prisma/migrations/20211215205808_added_dividend_to_order_type/migration.sql
  12. 1
      prisma/schema.prisma
  13. 1
      test/import/ok.csv

5
CHANGELOG.md

@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added the transactions to the position detail dialog - Added the transactions to the position detail dialog
- Added support for dividend
### Todo
- Apply data migration (`yarn database:migrate`)
## 1.96.0 - 27.12.2021 ## 1.96.0 - 27.12.2021

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

@ -3,7 +3,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, Order, Prisma } from '@prisma/client'; import { DataSource, Order, Prisma, Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
@ -85,9 +85,11 @@ export class OrderService {
public async getOrders({ public async getOrders({
includeDrafts = false, includeDrafts = false,
types,
userId userId
}: { }: {
includeDrafts?: boolean; includeDrafts?: boolean;
types?: TypeOfOrder[];
userId: string; userId: string;
}) { }) {
const where: Prisma.OrderWhereInput = { userId }; const where: Prisma.OrderWhereInput = { userId };
@ -96,6 +98,16 @@ export class OrderService {
where.isDraft = false; where.isDraft = false;
} }
if (types) {
where.OR = types.map((type) => {
return {
type: {
equals: type
}
};
});
}
return ( return (
await this.orders({ await this.orders({
where, where,

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

@ -330,6 +330,7 @@ export class PortfolioController {
'currentGrossPerformance', 'currentGrossPerformance',
'currentNetPerformance', 'currentNetPerformance',
'currentValue', 'currentValue',
'dividend',
'fees', 'fees',
'netWorth', 'netWorth',
'totalBuy', 'totalBuy',

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

@ -401,7 +401,11 @@ export class PortfolioService {
const positionCurrency = orders[0].currency; const positionCurrency = orders[0].currency;
const name = orders[0].SymbolProfile?.name ?? ''; const name = orders[0].SymbolProfile?.name ?? '';
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ const portfolioOrders: PortfolioOrder[] = orders
.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL';
})
.map((order) => ({
currency: order.currency, currency: order.currency,
dataSource: order.dataSource, dataSource: order.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
@ -729,22 +733,6 @@ export class PortfolioService {
}; };
} }
public getFees(orders: OrderWithAccount[], date = new Date(0)) {
return orders
.filter((order) => {
// Filter out all orders before given date
return isBefore(date, new Date(order.date));
})
.map((order) => {
return this.exchangeRateDataService.toCurrency(
order.fee,
order.currency,
this.request.user.Settings.currency
);
})
.reduce((previous, current) => previous + current, 0);
}
public async getReport(impersonationId: string): Promise<PortfolioReport> { public async getReport(impersonationId: string): Promise<PortfolioReport> {
const currency = this.request.user.Settings.currency; const currency = this.request.user.Settings.currency;
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);
@ -825,7 +813,7 @@ export class PortfolioService {
new FeeRatioInitialInvestment( new FeeRatioInitialInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
currentPositions.totalInvestment.toNumber(), currentPositions.totalInvestment.toNumber(),
this.getFees(orders) this.getFees(orders).toNumber()
) )
], ],
{ baseCurrency: currency } { baseCurrency: currency }
@ -844,8 +832,11 @@ export class PortfolioService {
userId, userId,
currency currency
); );
const orders = await this.orderService.getOrders({ userId }); const orders = await this.orderService.getOrders({
const fees = this.getFees(orders); userId
});
const dividend = this.getDividend(orders).toNumber();
const fees = this.getFees(orders).toNumber();
const firstOrderDate = orders[0]?.date; const firstOrderDate = orders[0]?.date;
const totalBuy = this.getTotalByType(orders, currency, 'BUY'); const totalBuy = this.getTotalByType(orders, currency, 'BUY');
@ -859,14 +850,17 @@ export class PortfolioService {
return { return {
...performanceInformation.performance, ...performanceInformation.performance,
dividend,
fees, fees,
firstOrderDate, firstOrderDate,
netWorth, netWorth,
totalBuy,
totalSell,
cash: balance, cash: balance,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
ordersCount: orders.length, ordersCount: orders.filter((order) => {
totalBuy: totalBuy, return order.type === 'BUY' || order.type === 'SELL';
totalSell: totalSell }).length
}; };
} }
@ -939,6 +933,47 @@ export class PortfolioService {
return cashPositions; return cashPositions;
} }
private getDividend(orders: OrderWithAccount[], date = new Date(0)) {
return orders
.filter((order) => {
// Filter out all orders before given date and type dividend
return (
isBefore(date, new Date(order.date)) &&
order.type === TypeOfOrder.DIVIDEND
);
})
.map((order) => {
return this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.currency,
this.request.user.Settings.currency
);
})
.reduce(
(previous, current) => new Big(previous).plus(current),
new Big(0)
);
}
private getFees(orders: OrderWithAccount[], date = new Date(0)) {
return orders
.filter((order) => {
// Filter out all orders before given date
return isBefore(date, new Date(order.date));
})
.map((order) => {
return this.exchangeRateDataService.toCurrency(
order.fee,
order.currency,
this.request.user.Settings.currency
);
})
.reduce(
(previous, current) => new Big(previous).plus(current),
new Big(0)
);
}
private getStartDate(aDateRange: DateRange, portfolioStart: Date) { private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
switch (aDateRange) { switch (aDateRange) {
case '1d': case '1d':
@ -967,7 +1002,11 @@ export class PortfolioService {
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
orders: OrderWithAccount[]; orders: OrderWithAccount[];
}> { }> {
const orders = await this.orderService.getOrders({ includeDrafts, userId }); const orders = await this.orderService.getOrders({
includeDrafts,
userId,
types: ['BUY', 'SELL']
});
if (orders.length <= 0) { if (orders.length <= 0) {
return { transactionPoints: [], orders: [] }; return { transactionPoints: [], orders: [] };

14
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -169,4 +169,18 @@
></gf-value> ></gf-value>
</div> </div>
</div> </div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Dividend</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.dividend"
></gf-value>
</div>
</div>
</div> </div>

8
apps/client/src/app/components/transactions-table/transactions-table.component.html

@ -77,11 +77,15 @@
<td mat-cell *matCellDef="let element" class="px-1"> <td mat-cell *matCellDef="let element" class="px-1">
<div <div
class="d-inline-flex p-1 type-badge" class="d-inline-flex p-1 type-badge"
[ngClass]="element.type == 'BUY' ? 'buy' : 'sell'" [ngClass]="{
buy: element.type === 'BUY',
dividend: element.type === 'DIVIDEND',
sell: element.type === 'SELL'
}"
> >
<ion-icon <ion-icon
[name]=" [name]="
element.type === 'BUY' element.type === 'BUY' || element.type === 'DIVIDEND'
? 'arrow-forward-circle-outline' ? 'arrow-forward-circle-outline'
: 'arrow-back-circle-outline' : 'arrow-back-circle-outline'
" "

4
apps/client/src/app/components/transactions-table/transactions-table.component.scss

@ -37,6 +37,10 @@
color: var(--green); color: var(--green);
} }
&.dividend {
color: var(--blue);
}
&.sell { &.sell {
color: var(--orange); color: var(--orange);
} }

3
apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html

@ -53,6 +53,7 @@
<mat-label i18n>Type</mat-label> <mat-label i18n>Type</mat-label>
<mat-select name="type" required [(value)]="data.transaction.type"> <mat-select name="type" required [(value)]="data.transaction.type">
<mat-option value="BUY" i18n>BUY</mat-option> <mat-option value="BUY" i18n>BUY</mat-option>
<mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option>
<mat-option value="SELL" i18n>SELL</mat-option> <mat-option value="SELL" i18n>SELL</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -141,7 +142,7 @@
[(ngModel)]="data.transaction.unitPrice" [(ngModel)]="data.transaction.unitPrice"
/> />
<button <button
*ngIf="currentMarketPrice" *ngIf="currentMarketPrice && (data.transaction.type === 'BUY' || data.transaction.type === 'SELL')"
mat-icon-button mat-icon-button
matSuffix matSuffix
title="Apply current market price" title="Apply current market price"

9
apps/client/src/app/services/import-transactions.service.ts

@ -214,10 +214,15 @@ export class ImportTransactionsService {
for (const key of ImportTransactionsService.TYPE_KEYS) { for (const key of ImportTransactionsService.TYPE_KEYS) {
if (item[key]) { if (item[key]) {
if (item[key].toLowerCase() === 'buy') { switch (item[key].toLowerCase()) {
case 'buy':
return Type.BUY; return Type.BUY;
} else if (item[key].toLowerCase() === 'sell') { case 'dividend':
return Type.DIVIDEND;
case 'sell':
return Type.SELL; return Type.SELL;
default:
break;
} }
} }
} }

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

@ -3,6 +3,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
export interface PortfolioSummary extends PortfolioPerformance { export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercent: number; annualizedPerformancePercent: number;
cash: number; cash: number;
dividend: number;
committedFunds: number; committedFunds: number;
fees: number; fees: number;
firstOrderDate: Date; firstOrderDate: Date;

2
prisma/migrations/20211215205808_added_dividend_to_order_type/migration.sql

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Type" ADD VALUE 'DIVIDEND';

1
prisma/schema.prisma

@ -206,5 +206,6 @@ enum Role {
enum Type { enum Type {
BUY BUY
DIVIDEND
SELL SELL
} }

1
test/import/ok.csv

@ -1,2 +1,3 @@
Date,Code,Currency,Price,Quantity,Action,Fee Date,Code,Currency,Price,Quantity,Action,Fee
17/11/2021,MSFT,USD,0.62,5,dividend,0.00
16/09/2021,MSFT,USD,298.580,5,buy,19.00 16/09/2021,MSFT,USD,298.580,5,buy,19.00

1 Date Code Currency Price Quantity Action Fee
2 17/11/2021 MSFT USD 0.62 5 dividend 0.00
3 16/09/2021 MSFT USD 298.580 5 buy 19.00
Loading…
Cancel
Save