Initial implementation

pull/187/head
Thomas 4 years ago
parent f32bef071e
commit 6355fdb3ba

@ -43,6 +43,7 @@ export class ExperimentalService {
date: parseISO(order.date),
fee: 0,
id: undefined,
isDraft: false,
platformId: undefined,
symbolProfileId: undefined,
type: Type.BUY,

@ -22,7 +22,7 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Order as OrderModel } from '@prisma/client';
import { parseISO } from 'date-fns';
import { endOfToday, isAfter, parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { CreateOrderDto } from './create-order.dto';
@ -129,6 +129,8 @@ export class OrderController {
const accountId = data.accountId;
delete data.accountId;
const isDraft = isAfter(date, endOfToday());
return this.orderService.createOrder(
{
...data,
@ -138,6 +140,7 @@ export class OrderController {
}
},
date,
isDraft,
SymbolProfile: {
connectOrCreate: {
where: {
@ -192,11 +195,14 @@ export class OrderController {
const accountId = data.accountId;
delete data.accountId;
const isDraft = isAfter(date, endOfToday());
return this.orderService.updateOrder(
{
data: {
...data,
date,
isDraft,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }

@ -14,11 +14,13 @@ import { REQUEST } from '@nestjs/core';
import { DataSource } from '@prisma/client';
import {
add,
endOfToday,
format,
getDate,
getMonth,
getYear,
isAfter,
isBefore,
isSameDay,
parse,
parseISO,
@ -26,6 +28,7 @@ import {
setMonth,
sub
} from 'date-fns';
import { port } from 'envalid';
import { isEmpty } from 'lodash';
import * as roundTo from 'round-to';
@ -52,7 +55,7 @@ export class PortfolioService {
public async createPortfolio(aUserId: string): Promise<Portfolio> {
let portfolio: Portfolio;
let stringifiedPortfolio = await this.redisCacheService.get(
const stringifiedPortfolio = await this.redisCacheService.get(
`${aUserId}.portfolio`
);
@ -63,9 +66,8 @@ export class PortfolioService {
const {
orders,
portfolioItems
}: { orders: IOrder[]; portfolioItems: PortfolioItem[] } = JSON.parse(
stringifiedPortfolio
);
}: { orders: IOrder[]; portfolioItems: PortfolioItem[] } =
JSON.parse(stringifiedPortfolio);
portfolio = new Portfolio(
this.dataProviderService,
@ -104,15 +106,21 @@ export class PortfolioService {
}
// Enrich portfolio with current data
return await portfolio.addCurrentPortfolioItems();
await portfolio.addCurrentPortfolioItems();
// Enrich portfolio with future data
await portfolio.addFuturePortfolioItems();
return portfolio;
}
public async findAll(aImpersonationId: string): Promise<PortfolioItem[]> {
try {
const impersonationUserId = await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const portfolio = await this.createPortfolio(
impersonationUserId || this.request.user.id
@ -127,10 +135,11 @@ export class PortfolioService {
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<HistoricalDataItem[]> {
const impersonationUserId = await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const portfolio = await this.createPortfolio(
impersonationUserId || this.request.user.id
@ -148,6 +157,11 @@ export class PortfolioService {
return portfolio
.get()
.filter((portfolioItem) => {
if (isAfter(parseISO(portfolioItem.date), endOfToday())) {
// Filter out future dates
return false;
}
if (dateRangeDate === undefined) {
return true;
}
@ -170,10 +184,11 @@ export class PortfolioService {
public async getOverview(
aImpersonationId: string
): Promise<PortfolioOverview> {
const impersonationUserId = await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const portfolio = await this.createPortfolio(
impersonationUserId || this.request.user.id
@ -195,10 +210,11 @@ export class PortfolioService {
aImpersonationId: string,
aSymbol: string
): Promise<PortfolioPositionDetail> {
const impersonationUserId = await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId,
this.request.user.id
);
const portfolio = await this.createPortfolio(
impersonationUserId || this.request.user.id

@ -10,6 +10,7 @@ export class Order {
private fee: number;
private date: string;
private id: string;
private isDraft: boolean;
private quantity: number;
private symbol: string;
private symbolProfile: SymbolProfile;
@ -23,6 +24,7 @@ export class Order {
this.fee = data.fee;
this.date = data.date;
this.id = data.id || uuidv4();
this.isDraft = data.isDraft ?? false;
this.quantity = data.quantity;
this.symbol = data.symbol;
this.symbolProfile = data.symbolProfile;
@ -52,6 +54,10 @@ export class Order {
return this.id;
}
public getIsDraft() {
return this.isDraft;
}
public getQuantity() {
return this.quantity;
}

@ -73,7 +73,7 @@ export class Portfolio implements PortfolioInterface {
const [portfolioItemsYesterday] = this.get(yesterday);
let positions: { [symbol: string]: Position } = {};
const positions: { [symbol: string]: Position } = {};
this.getSymbols().forEach((symbol) => {
positions[symbol] = {
@ -105,14 +105,49 @@ export class Portfolio implements PortfolioInterface {
);
// Set value after pushing today's portfolio items
this.portfolioItems[portfolioItemsLength - 1].value = this.getValue(
today
);
this.portfolioItems[portfolioItemsLength - 1].value =
this.getValue(today);
}
return this;
}
public async addFuturePortfolioItems() {
let investment = this.getInvestment(new Date());
this.getOrders()
.filter((order) => order.getIsDraft() === true)
.forEach((order) => {
const portfolioItem = this.portfolioItems.find((item) => {
return item.date === order.getDate();
});
if (portfolioItem) {
portfolioItem.investment += this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
} else {
investment += this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
this.portfolioItems.push({
investment,
date: order.getDate(),
grossPerformancePercent: 0,
positions: {},
value: 0
});
}
});
return this;
}
public createFromData({
orders,
portfolioItems,
@ -129,6 +164,7 @@ export class Portfolio implements PortfolioInterface {
fee,
date,
id,
isDraft,
quantity,
symbol,
symbolProfile,
@ -142,6 +178,7 @@ export class Portfolio implements PortfolioInterface {
fee,
date,
id,
isDraft,
quantity,
symbol,
symbolProfile,
@ -178,9 +215,12 @@ export class Portfolio implements PortfolioInterface {
if (filteredPortfolio) {
return [cloneDeep(filteredPortfolio)];
}
return [];
}
return cloneDeep(this.portfolioItems);
// return [];
}
public getCommittedFunds() {
@ -239,12 +279,10 @@ export class Portfolio implements PortfolioInterface {
if (
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY]?.current
) {
accounts[
orderOfSymbol.getAccount()?.name || UNKNOWN_KEY
].current += currentValueOfSymbol;
accounts[
orderOfSymbol.getAccount()?.name || UNKNOWN_KEY
].original += originalValueOfSymbol;
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY].current +=
currentValueOfSymbol;
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY].original +=
originalValueOfSymbol;
} else {
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY] = {
current: currentValueOfSymbol,
@ -365,7 +403,11 @@ export class Portfolio implements PortfolioInterface {
}
public getMinDate() {
if (this.orders.length > 0) {
const orders = this.getOrders().filter(
(order) => order.getIsDraft() === false
);
if (orders.length > 0) {
return new Date(this.orders[0].getDate());
}
@ -492,9 +534,11 @@ export class Portfolio implements PortfolioInterface {
}
}
} else {
symbols = this.orders.map((order) => {
return order.getSymbol();
});
symbols = this.orders
.filter((order) => order.getIsDraft() === false)
.map((order) => {
return order.getSymbol();
});
}
// unique values
@ -503,7 +547,9 @@ export class Portfolio implements PortfolioInterface {
public getTotalBuy() {
return this.orders
.filter((order) => order.getType() === 'BUY')
.filter(
(order) => order.getIsDraft() === false && order.getType() === 'BUY'
)
.map((order) => {
return this.exchangeRateDataService.toCurrency(
order.getTotal(),
@ -516,7 +562,9 @@ export class Portfolio implements PortfolioInterface {
public getTotalSell() {
return this.orders
.filter((order) => order.getType() === 'SELL')
.filter(
(order) => order.getIsDraft() === false && order.getType() === 'SELL'
)
.map((order) => {
return this.exchangeRateDataService.toCurrency(
order.getTotal(),
@ -583,6 +631,7 @@ export class Portfolio implements PortfolioInterface {
currency: order.currency,
date: order.date.toISOString(),
fee: order.fee,
isDraft: order.isDraft,
quantity: order.quantity,
symbol: order.symbol,
symbolProfile: order.SymbolProfile,
@ -686,10 +735,10 @@ export class Portfolio implements PortfolioInterface {
this.portfolioItems.push(
cloneDeep({
positions,
date: yesterday.toISOString(),
grossPerformancePercent: 0,
investment: 0,
positions: positions,
value: 0
})
);
@ -746,8 +795,6 @@ export class Portfolio implements PortfolioInterface {
}
private updatePortfolioItems() {
// console.time('update-portfolio-items');
let currentDate = new Date();
const year = getYear(currentDate);
@ -771,107 +818,99 @@ export class Portfolio implements PortfolioInterface {
}
this.orders.forEach((order) => {
let index = this.portfolioItems.findIndex((item) => {
const dateOfOrder = setDate(parseISO(order.getDate()), 1);
return isSameDay(parseISO(item.date), dateOfOrder);
});
if (index === -1) {
// if not found, we only have one order, which means we do not loop below
index = 0;
}
for (let i = index; i < this.portfolioItems.length; i++) {
// Set currency
this.portfolioItems[i].positions[
order.getSymbol()
].currency = order.getCurrency();
if (order.getIsDraft() === false) {
let index = this.portfolioItems.findIndex((item) => {
const dateOfOrder = setDate(parseISO(order.getDate()), 1);
return isSameDay(parseISO(item.date), dateOfOrder);
});
this.portfolioItems[i].positions[
order.getSymbol()
].transactionCount += 1;
if (index === -1) {
// if not found, we only have one order, which means we do not loop below
index = 0;
}
if (order.getType() === 'BUY') {
if (
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate
) {
this.portfolioItems[i].positions[
order.getSymbol()
].firstBuyDate = resetHours(
parseISO(order.getDate())
).toISOString();
}
for (let i = index; i < this.portfolioItems.length; i++) {
// Set currency
this.portfolioItems[i].positions[order.getSymbol()].currency =
order.getCurrency();
this.portfolioItems[i].positions[
order.getSymbol()
].quantity += order.getQuantity();
this.portfolioItems[i].positions[
order.getSymbol()
].investment += this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
this.portfolioItems[i].positions[
order.getSymbol()
].investmentInOriginalCurrency += order.getTotal();
this.portfolioItems[
i
].investment += this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
} else if (order.getType() === 'SELL') {
this.portfolioItems[i].positions[
order.getSymbol()
].quantity -= order.getQuantity();
if (
this.portfolioItems[i].positions[order.getSymbol()].quantity === 0
) {
this.portfolioItems[i].positions[order.getSymbol()].investment = 0;
this.portfolioItems[i].positions[
order.getSymbol()
].investmentInOriginalCurrency = 0;
} else {
].transactionCount += 1;
if (order.getType() === 'BUY') {
if (
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate
) {
this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate =
resetHours(parseISO(order.getDate())).toISOString();
}
this.portfolioItems[i].positions[order.getSymbol()].quantity +=
order.getQuantity();
this.portfolioItems[i].positions[order.getSymbol()].investment +=
this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
this.portfolioItems[i].positions[
order.getSymbol()
].investment -= this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
this.portfolioItems[i].positions[
order.getSymbol()
].investmentInOriginalCurrency -= order.getTotal();
].investmentInOriginalCurrency += order.getTotal();
this.portfolioItems[i].investment +=
this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
} else if (order.getType() === 'SELL') {
this.portfolioItems[i].positions[order.getSymbol()].quantity -=
order.getQuantity();
if (
this.portfolioItems[i].positions[order.getSymbol()].quantity === 0
) {
this.portfolioItems[i].positions[
order.getSymbol()
].investment = 0;
this.portfolioItems[i].positions[
order.getSymbol()
].investmentInOriginalCurrency = 0;
} else {
this.portfolioItems[i].positions[order.getSymbol()].investment -=
this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
this.portfolioItems[i].positions[
order.getSymbol()
].investmentInOriginalCurrency -= order.getTotal();
}
this.portfolioItems[i].investment -=
this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
}
this.portfolioItems[
i
].investment -= this.exchangeRateDataService.toCurrency(
order.getTotal(),
order.getCurrency(),
this.user.Settings.currency
);
}
this.portfolioItems[i].positions[order.getSymbol()].averagePrice =
this.portfolioItems[i].positions[order.getSymbol()]
.investmentInOriginalCurrency /
this.portfolioItems[i].positions[order.getSymbol()].quantity;
this.portfolioItems[i].positions[order.getSymbol()].averagePrice =
this.portfolioItems[i].positions[order.getSymbol()]
.investmentInOriginalCurrency /
this.portfolioItems[i].positions[order.getSymbol()].quantity;
const currentValue = this.getValue(
parseISO(this.portfolioItems[i].date)
);
const currentValue = this.getValue(
parseISO(this.portfolioItems[i].date)
);
this.portfolioItems[i].grossPerformancePercent =
currentValue / this.portfolioItems[i].investment - 1 || 0;
this.portfolioItems[i].value = currentValue;
this.portfolioItems[i].grossPerformancePercent =
currentValue / this.portfolioItems[i].investment - 1 || 0;
this.portfolioItems[i].value = currentValue;
}
}
});
// console.timeEnd('update-portfolio-items');
}
}

@ -224,7 +224,10 @@ export class DataGatheringService {
const distinctOrders = await this.prisma.order.findMany({
distinct: ['symbol'],
orderBy: [{ symbol: 'asc' }],
select: { dataSource: true, symbol: true }
select: { dataSource: true, symbol: true },
where: {
isDraft: false
}
});
const distinctOrdersWithDate: IDataGatheringItem[] = distinctOrders
@ -280,7 +283,10 @@ export class DataGatheringService {
const distinctOrders = await this.prisma.order.findMany({
distinct: ['symbol'],
orderBy: [{ date: 'asc' }],
select: { dataSource: true, date: true, symbol: true }
select: { dataSource: true, date: true, symbol: true },
where: {
isDraft: false
}
});
return [

@ -22,6 +22,7 @@ export interface IOrder {
date: string;
fee: number;
id?: string;
isDraft: boolean;
quantity: number;
symbol: string;
symbolProfile: SymbolProfile;

@ -19,6 +19,7 @@ import {
TimeScale
} from 'chart.js';
import { Chart } from 'chart.js';
import { addMonths, parseISO, subMonths } from 'date-fns';
@Component({
selector: 'gf-investment-chart',
@ -52,9 +53,30 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
}
}
public ngOnDestroy() {
this.chart?.destroy();
}
private initialize() {
this.isLoading = true;
if (this.portfolioItems?.length > 0) {
// Extend chart by three months (before)
const firstItem = this.portfolioItems[0];
this.portfolioItems.unshift({
...firstItem,
date: subMonths(parseISO(firstItem.date), 3).toISOString(),
investment: 0
});
// Extend chart by three months (after)
const lastItem = this.portfolioItems[this.portfolioItems.length - 1];
this.portfolioItems.push({
...lastItem,
date: addMonths(parseISO(lastItem.date), 3).toISOString()
});
}
const data = {
labels: this.portfolioItems.map((position) => {
return position.date;
@ -122,8 +144,4 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
}
}
}
public ngOnDestroy() {
this.chart?.destroy();
}
}

@ -100,24 +100,27 @@
Symbol
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
{{ element.symbol | gfSymbol }}
<div class="d-flex align-items-center">
{{ element.symbol | gfSymbol }}
<span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
>Draft</span
>
</div>
</td>
</ng-container>
<ng-container matColumnDef="currency">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-center px-1"
mat-header-cell
class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell
mat-sort-header
>
Currency
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-center">
{{ element.currency }}
</div>
{{ element.currency }}
</td>
</ng-container>

@ -27,7 +27,7 @@
// @import '~bootstrap/scss/card';
// @import '~bootstrap/scss/breadcrumb';
// @import '~bootstrap/scss/pagination';
// @import '~bootstrap/scss/badge';
@import '~bootstrap/scss/badge';
// @import '~bootstrap/scss/jumbotron';
// @import '~bootstrap/scss/alert';
// @import '~bootstrap/scss/progress';

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;

@ -79,6 +79,7 @@ model Order {
date DateTime
fee Float
id String @default(uuid())
isDraft Boolean @default(false)
quantity Float
symbol String
SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id])

Loading…
Cancel
Save