2021-04-21 20:27:39 +02:00
|
|
|
import {
|
|
|
|
hasNotDefinedValuesInObject,
|
|
|
|
nullifyValuesInObject
|
|
|
|
} from '@ghostfolio/api/helper/object.helper';
|
|
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
|
|
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
2021-05-16 21:20:59 +02:00
|
|
|
import {
|
|
|
|
PortfolioItem,
|
|
|
|
PortfolioOverview,
|
|
|
|
PortfolioPerformance,
|
|
|
|
PortfolioPosition,
|
|
|
|
PortfolioReport
|
2021-05-16 22:11:14 +02:00
|
|
|
} from '@ghostfolio/common/interfaces';
|
2021-05-16 21:20:59 +02:00
|
|
|
import {
|
|
|
|
getPermissions,
|
|
|
|
hasPermission,
|
|
|
|
permissions
|
2021-05-16 22:11:14 +02:00
|
|
|
} from '@ghostfolio/common/permissions';
|
|
|
|
import { RequestWithUser } from '@ghostfolio/common/types';
|
2021-04-13 21:53:58 +02:00
|
|
|
import {
|
|
|
|
Controller,
|
|
|
|
Get,
|
|
|
|
Headers,
|
|
|
|
HttpException,
|
|
|
|
Inject,
|
|
|
|
Param,
|
|
|
|
Query,
|
|
|
|
Res,
|
|
|
|
UseGuards
|
|
|
|
} from '@nestjs/common';
|
|
|
|
import { REQUEST } from '@nestjs/core';
|
|
|
|
import { AuthGuard } from '@nestjs/passport';
|
|
|
|
import { Response } from 'express';
|
2021-07-28 15:15:31 +02:00
|
|
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
import {
|
|
|
|
HistoricalDataItem,
|
|
|
|
PortfolioPositionDetail
|
|
|
|
} from './interfaces/portfolio-position-detail.interface';
|
2021-07-20 22:52:50 +02:00
|
|
|
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
|
2021-04-13 21:53:58 +02:00
|
|
|
import { PortfolioService } from './portfolio.service';
|
2021-07-31 21:33:45 +02:00
|
|
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
@Controller('portfolio')
|
|
|
|
export class PortfolioController {
|
|
|
|
public constructor(
|
|
|
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
|
|
|
private readonly impersonationService: ImpersonationService,
|
|
|
|
private portfolioService: PortfolioService,
|
|
|
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
|
|
|
) {}
|
|
|
|
|
2021-07-31 21:33:45 +02:00
|
|
|
@Get('/investments')
|
2021-04-13 21:53:58 +02:00
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async findAll(
|
|
|
|
@Headers('impersonation-id') impersonationId
|
2021-07-31 21:33:45 +02:00
|
|
|
): Promise<InvestmentItem[]> {
|
|
|
|
let investments = await this.portfolioService.getInvestments(
|
|
|
|
impersonationId
|
|
|
|
);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
if (
|
|
|
|
impersonationId &&
|
|
|
|
!hasPermission(
|
|
|
|
getPermissions(this.request.user.role),
|
|
|
|
permissions.readForeignPortfolio
|
|
|
|
)
|
|
|
|
) {
|
2021-07-31 21:33:45 +02:00
|
|
|
const maxInvestment = investments.reduce(
|
|
|
|
(investment, item) => Math.max(investment, item.investment),
|
|
|
|
1
|
|
|
|
);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-07-31 21:33:45 +02:00
|
|
|
investments = investments.map((item) => ({
|
|
|
|
date: item.date,
|
|
|
|
investment: item.investment / maxInvestment
|
|
|
|
}));
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
|
2021-07-31 21:33:45 +02:00
|
|
|
return investments;
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Get('chart')
|
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async getChart(
|
|
|
|
@Headers('impersonation-id') impersonationId,
|
|
|
|
@Query('range') range,
|
|
|
|
@Res() res: Response
|
|
|
|
): Promise<HistoricalDataItem[]> {
|
|
|
|
let chartData = await this.portfolioService.getChart(
|
|
|
|
impersonationId,
|
|
|
|
range
|
|
|
|
);
|
|
|
|
|
|
|
|
let hasNullValue = false;
|
|
|
|
|
|
|
|
chartData.forEach((chartDataItem) => {
|
|
|
|
if (hasNotDefinedValuesInObject(chartDataItem)) {
|
|
|
|
hasNullValue = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (hasNullValue) {
|
|
|
|
res.status(StatusCodes.ACCEPTED);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
impersonationId &&
|
|
|
|
!hasPermission(
|
|
|
|
getPermissions(this.request.user.role),
|
|
|
|
permissions.readForeignPortfolio
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
let maxValue = 0;
|
|
|
|
|
|
|
|
chartData.forEach((portfolioItem) => {
|
|
|
|
if (portfolioItem.value > maxValue) {
|
|
|
|
maxValue = portfolioItem.value;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
chartData = chartData.map((historicalDataItem) => {
|
|
|
|
return {
|
|
|
|
...historicalDataItem,
|
|
|
|
marketPrice: Number((historicalDataItem.value / maxValue).toFixed(2))
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return <any>res.json(chartData);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Get('details')
|
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async getDetails(
|
|
|
|
@Headers('impersonation-id') impersonationId,
|
|
|
|
@Query('range') range,
|
|
|
|
@Res() res: Response
|
|
|
|
): Promise<{ [symbol: string]: PortfolioPosition }> {
|
|
|
|
let details: { [symbol: string]: PortfolioPosition } = {};
|
|
|
|
|
2021-07-07 21:23:36 +02:00
|
|
|
const impersonationUserId =
|
|
|
|
await this.impersonationService.validateImpersonationId(
|
|
|
|
impersonationId,
|
|
|
|
this.request.user.id
|
|
|
|
);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
try {
|
2021-07-31 20:45:12 +02:00
|
|
|
details = await this.portfolioService.getDetails(
|
|
|
|
impersonationUserId,
|
|
|
|
range
|
|
|
|
);
|
2021-04-13 21:53:58 +02:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
|
|
|
|
res.status(StatusCodes.ACCEPTED);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasNotDefinedValuesInObject(details)) {
|
|
|
|
res.status(StatusCodes.ACCEPTED);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
impersonationId &&
|
|
|
|
!hasPermission(
|
|
|
|
getPermissions(this.request.user.role),
|
|
|
|
permissions.readForeignPortfolio
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
const totalInvestment = Object.values(details)
|
|
|
|
.map((portfolioPosition) => {
|
|
|
|
return portfolioPosition.investment;
|
|
|
|
})
|
|
|
|
.reduce((a, b) => a + b, 0);
|
|
|
|
|
|
|
|
const totalValue = Object.values(details)
|
|
|
|
.map((portfolioPosition) => {
|
|
|
|
return this.exchangeRateDataService.toCurrency(
|
|
|
|
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
|
|
|
portfolioPosition.currency,
|
|
|
|
this.request.user.Settings.currency
|
|
|
|
);
|
|
|
|
})
|
|
|
|
.reduce((a, b) => a + b, 0);
|
|
|
|
|
|
|
|
for (const [symbol, portfolioPosition] of Object.entries(details)) {
|
|
|
|
portfolioPosition.grossPerformance = null;
|
|
|
|
portfolioPosition.investment =
|
|
|
|
portfolioPosition.investment / totalInvestment;
|
|
|
|
|
2021-05-02 21:18:52 +02:00
|
|
|
for (const [account, { current, original }] of Object.entries(
|
|
|
|
portfolioPosition.accounts
|
2021-04-13 21:53:58 +02:00
|
|
|
)) {
|
2021-05-02 21:18:52 +02:00
|
|
|
portfolioPosition.accounts[account].current = current / totalValue;
|
|
|
|
portfolioPosition.accounts[account].original =
|
2021-04-13 21:53:58 +02:00
|
|
|
original / totalInvestment;
|
|
|
|
}
|
|
|
|
|
|
|
|
portfolioPosition.quantity = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return <any>res.json(details);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Get('overview')
|
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async getOverview(
|
|
|
|
@Headers('impersonation-id') impersonationId
|
|
|
|
): Promise<PortfolioOverview> {
|
|
|
|
let overview = await this.portfolioService.getOverview(impersonationId);
|
|
|
|
|
|
|
|
if (
|
|
|
|
impersonationId &&
|
|
|
|
!hasPermission(
|
|
|
|
getPermissions(this.request.user.role),
|
|
|
|
permissions.readForeignPortfolio
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
overview = nullifyValuesInObject(overview, [
|
2021-07-07 21:23:36 +02:00
|
|
|
'cash',
|
2021-04-13 21:53:58 +02:00
|
|
|
'committedFunds',
|
|
|
|
'fees',
|
|
|
|
'totalBuy',
|
|
|
|
'totalSell'
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return overview;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Get('performance')
|
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async getPerformance(
|
|
|
|
@Headers('impersonation-id') impersonationId,
|
|
|
|
@Query('range') range,
|
|
|
|
@Res() res: Response
|
|
|
|
): Promise<PortfolioPerformance> {
|
2021-07-27 22:46:41 +02:00
|
|
|
const performanceInformation = await this.portfolioService.getPerformance(
|
2021-07-31 22:46:39 +02:00
|
|
|
impersonationId,
|
2021-07-27 22:46:41 +02:00
|
|
|
range
|
2021-04-13 21:53:58 +02:00
|
|
|
);
|
|
|
|
|
2021-07-27 22:54:00 +02:00
|
|
|
if (performanceInformation?.hasErrors) {
|
|
|
|
res.status(StatusCodes.ACCEPTED);
|
|
|
|
}
|
|
|
|
|
2021-07-27 22:46:41 +02:00
|
|
|
let performance = performanceInformation.performance;
|
2021-04-13 21:53:58 +02:00
|
|
|
if (
|
|
|
|
impersonationId &&
|
|
|
|
!hasPermission(
|
|
|
|
getPermissions(this.request.user.role),
|
|
|
|
permissions.readForeignPortfolio
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
performance = nullifyValuesInObject(performance, [
|
|
|
|
'currentGrossPerformance',
|
|
|
|
'currentNetPerformance',
|
|
|
|
'currentValue'
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return <any>res.json(performance);
|
|
|
|
}
|
|
|
|
|
2021-07-20 22:52:50 +02:00
|
|
|
@Get('positions')
|
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async getPositions(
|
2021-07-24 10:53:15 +02:00
|
|
|
@Headers('impersonation-id') impersonationId,
|
2021-07-27 12:01:35 +02:00
|
|
|
@Query('range') range,
|
|
|
|
@Res() res: Response
|
2021-07-20 22:52:50 +02:00
|
|
|
): Promise<PortfolioPositions> {
|
2021-07-27 12:01:35 +02:00
|
|
|
const result = await this.portfolioService.getPositions(
|
|
|
|
impersonationId,
|
|
|
|
range
|
|
|
|
);
|
|
|
|
|
|
|
|
if (result?.hasErrors) {
|
|
|
|
res.status(StatusCodes.ACCEPTED);
|
|
|
|
}
|
|
|
|
|
|
|
|
return <any>res.json(result);
|
2021-07-20 22:52:50 +02:00
|
|
|
}
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
@Get('position/:symbol')
|
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async getPosition(
|
|
|
|
@Headers('impersonation-id') impersonationId,
|
|
|
|
@Param('symbol') symbol
|
|
|
|
): Promise<PortfolioPositionDetail> {
|
|
|
|
let position = await this.portfolioService.getPosition(
|
|
|
|
impersonationId,
|
|
|
|
symbol
|
|
|
|
);
|
|
|
|
|
|
|
|
if (position) {
|
|
|
|
if (
|
|
|
|
impersonationId &&
|
|
|
|
!hasPermission(
|
|
|
|
getPermissions(this.request.user.role),
|
|
|
|
permissions.readForeignPortfolio
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
position = nullifyValuesInObject(position, ['grossPerformance']);
|
|
|
|
}
|
|
|
|
|
|
|
|
return position;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new HttpException(
|
|
|
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
|
|
|
StatusCodes.NOT_FOUND
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Get('report')
|
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async getReport(
|
|
|
|
@Headers('impersonation-id') impersonationId
|
|
|
|
): Promise<PortfolioReport> {
|
2021-07-31 23:33:50 +02:00
|
|
|
return await this.portfolioService.getReport(impersonationId);
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
}
|