|
|
|
@ -4,18 +4,14 @@ import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-or
|
|
|
|
|
import { TimelineSpecification } from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
|
|
|
|
|
import { PortfolioCalculator } from '@ghostfolio/api/app/core/portfolio-calculator';
|
|
|
|
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
|
|
|
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
|
|
|
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
|
|
|
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
|
|
|
|
import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
|
|
|
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
|
|
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
|
|
|
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
|
|
|
|
import { IOrder, Type } from '@ghostfolio/api/services/interfaces/interfaces';
|
|
|
|
|
import { Type } from '@ghostfolio/api/services/interfaces/interfaces';
|
|
|
|
|
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
|
|
|
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
|
|
|
|
import {
|
|
|
|
|
PortfolioItem,
|
|
|
|
|
PortfolioOverview,
|
|
|
|
|
PortfolioPerformance,
|
|
|
|
|
PortfolioPosition,
|
|
|
|
@ -30,11 +26,10 @@ import {
|
|
|
|
|
} from '@ghostfolio/common/types';
|
|
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
|
|
|
import { REQUEST } from '@nestjs/core';
|
|
|
|
|
import { DataSource, Currency, Type as TypeOfOrder } from '@prisma/client';
|
|
|
|
|
import { Currency, DataSource, Type as TypeOfOrder } from '@prisma/client';
|
|
|
|
|
import Big from 'big.js';
|
|
|
|
|
import {
|
|
|
|
|
add,
|
|
|
|
|
addMonths,
|
|
|
|
|
endOfToday,
|
|
|
|
|
format,
|
|
|
|
|
getDate,
|
|
|
|
@ -42,7 +37,6 @@ import {
|
|
|
|
|
getYear,
|
|
|
|
|
isAfter,
|
|
|
|
|
isBefore,
|
|
|
|
|
isSameDay,
|
|
|
|
|
max,
|
|
|
|
|
parse,
|
|
|
|
|
parseISO,
|
|
|
|
@ -54,7 +48,6 @@ import {
|
|
|
|
|
subYears
|
|
|
|
|
} from 'date-fns';
|
|
|
|
|
import { isEmpty } from 'lodash';
|
|
|
|
|
import * as roundTo from 'round-to';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
HistoricalDataItem,
|
|
|
|
@ -83,69 +76,11 @@ export class PortfolioService {
|
|
|
|
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
|
|
|
|
private readonly impersonationService: ImpersonationService,
|
|
|
|
|
private readonly orderService: OrderService,
|
|
|
|
|
private readonly redisCacheService: RedisCacheService,
|
|
|
|
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
|
|
|
|
private readonly rulesService: RulesService,
|
|
|
|
|
private readonly userService: UserService,
|
|
|
|
|
private readonly symbolProfileService: SymbolProfileService
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
public async createPortfolio(aUserId: string): Promise<Portfolio> {
|
|
|
|
|
let portfolio: Portfolio;
|
|
|
|
|
const stringifiedPortfolio = await this.redisCacheService.get(
|
|
|
|
|
`${aUserId}.portfolio`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const user = await this.userService.user({ id: aUserId });
|
|
|
|
|
|
|
|
|
|
if (stringifiedPortfolio) {
|
|
|
|
|
// Get portfolio from redis
|
|
|
|
|
const {
|
|
|
|
|
orders,
|
|
|
|
|
portfolioItems
|
|
|
|
|
}: { orders: IOrder[]; portfolioItems: PortfolioItem[] } =
|
|
|
|
|
JSON.parse(stringifiedPortfolio);
|
|
|
|
|
|
|
|
|
|
portfolio = new Portfolio(
|
|
|
|
|
this.accountService,
|
|
|
|
|
this.dataProviderService,
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
this.rulesService
|
|
|
|
|
).createFromData({ orders, portfolioItems, user });
|
|
|
|
|
} else {
|
|
|
|
|
// Get portfolio from database
|
|
|
|
|
const orders = await this.getOrders(aUserId);
|
|
|
|
|
|
|
|
|
|
portfolio = new Portfolio(
|
|
|
|
|
this.accountService,
|
|
|
|
|
this.dataProviderService,
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
this.rulesService
|
|
|
|
|
);
|
|
|
|
|
portfolio.setUser(user);
|
|
|
|
|
await portfolio.setOrders(orders);
|
|
|
|
|
|
|
|
|
|
// Cache data for the next time...
|
|
|
|
|
const portfolioData = {
|
|
|
|
|
orders: portfolio.getOrders(),
|
|
|
|
|
portfolioItems: portfolio.getPortfolioItems()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisCacheService.set(
|
|
|
|
|
`${aUserId}.portfolio`,
|
|
|
|
|
JSON.stringify(portfolioData)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enrich portfolio with current data
|
|
|
|
|
await portfolio.addCurrentPortfolioItems();
|
|
|
|
|
|
|
|
|
|
// Enrich portfolio with future data
|
|
|
|
|
await portfolio.addFuturePortfolioItems();
|
|
|
|
|
|
|
|
|
|
return portfolio;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getInvestments(
|
|
|
|
|
aImpersonationId: string
|
|
|
|
|
): Promise<InvestmentItem[]> {
|
|
|
|
@ -603,35 +538,49 @@ export class PortfolioService {
|
|
|
|
|
|
|
|
|
|
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
|
|
|
|
const userId = await this.getUserId(impersonationId);
|
|
|
|
|
const portfolio = await this.createPortfolio(userId);
|
|
|
|
|
const baseCurrency = this.request.user.Settings.currency;
|
|
|
|
|
|
|
|
|
|
const details = await portfolio.getDetails();
|
|
|
|
|
const { orders } = await this.getTransactionPoints(userId);
|
|
|
|
|
const { transactionPoints, orders } = await this.getTransactionPoints(
|
|
|
|
|
userId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isEmpty(details)) {
|
|
|
|
|
if (isEmpty(orders)) {
|
|
|
|
|
return {
|
|
|
|
|
rules: {}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fees = this.getFees(orders);
|
|
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
|
this.currentRateService,
|
|
|
|
|
this.request.user.Settings.currency
|
|
|
|
|
);
|
|
|
|
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
|
|
|
|
|
|
|
|
|
const baseCurrency = this.request.user.Settings.currency;
|
|
|
|
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
|
|
|
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
|
|
|
|
portfolioStart
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
|
|
|
|
for (const position of currentPositions.positions) {
|
|
|
|
|
portfolioItemsNow[position.symbol] = position;
|
|
|
|
|
}
|
|
|
|
|
const accounts = this.getAccounts(orders, portfolioItemsNow, baseCurrency);
|
|
|
|
|
return {
|
|
|
|
|
rules: {
|
|
|
|
|
accountClusterRisk: await this.rulesService.evaluate(
|
|
|
|
|
[
|
|
|
|
|
new AccountClusterRiskInitialInvestment(
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
details
|
|
|
|
|
accounts
|
|
|
|
|
),
|
|
|
|
|
new AccountClusterRiskCurrentInvestment(
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
details
|
|
|
|
|
accounts
|
|
|
|
|
),
|
|
|
|
|
new AccountClusterRiskSingleAccount(
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
details
|
|
|
|
|
accounts
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
{ baseCurrency }
|
|
|
|
@ -640,19 +589,19 @@ export class PortfolioService {
|
|
|
|
|
[
|
|
|
|
|
new CurrencyClusterRiskBaseCurrencyInitialInvestment(
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
details
|
|
|
|
|
currentPositions
|
|
|
|
|
),
|
|
|
|
|
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
details
|
|
|
|
|
currentPositions
|
|
|
|
|
),
|
|
|
|
|
new CurrencyClusterRiskInitialInvestment(
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
details
|
|
|
|
|
currentPositions
|
|
|
|
|
),
|
|
|
|
|
new CurrencyClusterRiskCurrentInvestment(
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
details
|
|
|
|
|
currentPositions
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
{ baseCurrency }
|
|
|
|
@ -661,8 +610,8 @@ export class PortfolioService {
|
|
|
|
|
[
|
|
|
|
|
new FeeRatioInitialInvestment(
|
|
|
|
|
this.exchangeRateDataService,
|
|
|
|
|
details,
|
|
|
|
|
fees
|
|
|
|
|
currentPositions.totalInvestment.toNumber(),
|
|
|
|
|
this.getFees(orders)
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
{ baseCurrency }
|
|
|
|
|