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
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -330,6 +330,7 @@ export class PortfolioController {
|
|||||||
'currentGrossPerformance',
|
'currentGrossPerformance',
|
||||||
'currentNetPerformance',
|
'currentNetPerformance',
|
||||||
'currentValue',
|
'currentValue',
|
||||||
|
'dividend',
|
||||||
'fees',
|
'fees',
|
||||||
'netWorth',
|
'netWorth',
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
|
@ -401,17 +401,21 @@ 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
|
||||||
currency: order.currency,
|
.filter((order) => {
|
||||||
dataSource: order.dataSource,
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
date: format(order.date, DATE_FORMAT),
|
})
|
||||||
fee: new Big(order.fee),
|
.map((order) => ({
|
||||||
name: order.SymbolProfile?.name,
|
currency: order.currency,
|
||||||
quantity: new Big(order.quantity),
|
dataSource: order.dataSource,
|
||||||
symbol: order.symbol,
|
date: format(order.date, DATE_FORMAT),
|
||||||
type: order.type,
|
fee: new Big(order.fee),
|
||||||
unitPrice: new Big(order.unitPrice)
|
name: order.SymbolProfile?.name,
|
||||||
}));
|
quantity: new Big(order.quantity),
|
||||||
|
symbol: order.symbol,
|
||||||
|
type: order.type,
|
||||||
|
unitPrice: new Big(order.unitPrice)
|
||||||
|
}));
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
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> {
|
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: [] };
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
"
|
"
|
||||||
|
@ -37,6 +37,10 @@
|
|||||||
color: var(--green);
|
color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.dividend {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
&.sell {
|
&.sell {
|
||||||
color: var(--orange);
|
color: var(--orange);
|
||||||
}
|
}
|
||||||
|
@ -52,8 +52,9 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<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="SELL" i18n> SELL </mat-option>
|
<mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option>
|
||||||
|
<mat-option value="SELL" i18n>SELL</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -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"
|
||||||
|
@ -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()) {
|
||||||
return Type.BUY;
|
case 'buy':
|
||||||
} else if (item[key].toLowerCase() === 'sell') {
|
return Type.BUY;
|
||||||
return Type.SELL;
|
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 {
|
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;
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "Type" ADD VALUE 'DIVIDEND';
|
@ -206,5 +206,6 @@ enum Role {
|
|||||||
|
|
||||||
enum Type {
|
enum Type {
|
||||||
BUY
|
BUY
|
||||||
|
DIVIDEND
|
||||||
SELL
|
SELL
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user