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>
This commit is contained in:
parent
ff638adf03
commit
9e1a7fc981
@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- 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
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
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 { endOfToday, isAfter } from 'date-fns';
|
||||
|
||||
@ -85,9 +85,11 @@ export class OrderService {
|
||||
|
||||
public async getOrders({
|
||||
includeDrafts = false,
|
||||
types,
|
||||
userId
|
||||
}: {
|
||||
includeDrafts?: boolean;
|
||||
types?: TypeOfOrder[];
|
||||
userId: string;
|
||||
}) {
|
||||
const where: Prisma.OrderWhereInput = { userId };
|
||||
@ -96,6 +98,16 @@ export class OrderService {
|
||||
where.isDraft = false;
|
||||
}
|
||||
|
||||
if (types) {
|
||||
where.OR = types.map((type) => {
|
||||
return {
|
||||
type: {
|
||||
equals: type
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
await this.orders({
|
||||
where,
|
||||
|
@ -330,6 +330,7 @@ export class PortfolioController {
|
||||
'currentGrossPerformance',
|
||||
'currentNetPerformance',
|
||||
'currentValue',
|
||||
'dividend',
|
||||
'fees',
|
||||
'netWorth',
|
||||
'totalBuy',
|
||||
|
@ -401,17 +401,21 @@ export class PortfolioService {
|
||||
const positionCurrency = orders[0].currency;
|
||||
const name = orders[0].SymbolProfile?.name ?? '';
|
||||
|
||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||
currency: order.currency,
|
||||
dataSource: order.dataSource,
|
||||
date: format(order.date, DATE_FORMAT),
|
||||
fee: new Big(order.fee),
|
||||
name: order.SymbolProfile?.name,
|
||||
quantity: new Big(order.quantity),
|
||||
symbol: order.symbol,
|
||||
type: order.type,
|
||||
unitPrice: new Big(order.unitPrice)
|
||||
}));
|
||||
const portfolioOrders: PortfolioOrder[] = orders
|
||||
.filter((order) => {
|
||||
return order.type === 'BUY' || order.type === 'SELL';
|
||||
})
|
||||
.map((order) => ({
|
||||
currency: order.currency,
|
||||
dataSource: order.dataSource,
|
||||
date: format(order.date, DATE_FORMAT),
|
||||
fee: new Big(order.fee),
|
||||
name: order.SymbolProfile?.name,
|
||||
quantity: new Big(order.quantity),
|
||||
symbol: order.symbol,
|
||||
type: order.type,
|
||||
unitPrice: new Big(order.unitPrice)
|
||||
}));
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator(
|
||||
this.currentRateService,
|
||||
@ -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> {
|
||||
const currency = this.request.user.Settings.currency;
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
@ -825,7 +813,7 @@ export class PortfolioService {
|
||||
new FeeRatioInitialInvestment(
|
||||
this.exchangeRateDataService,
|
||||
currentPositions.totalInvestment.toNumber(),
|
||||
this.getFees(orders)
|
||||
this.getFees(orders).toNumber()
|
||||
)
|
||||
],
|
||||
{ baseCurrency: currency }
|
||||
@ -844,8 +832,11 @@ export class PortfolioService {
|
||||
userId,
|
||||
currency
|
||||
);
|
||||
const orders = await this.orderService.getOrders({ userId });
|
||||
const fees = this.getFees(orders);
|
||||
const orders = await this.orderService.getOrders({
|
||||
userId
|
||||
});
|
||||
const dividend = this.getDividend(orders).toNumber();
|
||||
const fees = this.getFees(orders).toNumber();
|
||||
const firstOrderDate = orders[0]?.date;
|
||||
|
||||
const totalBuy = this.getTotalByType(orders, currency, 'BUY');
|
||||
@ -859,14 +850,17 @@ export class PortfolioService {
|
||||
|
||||
return {
|
||||
...performanceInformation.performance,
|
||||
dividend,
|
||||
fees,
|
||||
firstOrderDate,
|
||||
netWorth,
|
||||
totalBuy,
|
||||
totalSell,
|
||||
cash: balance,
|
||||
committedFunds: committedFunds.toNumber(),
|
||||
ordersCount: orders.length,
|
||||
totalBuy: totalBuy,
|
||||
totalSell: totalSell
|
||||
ordersCount: orders.filter((order) => {
|
||||
return order.type === 'BUY' || order.type === 'SELL';
|
||||
}).length
|
||||
};
|
||||
}
|
||||
|
||||
@ -939,6 +933,47 @@ export class PortfolioService {
|
||||
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) {
|
||||
switch (aDateRange) {
|
||||
case '1d':
|
||||
@ -967,7 +1002,11 @@ export class PortfolioService {
|
||||
transactionPoints: TransactionPoint[];
|
||||
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) {
|
||||
return { transactionPoints: [], orders: [] };
|
||||
|
@ -169,4 +169,18 @@
|
||||
></gf-value>
|
||||
</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>
|
||||
|
@ -77,11 +77,15 @@
|
||||
<td mat-cell *matCellDef="let element" class="px-1">
|
||||
<div
|
||||
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
|
||||
[name]="
|
||||
element.type === 'BUY'
|
||||
element.type === 'BUY' || element.type === 'DIVIDEND'
|
||||
? 'arrow-forward-circle-outline'
|
||||
: 'arrow-back-circle-outline'
|
||||
"
|
||||
|
@ -37,6 +37,10 @@
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
&.dividend {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
&.sell {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
@ -52,8 +52,9 @@
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Type</mat-label>
|
||||
<mat-select name="type" required [(value)]="data.transaction.type">
|
||||
<mat-option value="BUY" i18n> BUY </mat-option>
|
||||
<mat-option value="SELL" i18n> SELL </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-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@ -141,7 +142,7 @@
|
||||
[(ngModel)]="data.transaction.unitPrice"
|
||||
/>
|
||||
<button
|
||||
*ngIf="currentMarketPrice"
|
||||
*ngIf="currentMarketPrice && (data.transaction.type === 'BUY' || data.transaction.type === 'SELL')"
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
title="Apply current market price"
|
||||
|
@ -214,10 +214,15 @@ export class ImportTransactionsService {
|
||||
|
||||
for (const key of ImportTransactionsService.TYPE_KEYS) {
|
||||
if (item[key]) {
|
||||
if (item[key].toLowerCase() === 'buy') {
|
||||
return Type.BUY;
|
||||
} else if (item[key].toLowerCase() === 'sell') {
|
||||
return Type.SELL;
|
||||
switch (item[key].toLowerCase()) {
|
||||
case 'buy':
|
||||
return Type.BUY;
|
||||
case 'dividend':
|
||||
return Type.DIVIDEND;
|
||||
case 'sell':
|
||||
return Type.SELL;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||
export interface PortfolioSummary extends PortfolioPerformance {
|
||||
annualizedPerformancePercent: number;
|
||||
cash: number;
|
||||
dividend: number;
|
||||
committedFunds: number;
|
||||
fees: number;
|
||||
firstOrderDate: Date;
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "Type" ADD VALUE 'DIVIDEND';
|
@ -206,5 +206,6 @@ enum Role {
|
||||
|
||||
enum Type {
|
||||
BUY
|
||||
DIVIDEND
|
||||
SELL
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
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
|
||||
|
|
Loading…
x
Reference in New Issue
Block a user