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-26 23:03:22 +02:00
|
|
|
import { getReasonPhrase, StatusCodes } 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';
|
|
|
|
|
|
|
|
@Controller('portfolio')
|
|
|
|
export class PortfolioController {
|
|
|
|
public constructor(
|
|
|
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
|
|
|
private readonly impersonationService: ImpersonationService,
|
|
|
|
private portfolioService: PortfolioService,
|
|
|
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
|
|
|
) {}
|
|
|
|
|
|
|
|
@Get()
|
|
|
|
@UseGuards(AuthGuard('jwt'))
|
|
|
|
public async findAll(
|
|
|
|
@Headers('impersonation-id') impersonationId
|
|
|
|
): Promise<PortfolioItem[]> {
|
|
|
|
let portfolio = await this.portfolioService.findAll(impersonationId);
|
|
|
|
|
|
|
|
if (
|
|
|
|
impersonationId &&
|
|
|
|
!hasPermission(
|
|
|
|
getPermissions(this.request.user.role),
|
|
|
|
permissions.readForeignPortfolio
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
portfolio = portfolio.map((portfolioItem) => {
|
|
|
|
Object.keys(portfolioItem.positions).forEach((symbol) => {
|
|
|
|
portfolioItem.positions[symbol].investment =
|
|
|
|
portfolioItem.positions[symbol].investment > 0 ? 1 : 0;
|
|
|
|
portfolioItem.positions[symbol].investmentInOriginalCurrency =
|
|
|
|
portfolioItem.positions[symbol].investmentInOriginalCurrency > 0
|
|
|
|
? 1
|
|
|
|
: 0;
|
|
|
|
portfolioItem.positions[symbol].quantity =
|
|
|
|
portfolioItem.positions[symbol].quantity > 0 ? 1 : 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
portfolioItem.investment = null;
|
|
|
|
|
|
|
|
return portfolioItem;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return portfolio;
|
|
|
|
}
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
const portfolio = await this.portfolioService.createPortfolio(
|
|
|
|
impersonationUserId || this.request.user.id
|
|
|
|
);
|
|
|
|
|
|
|
|
try {
|
|
|
|
details = await portfolio.getDetails(range);
|
|
|
|
} 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-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
|
|
|
|
2021-07-27 22:46:41 +02:00
|
|
|
const performanceInformation = await this.portfolioService.getPerformance(
|
|
|
|
impersonationUserId,
|
|
|
|
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-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
|
|
|
|
|
|
|
const portfolio = await this.portfolioService.createPortfolio(
|
|
|
|
impersonationUserId || this.request.user.id
|
|
|
|
);
|
|
|
|
|
2021-06-01 21:40:32 +02:00
|
|
|
return await portfolio.getReport();
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
}
|