2021-07-07 21:23:36 +02:00
|
|
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
2021-08-09 21:26:41 +02:00
|
|
|
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
2021-07-07 21:23:36 +02:00
|
|
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
2021-08-14 16:55:40 +02:00
|
|
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
|
|
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
|
|
|
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
|
|
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
|
|
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator';
|
2021-08-01 09:41:44 +02:00
|
|
|
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
|
|
|
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
|
|
|
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
|
|
|
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
|
|
|
|
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-initial-investment';
|
|
|
|
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
|
|
|
|
import { CurrencyClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/initial-investment';
|
|
|
|
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
|
2021-08-14 16:55:40 +02:00
|
|
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
2021-04-21 20:27:39 +02:00
|
|
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
|
|
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
2021-08-09 21:26:41 +02:00
|
|
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
2021-08-01 09:41:44 +02:00
|
|
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
|
|
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
2021-12-19 16:52:35 +01:00
|
|
|
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
2021-07-28 16:11:19 +02:00
|
|
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
2021-05-16 21:20:59 +02:00
|
|
|
import {
|
2021-11-14 17:04:52 +01:00
|
|
|
Accounts,
|
2021-08-21 15:03:55 +02:00
|
|
|
PortfolioDetails,
|
2021-07-27 22:46:41 +02:00
|
|
|
PortfolioPerformance,
|
2021-07-31 23:33:50 +02:00
|
|
|
PortfolioReport,
|
2021-08-13 19:26:48 +02:00
|
|
|
PortfolioSummary,
|
2021-07-31 20:45:12 +02:00
|
|
|
Position,
|
|
|
|
TimelinePosition
|
2021-05-16 22:11:14 +02:00
|
|
|
} from '@ghostfolio/common/interfaces';
|
2021-08-01 09:41:44 +02:00
|
|
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
2021-09-11 09:27:22 +02:00
|
|
|
import type {
|
2021-11-13 20:38:29 +01:00
|
|
|
AccountWithValue,
|
2021-07-31 20:45:12 +02:00
|
|
|
DateRange,
|
|
|
|
OrderWithAccount,
|
|
|
|
RequestWithUser
|
|
|
|
} from '@ghostfolio/common/types';
|
2021-04-13 21:53:58 +02:00
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
|
|
import { REQUEST } from '@nestjs/core';
|
2021-09-24 21:09:48 +02:00
|
|
|
import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client';
|
2021-07-09 21:35:06 +02:00
|
|
|
import Big from 'big.js';
|
2021-04-13 21:53:58 +02:00
|
|
|
import {
|
2021-07-31 21:07:04 +02:00
|
|
|
endOfToday,
|
2021-04-13 21:53:58 +02:00
|
|
|
format,
|
|
|
|
isAfter,
|
2021-07-31 21:07:04 +02:00
|
|
|
isBefore,
|
2021-07-07 23:29:37 +02:00
|
|
|
max,
|
2021-06-05 17:17:53 +02:00
|
|
|
parse,
|
2021-04-13 21:53:58 +02:00
|
|
|
parseISO,
|
2021-07-07 23:29:37 +02:00
|
|
|
setDayOfYear,
|
2021-11-22 21:28:32 +01:00
|
|
|
startOfDay,
|
2021-07-07 23:29:37 +02:00
|
|
|
subDays,
|
|
|
|
subYears
|
2021-04-13 21:53:58 +02:00
|
|
|
} from 'date-fns';
|
2021-09-24 21:09:48 +02:00
|
|
|
import { isEmpty } from 'lodash';
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
import {
|
2021-11-22 21:28:32 +01:00
|
|
|
HistoricalDataContainer,
|
2021-04-13 21:53:58 +02:00
|
|
|
HistoricalDataItem,
|
|
|
|
PortfolioPositionDetail
|
|
|
|
} from './interfaces/portfolio-position-detail.interface';
|
2021-08-14 16:55:40 +02:00
|
|
|
import { RulesService } from './rules.service';
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class PortfolioService {
|
|
|
|
public constructor(
|
2021-07-07 21:23:36 +02:00
|
|
|
private readonly accountService: AccountService,
|
2021-07-28 16:11:19 +02:00
|
|
|
private readonly currentRateService: CurrentRateService,
|
2021-04-13 21:53:58 +02:00
|
|
|
private readonly dataProviderService: DataProviderService,
|
|
|
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
|
|
|
private readonly impersonationService: ImpersonationService,
|
|
|
|
private readonly orderService: OrderService,
|
|
|
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
|
|
|
private readonly rulesService: RulesService,
|
2021-07-31 20:45:12 +02:00
|
|
|
private readonly symbolProfileService: SymbolProfileService
|
2021-04-13 21:53:58 +02:00
|
|
|
) {}
|
|
|
|
|
2021-11-13 20:38:29 +01:00
|
|
|
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
|
|
|
|
const [accounts, details] = await Promise.all([
|
|
|
|
this.accountService.accounts({
|
|
|
|
include: { Order: true, Platform: true },
|
|
|
|
orderBy: { name: 'asc' },
|
|
|
|
where: { userId: aUserId }
|
|
|
|
}),
|
|
|
|
this.getDetails(aUserId, aUserId)
|
|
|
|
]);
|
|
|
|
|
|
|
|
const userCurrency = this.request.user.Settings.currency;
|
|
|
|
|
|
|
|
return accounts.map((account) => {
|
2021-11-14 19:06:54 +01:00
|
|
|
let transactionCount = 0;
|
|
|
|
|
|
|
|
for (const order of account.Order) {
|
|
|
|
if (!order.isDraft) {
|
|
|
|
transactionCount += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-13 20:38:29 +01:00
|
|
|
const result = {
|
|
|
|
...account,
|
2021-11-14 19:06:54 +01:00
|
|
|
transactionCount,
|
2021-11-13 20:38:29 +01:00
|
|
|
convertedBalance: this.exchangeRateDataService.toCurrency(
|
|
|
|
account.balance,
|
|
|
|
account.currency,
|
|
|
|
userCurrency
|
|
|
|
),
|
2021-11-14 17:04:52 +01:00
|
|
|
value: details.accounts[account.name]?.current ?? 0
|
2021-11-13 20:38:29 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
delete result.Order;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-11-14 17:04:52 +01:00
|
|
|
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
|
|
|
|
const accounts = await this.getAccounts(aUserId);
|
|
|
|
let totalBalance = 0;
|
|
|
|
let totalValue = 0;
|
|
|
|
let transactionCount = 0;
|
|
|
|
|
|
|
|
for (const account of accounts) {
|
|
|
|
totalBalance += account.convertedBalance;
|
|
|
|
totalValue += account.value;
|
|
|
|
transactionCount += account.transactionCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { accounts, totalBalance, totalValue, transactionCount };
|
|
|
|
}
|
|
|
|
|
2021-07-31 21:33:45 +02:00
|
|
|
public async getInvestments(
|
|
|
|
aImpersonationId: string
|
|
|
|
): Promise<InvestmentItem[]> {
|
2021-10-19 18:27:50 +02:00
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-07-31 21:33:45 +02:00
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
this.currentRateService,
|
|
|
|
this.request.user.Settings.currency
|
|
|
|
);
|
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
const { transactionPoints } = await this.getTransactionPoints({
|
|
|
|
userId,
|
|
|
|
includeDrafts: true
|
|
|
|
});
|
2021-07-31 21:33:45 +02:00
|
|
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
|
|
|
if (transactionPoints.length === 0) {
|
|
|
|
return [];
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
2021-07-31 21:33:45 +02:00
|
|
|
|
|
|
|
return portfolioCalculator.getInvestments().map((item) => {
|
|
|
|
return {
|
|
|
|
date: item.date,
|
|
|
|
investment: item.investment.toNumber()
|
|
|
|
};
|
|
|
|
});
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async getChart(
|
|
|
|
aImpersonationId: string,
|
|
|
|
aDateRange: DateRange = 'max'
|
2021-11-22 21:28:32 +01:00
|
|
|
): Promise<HistoricalDataContainer> {
|
2021-10-19 18:27:50 +02:00
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-07-07 23:29:37 +02:00
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
this.currentRateService,
|
|
|
|
this.request.user.Settings.currency
|
|
|
|
);
|
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
const { transactionPoints } = await this.getTransactionPoints({ userId });
|
2021-07-20 22:52:50 +02:00
|
|
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
2021-07-07 23:29:37 +02:00
|
|
|
if (transactionPoints.length === 0) {
|
2021-11-22 21:28:32 +01:00
|
|
|
return {
|
|
|
|
isAllTimeHigh: false,
|
|
|
|
isAllTimeLow: false,
|
|
|
|
items: []
|
|
|
|
};
|
2021-07-07 23:29:37 +02:00
|
|
|
}
|
|
|
|
let portfolioStart = parse(
|
|
|
|
transactionPoints[0].date,
|
2021-07-28 16:11:19 +02:00
|
|
|
DATE_FORMAT,
|
2021-07-07 23:29:37 +02:00
|
|
|
new Date()
|
|
|
|
);
|
2021-11-22 21:28:32 +01:00
|
|
|
|
|
|
|
// Get start date for the full portfolio because of because of the
|
|
|
|
// min and max calculation
|
|
|
|
portfolioStart = this.getStartDate('max', portfolioStart);
|
2021-07-07 23:29:37 +02:00
|
|
|
|
|
|
|
const timelineSpecification: TimelineSpecification[] = [
|
|
|
|
{
|
2021-07-28 16:11:19 +02:00
|
|
|
start: format(portfolioStart, DATE_FORMAT),
|
2021-07-07 23:29:37 +02:00
|
|
|
accuracy: 'day'
|
|
|
|
}
|
|
|
|
];
|
|
|
|
|
2021-11-22 21:28:32 +01:00
|
|
|
const timelineInfo = await portfolioCalculator.calculateTimeline(
|
2021-07-07 23:29:37 +02:00
|
|
|
timelineSpecification,
|
2021-07-28 16:11:19 +02:00
|
|
|
format(new Date(), DATE_FORMAT)
|
2021-07-07 23:29:37 +02:00
|
|
|
);
|
|
|
|
|
2021-11-22 21:28:32 +01:00
|
|
|
const timeline = timelineInfo.timelinePeriods;
|
|
|
|
|
|
|
|
const items = timeline
|
2021-07-13 22:51:32 +02:00
|
|
|
.filter((timelineItem) => timelineItem !== null)
|
|
|
|
.map((timelineItem) => ({
|
|
|
|
date: timelineItem.date,
|
2021-07-28 16:11:19 +02:00
|
|
|
marketPrice: timelineItem.value,
|
2021-09-05 21:21:22 +02:00
|
|
|
value: timelineItem.netPerformance.toNumber()
|
2021-07-13 22:51:32 +02:00
|
|
|
}));
|
2021-11-22 21:28:32 +01:00
|
|
|
|
|
|
|
let lastItem = null;
|
|
|
|
if (timeline.length > 0) {
|
|
|
|
lastItem = timeline[timeline.length - 1];
|
|
|
|
}
|
|
|
|
|
|
|
|
let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq(
|
|
|
|
lastItem?.netPerformance
|
|
|
|
);
|
|
|
|
let isAllTimeLow = timelineInfo.minNetPerformance?.eq(
|
|
|
|
lastItem?.netPerformance
|
|
|
|
);
|
|
|
|
if (isAllTimeHigh && isAllTimeLow) {
|
|
|
|
isAllTimeHigh = false;
|
|
|
|
isAllTimeLow = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
portfolioStart = startOfDay(
|
|
|
|
this.getStartDate(
|
|
|
|
aDateRange,
|
|
|
|
parse(transactionPoints[0].date, DATE_FORMAT, new Date())
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
isAllTimeHigh,
|
|
|
|
isAllTimeLow,
|
|
|
|
items: items.filter((item) => {
|
|
|
|
// Filter items of date range
|
|
|
|
return !isAfter(portfolioStart, parseDate(item.date));
|
|
|
|
})
|
|
|
|
};
|
2021-07-07 23:29:37 +02:00
|
|
|
}
|
|
|
|
|
2021-07-31 20:45:12 +02:00
|
|
|
public async getDetails(
|
|
|
|
aImpersonationId: string,
|
2021-10-19 18:27:50 +02:00
|
|
|
aUserId: string,
|
2021-07-31 20:45:12 +02:00
|
|
|
aDateRange: DateRange = 'max'
|
2021-08-21 15:03:55 +02:00
|
|
|
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
2021-10-19 18:27:50 +02:00
|
|
|
const userId = await this.getUserId(aImpersonationId, aUserId);
|
2021-07-31 20:45:12 +02:00
|
|
|
|
2021-10-19 18:27:50 +02:00
|
|
|
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
2021-07-31 20:45:12 +02:00
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
this.currentRateService,
|
|
|
|
userCurrency
|
|
|
|
);
|
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
const { orders, transactionPoints } = await this.getTransactionPoints({
|
2021-07-31 20:45:12 +02:00
|
|
|
userId
|
2021-08-07 20:52:55 +02:00
|
|
|
});
|
2021-07-31 20:45:12 +02:00
|
|
|
|
|
|
|
if (transactionPoints?.length <= 0) {
|
2021-08-21 15:03:55 +02:00
|
|
|
return { accounts: {}, holdings: {}, hasErrors: false };
|
2021-07-31 20:45:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
|
|
|
|
|
|
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
|
|
|
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
|
|
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
|
|
|
startDate
|
|
|
|
);
|
|
|
|
|
2021-08-09 21:26:41 +02:00
|
|
|
const cashDetails = await this.accountService.getCashDetails(
|
|
|
|
userId,
|
|
|
|
userCurrency
|
|
|
|
);
|
|
|
|
|
2021-08-21 15:03:55 +02:00
|
|
|
const holdings: PortfolioDetails['holdings'] = {};
|
2021-08-09 21:26:41 +02:00
|
|
|
const totalInvestment = currentPositions.totalInvestment.plus(
|
|
|
|
cashDetails.balance
|
|
|
|
);
|
|
|
|
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
|
2021-07-31 20:45:12 +02:00
|
|
|
|
2021-09-18 19:32:22 +02:00
|
|
|
const dataGatheringItems = currentPositions.positions.map((position) => {
|
|
|
|
return {
|
|
|
|
dataSource: position.dataSource,
|
|
|
|
symbol: position.symbol
|
|
|
|
};
|
|
|
|
});
|
2021-07-31 20:45:12 +02:00
|
|
|
const symbols = currentPositions.positions.map(
|
|
|
|
(position) => position.symbol
|
|
|
|
);
|
|
|
|
|
|
|
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
2021-09-18 19:32:22 +02:00
|
|
|
this.dataProviderService.get(dataGatheringItems),
|
2021-07-31 20:45:12 +02:00
|
|
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
|
|
|
]);
|
|
|
|
|
|
|
|
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
|
|
|
for (const symbolProfile of symbolProfiles) {
|
|
|
|
symbolProfileMap[symbolProfile.symbol] = symbolProfile;
|
|
|
|
}
|
|
|
|
|
|
|
|
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
|
|
|
|
for (const position of currentPositions.positions) {
|
|
|
|
portfolioItemsNow[position.symbol] = position;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const item of currentPositions.positions) {
|
2021-08-26 17:45:04 +02:00
|
|
|
if (item.quantity.lte(0)) {
|
|
|
|
// Ignore positions without any quantity
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-07-31 20:45:12 +02:00
|
|
|
const value = item.quantity.mul(item.marketPrice);
|
|
|
|
const symbolProfile = symbolProfileMap[item.symbol];
|
|
|
|
const dataProviderResponse = dataProviderResponses[item.symbol];
|
2021-08-21 15:03:55 +02:00
|
|
|
holdings[item.symbol] = {
|
2021-07-31 20:45:12 +02:00
|
|
|
allocationCurrent: value.div(totalValue).toNumber(),
|
2021-08-09 21:26:41 +02:00
|
|
|
allocationInvestment: item.investment.div(totalInvestment).toNumber(),
|
2021-08-08 19:27:58 +02:00
|
|
|
assetClass: symbolProfile.assetClass,
|
2021-08-22 22:19:10 +02:00
|
|
|
assetSubClass: symbolProfile.assetSubClass,
|
2021-07-31 20:45:12 +02:00
|
|
|
countries: symbolProfile.countries,
|
|
|
|
currency: item.currency,
|
|
|
|
exchange: dataProviderResponse.exchange,
|
2021-08-19 21:44:10 +02:00
|
|
|
grossPerformance: item.grossPerformance?.toNumber() ?? 0,
|
|
|
|
grossPerformancePercent:
|
|
|
|
item.grossPerformancePercentage?.toNumber() ?? 0,
|
2021-07-31 20:45:12 +02:00
|
|
|
investment: item.investment.toNumber(),
|
|
|
|
marketPrice: item.marketPrice,
|
|
|
|
marketState: dataProviderResponse.marketState,
|
2021-08-01 20:43:12 +02:00
|
|
|
name: symbolProfile.name,
|
2021-09-05 21:21:22 +02:00
|
|
|
netPerformance: item.netPerformance?.toNumber() ?? 0,
|
|
|
|
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,
|
2021-07-31 20:45:12 +02:00
|
|
|
quantity: item.quantity.toNumber(),
|
|
|
|
sectors: symbolProfile.sectors,
|
|
|
|
symbol: item.symbol,
|
|
|
|
transactionCount: item.transactionCount,
|
|
|
|
value: value.toNumber()
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-12-02 21:53:38 +01:00
|
|
|
const cashPositions = await this.getCashPositions({
|
2021-08-09 21:26:41 +02:00
|
|
|
cashDetails,
|
2021-12-02 21:53:38 +01:00
|
|
|
userCurrency,
|
2021-08-09 21:26:41 +02:00
|
|
|
investment: totalInvestment,
|
|
|
|
value: totalValue
|
|
|
|
});
|
|
|
|
|
2021-12-02 21:53:38 +01:00
|
|
|
for (const symbol of Object.keys(cashPositions)) {
|
|
|
|
holdings[symbol] = cashPositions[symbol];
|
|
|
|
}
|
|
|
|
|
2021-11-13 20:38:29 +01:00
|
|
|
const accounts = await this.getValueOfAccounts(
|
2021-08-21 15:03:55 +02:00
|
|
|
orders,
|
|
|
|
portfolioItemsNow,
|
|
|
|
userCurrency,
|
|
|
|
userId
|
|
|
|
);
|
|
|
|
|
|
|
|
return { accounts, holdings, hasErrors: currentPositions.hasErrors };
|
2021-07-31 20:45:12 +02:00
|
|
|
}
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
public async getPosition(
|
|
|
|
aImpersonationId: string,
|
|
|
|
aSymbol: string
|
|
|
|
): Promise<PortfolioPositionDetail> {
|
2021-10-19 18:27:50 +02:00
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
const orders = (await this.orderService.getOrders({ userId })).filter(
|
2021-08-06 21:47:27 +02:00
|
|
|
(order) => order.symbol === aSymbol
|
|
|
|
);
|
2021-07-31 22:13:57 +02:00
|
|
|
|
2021-08-05 22:36:18 +02:00
|
|
|
if (orders.length <= 0) {
|
2021-07-31 22:13:57 +02:00
|
|
|
return {
|
|
|
|
averagePrice: undefined,
|
|
|
|
currency: undefined,
|
|
|
|
firstBuyDate: undefined,
|
|
|
|
grossPerformance: undefined,
|
|
|
|
grossPerformancePercent: undefined,
|
|
|
|
historicalData: [],
|
|
|
|
investment: undefined,
|
|
|
|
marketPrice: undefined,
|
|
|
|
maxPrice: undefined,
|
|
|
|
minPrice: undefined,
|
2021-09-11 11:23:47 +02:00
|
|
|
name: undefined,
|
2021-09-05 21:21:22 +02:00
|
|
|
netPerformance: undefined,
|
|
|
|
netPerformancePercent: undefined,
|
2021-07-31 22:13:57 +02:00
|
|
|
quantity: undefined,
|
|
|
|
symbol: aSymbol,
|
2021-11-27 09:51:08 +01:00
|
|
|
transactionCount: undefined,
|
|
|
|
value: undefined
|
2021-07-31 22:13:57 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-11-11 21:21:37 +01:00
|
|
|
const assetClass = orders[0].SymbolProfile?.assetClass;
|
|
|
|
const assetSubClass = orders[0].SymbolProfile?.assetSubClass;
|
2021-08-05 22:36:18 +02:00
|
|
|
const positionCurrency = orders[0].currency;
|
2021-09-11 11:23:47 +02:00
|
|
|
const name = orders[0].SymbolProfile?.name ?? '';
|
2021-08-05 22:36:18 +02:00
|
|
|
|
|
|
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
|
|
|
currency: order.currency,
|
2021-09-18 19:32:22 +02:00
|
|
|
dataSource: order.dataSource,
|
2021-08-05 22:36:18 +02:00
|
|
|
date: format(order.date, DATE_FORMAT),
|
2021-09-05 21:21:22 +02:00
|
|
|
fee: new Big(order.fee),
|
2021-08-05 22:36:18 +02:00
|
|
|
name: order.SymbolProfile?.name,
|
|
|
|
quantity: new Big(order.quantity),
|
|
|
|
symbol: order.symbol,
|
2021-12-19 16:52:35 +01:00
|
|
|
type: order.type,
|
2021-08-05 22:36:18 +02:00
|
|
|
unitPrice: new Big(order.unitPrice)
|
|
|
|
}));
|
|
|
|
|
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
this.currentRateService,
|
|
|
|
positionCurrency
|
|
|
|
);
|
|
|
|
portfolioCalculator.computeTransactionPoints(portfolioOrders);
|
|
|
|
const transactionPoints = portfolioCalculator.getTransactionPoints();
|
2021-07-31 22:13:57 +02:00
|
|
|
|
|
|
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
|
|
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
|
|
|
portfolioStart
|
|
|
|
);
|
|
|
|
|
|
|
|
const position = currentPositions.positions.find(
|
|
|
|
(item) => item.symbol === aSymbol
|
|
|
|
);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-07-10 18:16:46 +02:00
|
|
|
if (position) {
|
|
|
|
const {
|
2021-04-13 21:53:58 +02:00
|
|
|
averagePrice,
|
|
|
|
currency,
|
2021-09-18 19:32:22 +02:00
|
|
|
dataSource,
|
2021-04-13 21:53:58 +02:00
|
|
|
firstBuyDate,
|
2021-08-05 22:36:18 +02:00
|
|
|
marketPrice,
|
2021-04-30 21:08:43 +02:00
|
|
|
quantity,
|
|
|
|
transactionCount
|
2021-07-10 18:16:46 +02:00
|
|
|
} = position;
|
2021-06-05 17:17:53 +02:00
|
|
|
|
2021-09-05 21:21:22 +02:00
|
|
|
// Convert investment, gross and net performance to currency of user
|
2021-08-05 22:36:18 +02:00
|
|
|
const userCurrency = this.request.user.Settings.currency;
|
|
|
|
const investment = this.exchangeRateDataService.toCurrency(
|
|
|
|
position.investment.toNumber(),
|
|
|
|
currency,
|
|
|
|
userCurrency
|
|
|
|
);
|
|
|
|
const grossPerformance = this.exchangeRateDataService.toCurrency(
|
|
|
|
position.grossPerformance.toNumber(),
|
|
|
|
currency,
|
|
|
|
userCurrency
|
2021-08-04 20:43:00 +02:00
|
|
|
);
|
2021-09-05 21:21:22 +02:00
|
|
|
const netPerformance = this.exchangeRateDataService.toCurrency(
|
|
|
|
position.netPerformance.toNumber(),
|
|
|
|
currency,
|
|
|
|
userCurrency
|
|
|
|
);
|
2021-08-04 20:43:00 +02:00
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
const historicalData = await this.dataProviderService.getHistorical(
|
2021-09-18 19:32:22 +02:00
|
|
|
[{ dataSource, symbol: aSymbol }],
|
2021-04-13 21:53:58 +02:00
|
|
|
'day',
|
|
|
|
parseISO(firstBuyDate),
|
|
|
|
new Date()
|
|
|
|
);
|
|
|
|
|
|
|
|
const historicalDataArray: HistoricalDataItem[] = [];
|
2021-08-17 21:31:32 +02:00
|
|
|
let maxPrice = Math.max(orders[0].unitPrice, marketPrice);
|
|
|
|
let minPrice = Math.min(orders[0].unitPrice, marketPrice);
|
2021-08-12 23:30:04 +02:00
|
|
|
|
2021-08-17 21:31:32 +02:00
|
|
|
if (!historicalData?.[aSymbol]?.[firstBuyDate]) {
|
2021-08-12 23:30:04 +02:00
|
|
|
// Add historical entry for buy date, if no historical data available
|
|
|
|
historicalDataArray.push({
|
|
|
|
averagePrice: orders[0].unitPrice,
|
|
|
|
date: firstBuyDate,
|
|
|
|
value: orders[0].unitPrice
|
|
|
|
});
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
if (historicalData[aSymbol]) {
|
2021-07-31 22:13:57 +02:00
|
|
|
let j = -1;
|
2021-04-13 21:53:58 +02:00
|
|
|
for (const [date, { marketPrice }] of Object.entries(
|
|
|
|
historicalData[aSymbol]
|
|
|
|
)) {
|
2021-07-31 22:13:57 +02:00
|
|
|
while (
|
|
|
|
j + 1 < transactionPoints.length &&
|
|
|
|
!isAfter(parseDate(transactionPoints[j + 1].date), parseDate(date))
|
2021-06-05 17:17:53 +02:00
|
|
|
) {
|
2021-07-31 22:13:57 +02:00
|
|
|
j++;
|
|
|
|
}
|
|
|
|
let currentAveragePrice = 0;
|
|
|
|
const currentSymbol = transactionPoints[j].items.find(
|
|
|
|
(item) => item.symbol === aSymbol
|
|
|
|
);
|
|
|
|
if (currentSymbol) {
|
2021-08-07 07:07:12 +02:00
|
|
|
currentAveragePrice = currentSymbol.quantity.eq(0)
|
|
|
|
? 0
|
|
|
|
: currentSymbol.investment.div(currentSymbol.quantity).toNumber();
|
2021-06-05 17:17:53 +02:00
|
|
|
}
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
historicalDataArray.push({
|
|
|
|
date,
|
2021-06-05 17:17:53 +02:00
|
|
|
averagePrice: currentAveragePrice,
|
2021-04-13 21:53:58 +02:00
|
|
|
value: marketPrice
|
|
|
|
});
|
|
|
|
|
2021-07-31 22:13:57 +02:00
|
|
|
maxPrice = Math.max(marketPrice ?? 0, maxPrice);
|
|
|
|
minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice);
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2021-11-11 21:21:37 +01:00
|
|
|
assetClass,
|
|
|
|
assetSubClass,
|
2021-04-13 21:53:58 +02:00
|
|
|
currency,
|
|
|
|
firstBuyDate,
|
2021-08-05 22:36:18 +02:00
|
|
|
grossPerformance,
|
|
|
|
investment,
|
2021-04-13 21:53:58 +02:00
|
|
|
marketPrice,
|
|
|
|
maxPrice,
|
|
|
|
minPrice,
|
2021-09-11 11:23:47 +02:00
|
|
|
name,
|
2021-09-05 21:21:22 +02:00
|
|
|
netPerformance,
|
2021-04-30 21:08:43 +02:00
|
|
|
transactionCount,
|
2021-08-01 17:26:39 +02:00
|
|
|
averagePrice: averagePrice.toNumber(),
|
2021-07-31 22:13:57 +02:00
|
|
|
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
2021-04-13 21:53:58 +02:00
|
|
|
historicalData: historicalDataArray,
|
2021-09-05 21:21:22 +02:00
|
|
|
netPerformancePercent: position.netPerformancePercentage.toNumber(),
|
2021-08-01 17:26:39 +02:00
|
|
|
quantity: quantity.toNumber(),
|
2021-11-27 09:51:08 +01:00
|
|
|
symbol: aSymbol,
|
|
|
|
value: this.exchangeRateDataService.toCurrency(
|
|
|
|
quantity.mul(marketPrice).toNumber(),
|
|
|
|
currency,
|
|
|
|
userCurrency
|
|
|
|
)
|
2021-04-13 21:53:58 +02:00
|
|
|
};
|
2021-07-31 22:13:57 +02:00
|
|
|
} else {
|
2021-09-18 19:32:22 +02:00
|
|
|
const currentData = await this.dataProviderService.get([
|
|
|
|
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
|
|
|
]);
|
2021-08-07 07:03:35 +02:00
|
|
|
const marketPrice = currentData[aSymbol]?.marketPrice;
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
let historicalData = await this.dataProviderService.getHistorical(
|
2021-09-18 19:32:22 +02:00
|
|
|
[{ dataSource: DataSource.YAHOO, symbol: aSymbol }],
|
2021-04-13 21:53:58 +02:00
|
|
|
'day',
|
2021-07-31 22:13:57 +02:00
|
|
|
portfolioStart,
|
2021-04-13 21:53:58 +02:00
|
|
|
new Date()
|
|
|
|
);
|
|
|
|
|
|
|
|
if (isEmpty(historicalData)) {
|
|
|
|
historicalData = await this.dataProviderService.getHistoricalRaw(
|
2021-05-27 20:50:10 +02:00
|
|
|
[{ dataSource: DataSource.YAHOO, symbol: aSymbol }],
|
2021-07-31 22:13:57 +02:00
|
|
|
portfolioStart,
|
2021-04-13 21:53:58 +02:00
|
|
|
new Date()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const historicalDataArray: HistoricalDataItem[] = [];
|
2021-08-07 07:03:35 +02:00
|
|
|
let maxPrice = marketPrice;
|
|
|
|
let minPrice = marketPrice;
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-07-03 11:32:03 +02:00
|
|
|
for (const [date, { marketPrice }] of Object.entries(
|
2021-04-13 21:53:58 +02:00
|
|
|
historicalData[aSymbol]
|
2021-08-07 07:03:35 +02:00
|
|
|
)) {
|
2021-04-13 21:53:58 +02:00
|
|
|
historicalDataArray.push({
|
|
|
|
date,
|
|
|
|
value: marketPrice
|
|
|
|
});
|
2021-08-07 07:03:35 +02:00
|
|
|
|
|
|
|
maxPrice = Math.max(marketPrice ?? 0, maxPrice);
|
|
|
|
minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice);
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2021-11-11 21:21:37 +01:00
|
|
|
assetClass,
|
|
|
|
assetSubClass,
|
2021-08-07 07:03:35 +02:00
|
|
|
marketPrice,
|
|
|
|
maxPrice,
|
|
|
|
minPrice,
|
2021-09-11 11:23:47 +02:00
|
|
|
name,
|
2021-08-07 07:03:35 +02:00
|
|
|
averagePrice: 0,
|
2021-07-10 14:57:03 +02:00
|
|
|
currency: currentData[aSymbol]?.currency,
|
2021-04-13 21:53:58 +02:00
|
|
|
firstBuyDate: undefined,
|
|
|
|
grossPerformance: undefined,
|
|
|
|
grossPerformancePercent: undefined,
|
|
|
|
historicalData: historicalDataArray,
|
2021-08-07 07:03:35 +02:00
|
|
|
investment: 0,
|
2021-09-05 21:21:22 +02:00
|
|
|
netPerformance: undefined,
|
|
|
|
netPerformancePercent: undefined,
|
2021-08-07 07:03:35 +02:00
|
|
|
quantity: 0,
|
2021-04-30 21:08:43 +02:00
|
|
|
symbol: aSymbol,
|
2021-11-27 09:51:08 +01:00
|
|
|
transactionCount: undefined,
|
|
|
|
value: 0
|
2021-04-13 21:53:58 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2021-04-20 21:52:01 +02:00
|
|
|
|
2021-07-25 13:31:44 +02:00
|
|
|
public async getPositions(
|
|
|
|
aImpersonationId: string,
|
|
|
|
aDateRange: DateRange = 'max'
|
2021-07-26 23:03:22 +02:00
|
|
|
): Promise<{ hasErrors: boolean; positions: Position[] }> {
|
2021-10-19 18:27:50 +02:00
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
2021-07-25 13:31:44 +02:00
|
|
|
|
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
this.currentRateService,
|
|
|
|
this.request.user.Settings.currency
|
|
|
|
);
|
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
const { transactionPoints } = await this.getTransactionPoints({ userId });
|
2021-07-25 13:31:44 +02:00
|
|
|
|
2021-07-31 09:48:48 +02:00
|
|
|
if (transactionPoints?.length <= 0) {
|
|
|
|
return {
|
|
|
|
hasErrors: false,
|
|
|
|
positions: []
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-07-25 13:31:44 +02:00
|
|
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
|
|
|
|
2021-07-26 22:13:09 +02:00
|
|
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
|
|
|
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
2021-07-26 23:03:22 +02:00
|
|
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
|
|
|
startDate
|
|
|
|
);
|
2021-07-25 13:31:44 +02:00
|
|
|
|
2021-08-07 07:07:12 +02:00
|
|
|
const positions = currentPositions.positions.filter(
|
|
|
|
(item) => !item.quantity.eq(0)
|
2021-08-01 20:43:12 +02:00
|
|
|
);
|
2021-09-18 19:32:22 +02:00
|
|
|
const dataGatheringItem = positions.map((position) => {
|
|
|
|
return {
|
|
|
|
dataSource: position.dataSource,
|
|
|
|
symbol: position.symbol
|
|
|
|
};
|
|
|
|
});
|
2021-08-07 07:07:12 +02:00
|
|
|
const symbols = positions.map((position) => position.symbol);
|
2021-08-01 20:43:12 +02:00
|
|
|
|
2021-08-04 17:55:34 +02:00
|
|
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
2021-09-18 19:32:22 +02:00
|
|
|
this.dataProviderService.get(dataGatheringItem),
|
2021-08-01 20:43:12 +02:00
|
|
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
|
|
|
]);
|
|
|
|
|
|
|
|
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
|
|
|
for (const symbolProfile of symbolProfiles) {
|
|
|
|
symbolProfileMap[symbolProfile.symbol] = symbolProfile;
|
|
|
|
}
|
|
|
|
|
2021-07-26 23:03:22 +02:00
|
|
|
return {
|
|
|
|
hasErrors: currentPositions.hasErrors,
|
2021-08-07 07:07:12 +02:00
|
|
|
positions: positions.map((position) => {
|
2021-07-26 23:03:22 +02:00
|
|
|
return {
|
|
|
|
...position,
|
2021-08-08 19:27:58 +02:00
|
|
|
assetClass: symbolProfileMap[position.symbol].assetClass,
|
2021-07-26 23:03:22 +02:00
|
|
|
averagePrice: new Big(position.averagePrice).toNumber(),
|
|
|
|
grossPerformance: position.grossPerformance?.toNumber() ?? null,
|
|
|
|
grossPerformancePercentage:
|
|
|
|
position.grossPerformancePercentage?.toNumber() ?? null,
|
|
|
|
investment: new Big(position.investment).toNumber(),
|
2021-08-04 17:55:34 +02:00
|
|
|
marketState: dataProviderResponses[position.symbol].marketState,
|
2021-08-01 20:43:12 +02:00
|
|
|
name: symbolProfileMap[position.symbol].name,
|
2021-09-05 21:21:22 +02:00
|
|
|
netPerformance: position.netPerformance?.toNumber() ?? null,
|
|
|
|
netPerformancePercentage:
|
|
|
|
position.netPerformancePercentage?.toNumber() ?? null,
|
2021-08-01 17:26:39 +02:00
|
|
|
quantity: new Big(position.quantity).toNumber()
|
2021-07-26 23:03:22 +02:00
|
|
|
};
|
|
|
|
})
|
|
|
|
};
|
2021-07-25 13:31:44 +02:00
|
|
|
}
|
|
|
|
|
2021-07-27 22:46:41 +02:00
|
|
|
public async getPerformance(
|
|
|
|
aImpersonationId: string,
|
|
|
|
aDateRange: DateRange = 'max'
|
|
|
|
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
|
2021-10-19 18:27:50 +02:00
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
2021-07-27 22:46:41 +02:00
|
|
|
|
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
this.currentRateService,
|
|
|
|
this.request.user.Settings.currency
|
|
|
|
);
|
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
const { transactionPoints } = await this.getTransactionPoints({ userId });
|
2021-07-27 22:46:41 +02:00
|
|
|
|
2021-07-31 09:31:45 +02:00
|
|
|
if (transactionPoints?.length <= 0) {
|
|
|
|
return {
|
|
|
|
hasErrors: false,
|
|
|
|
performance: {
|
2021-09-12 14:08:42 -04:00
|
|
|
annualizedPerformancePercent: 0,
|
2021-07-31 09:31:45 +02:00
|
|
|
currentGrossPerformance: 0,
|
|
|
|
currentGrossPerformancePercent: 0,
|
2021-09-05 21:21:22 +02:00
|
|
|
currentNetPerformance: 0,
|
|
|
|
currentNetPerformancePercent: 0,
|
2021-12-18 09:20:42 +01:00
|
|
|
currentValue: 0
|
2021-07-31 09:31:45 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-07-27 22:46:41 +02:00
|
|
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
|
|
|
|
|
|
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
|
|
|
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
|
|
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
|
|
|
startDate
|
|
|
|
);
|
|
|
|
|
2021-07-31 10:08:05 +02:00
|
|
|
const hasErrors = currentPositions.hasErrors;
|
2021-09-12 14:08:42 -04:00
|
|
|
const annualizedPerformancePercent =
|
|
|
|
currentPositions.netAnnualizedPerformance.toNumber();
|
2021-07-31 10:08:05 +02:00
|
|
|
const currentValue = currentPositions.currentValue.toNumber();
|
|
|
|
const currentGrossPerformance =
|
|
|
|
currentPositions.grossPerformance.toNumber();
|
|
|
|
const currentGrossPerformancePercent =
|
|
|
|
currentPositions.grossPerformancePercentage.toNumber();
|
2021-09-05 21:21:22 +02:00
|
|
|
const currentNetPerformance = currentPositions.netPerformance.toNumber();
|
|
|
|
const currentNetPerformancePercent =
|
|
|
|
currentPositions.netPerformancePercentage.toNumber();
|
|
|
|
|
2021-07-27 22:46:41 +02:00
|
|
|
return {
|
2021-07-27 23:04:31 +02:00
|
|
|
hasErrors: currentPositions.hasErrors || hasErrors,
|
2021-07-27 22:46:41 +02:00
|
|
|
performance: {
|
2021-09-12 14:08:42 -04:00
|
|
|
annualizedPerformancePercent,
|
2021-07-27 22:46:41 +02:00
|
|
|
currentGrossPerformance,
|
|
|
|
currentGrossPerformancePercent,
|
2021-09-05 21:21:22 +02:00
|
|
|
currentNetPerformance,
|
|
|
|
currentNetPerformancePercent,
|
2021-12-18 09:20:42 +01:00
|
|
|
currentValue
|
2021-07-27 22:46:41 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-07-31 21:07:04 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-07-31 23:33:50 +02:00
|
|
|
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
2021-10-19 18:27:50 +02:00
|
|
|
const currency = this.request.user.Settings.currency;
|
|
|
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
2021-07-31 23:33:50 +02:00
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
const { orders, transactionPoints } = await this.getTransactionPoints({
|
2021-08-01 00:15:37 +02:00
|
|
|
userId
|
2021-08-07 20:52:55 +02:00
|
|
|
});
|
2021-07-31 23:33:50 +02:00
|
|
|
|
2021-08-01 00:15:37 +02:00
|
|
|
if (isEmpty(orders)) {
|
2021-07-31 23:33:50 +02:00
|
|
|
return {
|
|
|
|
rules: {}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-08-01 00:15:37 +02:00
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
this.currentRateService,
|
2021-10-19 18:27:50 +02:00
|
|
|
currency
|
2021-08-01 00:15:37 +02:00
|
|
|
);
|
|
|
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
2021-07-31 23:33:50 +02:00
|
|
|
|
2021-08-01 00:15:37 +02:00
|
|
|
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;
|
|
|
|
}
|
2021-11-13 20:38:29 +01:00
|
|
|
const accounts = await this.getValueOfAccounts(
|
2021-08-21 15:03:55 +02:00
|
|
|
orders,
|
|
|
|
portfolioItemsNow,
|
2021-10-19 18:27:50 +02:00
|
|
|
currency,
|
2021-08-21 15:03:55 +02:00
|
|
|
userId
|
|
|
|
);
|
2021-07-31 23:33:50 +02:00
|
|
|
return {
|
|
|
|
rules: {
|
|
|
|
accountClusterRisk: await this.rulesService.evaluate(
|
|
|
|
[
|
|
|
|
new AccountClusterRiskInitialInvestment(
|
|
|
|
this.exchangeRateDataService,
|
2021-08-01 00:15:37 +02:00
|
|
|
accounts
|
2021-07-31 23:33:50 +02:00
|
|
|
),
|
|
|
|
new AccountClusterRiskCurrentInvestment(
|
|
|
|
this.exchangeRateDataService,
|
2021-08-01 00:15:37 +02:00
|
|
|
accounts
|
2021-07-31 23:33:50 +02:00
|
|
|
),
|
|
|
|
new AccountClusterRiskSingleAccount(
|
|
|
|
this.exchangeRateDataService,
|
2021-08-01 00:15:37 +02:00
|
|
|
accounts
|
2021-07-31 23:33:50 +02:00
|
|
|
)
|
|
|
|
],
|
2021-10-19 18:27:50 +02:00
|
|
|
{ baseCurrency: currency }
|
2021-07-31 23:33:50 +02:00
|
|
|
),
|
|
|
|
currencyClusterRisk: await this.rulesService.evaluate(
|
|
|
|
[
|
|
|
|
new CurrencyClusterRiskBaseCurrencyInitialInvestment(
|
|
|
|
this.exchangeRateDataService,
|
2021-08-01 00:15:37 +02:00
|
|
|
currentPositions
|
2021-07-31 23:33:50 +02:00
|
|
|
),
|
|
|
|
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
|
|
|
|
this.exchangeRateDataService,
|
2021-08-01 00:15:37 +02:00
|
|
|
currentPositions
|
2021-07-31 23:33:50 +02:00
|
|
|
),
|
|
|
|
new CurrencyClusterRiskInitialInvestment(
|
|
|
|
this.exchangeRateDataService,
|
2021-08-01 00:15:37 +02:00
|
|
|
currentPositions
|
2021-07-31 23:33:50 +02:00
|
|
|
),
|
|
|
|
new CurrencyClusterRiskCurrentInvestment(
|
|
|
|
this.exchangeRateDataService,
|
2021-08-01 00:15:37 +02:00
|
|
|
currentPositions
|
2021-07-31 23:33:50 +02:00
|
|
|
)
|
|
|
|
],
|
2021-10-19 18:27:50 +02:00
|
|
|
{ baseCurrency: currency }
|
2021-07-31 23:33:50 +02:00
|
|
|
),
|
|
|
|
fees: await this.rulesService.evaluate(
|
|
|
|
[
|
|
|
|
new FeeRatioInitialInvestment(
|
|
|
|
this.exchangeRateDataService,
|
2021-08-01 00:15:37 +02:00
|
|
|
currentPositions.totalInvestment.toNumber(),
|
|
|
|
this.getFees(orders)
|
2021-07-31 23:33:50 +02:00
|
|
|
)
|
|
|
|
],
|
2021-10-19 18:27:50 +02:00
|
|
|
{ baseCurrency: currency }
|
2021-07-31 23:33:50 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-08-13 19:26:48 +02:00
|
|
|
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
|
|
|
const currency = this.request.user.Settings.currency;
|
2021-10-19 18:27:50 +02:00
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
2021-08-13 19:26:48 +02:00
|
|
|
|
2021-08-14 19:15:26 +02:00
|
|
|
const performanceInformation = await this.getPerformance(aImpersonationId);
|
2021-08-13 19:26:48 +02:00
|
|
|
|
|
|
|
const { balance } = await this.accountService.getCashDetails(
|
|
|
|
userId,
|
|
|
|
currency
|
|
|
|
);
|
|
|
|
const orders = await this.orderService.getOrders({ userId });
|
|
|
|
const fees = this.getFees(orders);
|
|
|
|
const firstOrderDate = orders[0]?.date;
|
|
|
|
|
2021-12-19 16:52:35 +01:00
|
|
|
const totalBuy = this.getTotalByType(orders, currency, 'BUY');
|
|
|
|
const totalSell = this.getTotalByType(orders, currency, 'SELL');
|
2021-08-13 19:26:48 +02:00
|
|
|
|
|
|
|
const committedFunds = new Big(totalBuy).sub(totalSell);
|
|
|
|
|
|
|
|
const netWorth = new Big(balance)
|
|
|
|
.plus(performanceInformation.performance.currentValue)
|
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
return {
|
|
|
|
...performanceInformation.performance,
|
|
|
|
fees,
|
|
|
|
firstOrderDate,
|
|
|
|
netWorth,
|
|
|
|
cash: balance,
|
|
|
|
committedFunds: committedFunds.toNumber(),
|
|
|
|
ordersCount: orders.length,
|
|
|
|
totalBuy: totalBuy,
|
|
|
|
totalSell: totalSell
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-12-02 21:53:38 +01:00
|
|
|
private async getCashPositions({
|
2021-08-09 21:26:41 +02:00
|
|
|
cashDetails,
|
|
|
|
investment,
|
2021-12-02 21:53:38 +01:00
|
|
|
userCurrency,
|
2021-08-09 21:26:41 +02:00
|
|
|
value
|
|
|
|
}: {
|
|
|
|
cashDetails: CashDetails;
|
|
|
|
investment: Big;
|
|
|
|
value: Big;
|
2021-12-02 21:53:38 +01:00
|
|
|
userCurrency: string;
|
2021-08-09 21:26:41 +02:00
|
|
|
}) {
|
2021-12-02 21:53:38 +01:00
|
|
|
const cashPositions = {};
|
2021-08-09 21:26:41 +02:00
|
|
|
|
2021-12-02 21:53:38 +01:00
|
|
|
for (const account of cashDetails.accounts) {
|
|
|
|
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
|
|
|
account.balance,
|
|
|
|
account.currency,
|
|
|
|
userCurrency
|
|
|
|
);
|
|
|
|
|
|
|
|
if (convertedBalance === 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cashPositions[account.currency]) {
|
|
|
|
cashPositions[account.currency].investment += convertedBalance;
|
|
|
|
cashPositions[account.currency].value += convertedBalance;
|
|
|
|
} else {
|
|
|
|
cashPositions[account.currency] = {
|
|
|
|
allocationCurrent: 0,
|
|
|
|
allocationInvestment: 0,
|
|
|
|
assetClass: AssetClass.CASH,
|
|
|
|
assetSubClass: AssetClass.CASH,
|
|
|
|
countries: [],
|
|
|
|
currency: account.currency,
|
|
|
|
grossPerformance: 0,
|
|
|
|
grossPerformancePercent: 0,
|
|
|
|
investment: convertedBalance,
|
|
|
|
marketPrice: 0,
|
|
|
|
marketState: MarketState.open,
|
|
|
|
name: account.currency,
|
|
|
|
netPerformance: 0,
|
|
|
|
netPerformancePercent: 0,
|
|
|
|
quantity: 0,
|
|
|
|
sectors: [],
|
|
|
|
symbol: account.currency,
|
|
|
|
transactionCount: 0,
|
|
|
|
value: convertedBalance
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const symbol of Object.keys(cashPositions)) {
|
|
|
|
// Calculate allocations for each currency
|
|
|
|
cashPositions[symbol].allocationCurrent = new Big(
|
|
|
|
cashPositions[symbol].value
|
|
|
|
)
|
|
|
|
.div(value)
|
|
|
|
.toNumber();
|
|
|
|
cashPositions[symbol].allocationInvestment = new Big(
|
|
|
|
cashPositions[symbol].investment
|
|
|
|
)
|
|
|
|
.div(investment)
|
|
|
|
.toNumber();
|
|
|
|
}
|
|
|
|
|
|
|
|
return cashPositions;
|
2021-08-09 21:26:41 +02:00
|
|
|
}
|
|
|
|
|
2021-07-25 13:31:44 +02:00
|
|
|
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
|
|
|
|
switch (aDateRange) {
|
|
|
|
case '1d':
|
|
|
|
portfolioStart = max([portfolioStart, subDays(new Date(), 1)]);
|
|
|
|
break;
|
|
|
|
case 'ytd':
|
|
|
|
portfolioStart = max([portfolioStart, setDayOfYear(new Date(), 1)]);
|
|
|
|
break;
|
|
|
|
case '1y':
|
|
|
|
portfolioStart = max([portfolioStart, subYears(new Date(), 1)]);
|
|
|
|
break;
|
|
|
|
case '5y':
|
|
|
|
portfolioStart = max([portfolioStart, subYears(new Date(), 5)]);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return portfolioStart;
|
|
|
|
}
|
|
|
|
|
2021-08-07 20:52:55 +02:00
|
|
|
private async getTransactionPoints({
|
|
|
|
includeDrafts = false,
|
|
|
|
userId
|
|
|
|
}: {
|
|
|
|
includeDrafts?: boolean;
|
|
|
|
userId: string;
|
|
|
|
}): Promise<{
|
2021-07-31 20:45:12 +02:00
|
|
|
transactionPoints: TransactionPoint[];
|
|
|
|
orders: OrderWithAccount[];
|
|
|
|
}> {
|
2021-08-07 20:52:55 +02:00
|
|
|
const orders = await this.orderService.getOrders({ includeDrafts, userId });
|
2021-07-25 13:31:44 +02:00
|
|
|
|
|
|
|
if (orders.length <= 0) {
|
2021-07-31 20:45:12 +02:00
|
|
|
return { transactionPoints: [], orders: [] };
|
2021-07-25 13:31:44 +02:00
|
|
|
}
|
|
|
|
|
2021-10-19 18:27:50 +02:00
|
|
|
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
2021-07-25 13:31:44 +02:00
|
|
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
|
|
|
currency: order.currency,
|
2021-09-18 19:32:22 +02:00
|
|
|
dataSource: order.dataSource,
|
2021-07-28 16:11:19 +02:00
|
|
|
date: format(order.date, DATE_FORMAT),
|
2021-09-05 21:21:22 +02:00
|
|
|
fee: new Big(
|
|
|
|
this.exchangeRateDataService.toCurrency(
|
|
|
|
order.fee,
|
|
|
|
order.currency,
|
|
|
|
userCurrency
|
|
|
|
)
|
|
|
|
),
|
2021-07-25 13:31:44 +02:00
|
|
|
name: order.SymbolProfile?.name,
|
|
|
|
quantity: new Big(order.quantity),
|
|
|
|
symbol: order.symbol,
|
2021-12-19 16:52:35 +01:00
|
|
|
type: order.type,
|
2021-08-01 10:26:25 -04:00
|
|
|
unitPrice: new Big(
|
|
|
|
this.exchangeRateDataService.toCurrency(
|
|
|
|
order.unitPrice,
|
|
|
|
order.currency,
|
|
|
|
userCurrency
|
|
|
|
)
|
|
|
|
)
|
2021-07-25 13:31:44 +02:00
|
|
|
}));
|
|
|
|
|
|
|
|
const portfolioCalculator = new PortfolioCalculator(
|
|
|
|
this.currentRateService,
|
2021-08-01 10:26:25 -04:00
|
|
|
userCurrency
|
2021-07-25 13:31:44 +02:00
|
|
|
);
|
|
|
|
portfolioCalculator.computeTransactionPoints(portfolioOrders);
|
2021-07-31 20:45:12 +02:00
|
|
|
return {
|
|
|
|
transactionPoints: portfolioCalculator.getTransactionPoints(),
|
|
|
|
orders
|
|
|
|
};
|
2021-07-25 13:31:44 +02:00
|
|
|
}
|
|
|
|
|
2021-11-13 20:38:29 +01:00
|
|
|
private async getValueOfAccounts(
|
2021-07-31 20:45:12 +02:00
|
|
|
orders: OrderWithAccount[],
|
|
|
|
portfolioItemsNow: { [p: string]: TimelinePosition },
|
2021-09-24 21:09:48 +02:00
|
|
|
userCurrency: string,
|
2021-08-21 15:03:55 +02:00
|
|
|
userId: string
|
2021-07-31 20:45:12 +02:00
|
|
|
) {
|
2021-08-21 15:03:55 +02:00
|
|
|
const accounts: PortfolioDetails['accounts'] = {};
|
2021-07-31 20:45:12 +02:00
|
|
|
|
2021-08-21 15:03:55 +02:00
|
|
|
const currentAccounts = await this.accountService.getAccounts(userId);
|
2021-07-31 20:45:12 +02:00
|
|
|
|
2021-08-21 15:03:55 +02:00
|
|
|
for (const account of currentAccounts) {
|
|
|
|
const ordersByAccount = orders.filter(({ accountId }) => {
|
|
|
|
return accountId === account.id;
|
|
|
|
});
|
|
|
|
|
2021-11-13 20:38:29 +01:00
|
|
|
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
|
|
|
account.balance,
|
|
|
|
account.currency,
|
|
|
|
userCurrency
|
|
|
|
);
|
|
|
|
accounts[account.name] = {
|
2021-12-02 21:53:38 +01:00
|
|
|
balance: convertedBalance,
|
|
|
|
currency: account.currency,
|
2021-11-13 20:38:29 +01:00
|
|
|
current: convertedBalance,
|
|
|
|
original: convertedBalance
|
|
|
|
};
|
2021-08-21 15:03:55 +02:00
|
|
|
|
|
|
|
for (const order of ordersByAccount) {
|
2021-11-14 17:04:52 +01:00
|
|
|
let currentValueOfSymbol =
|
|
|
|
order.quantity * portfolioItemsNow[order.symbol].marketPrice;
|
|
|
|
let originalValueOfSymbol = order.quantity * order.unitPrice;
|
2021-08-21 15:03:55 +02:00
|
|
|
|
|
|
|
if (order.type === 'SELL') {
|
|
|
|
currentValueOfSymbol *= -1;
|
|
|
|
originalValueOfSymbol *= -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (accounts[order.Account?.name || UNKNOWN_KEY]?.current) {
|
|
|
|
accounts[order.Account?.name || UNKNOWN_KEY].current +=
|
|
|
|
currentValueOfSymbol;
|
|
|
|
accounts[order.Account?.name || UNKNOWN_KEY].original +=
|
|
|
|
originalValueOfSymbol;
|
|
|
|
} else {
|
|
|
|
accounts[order.Account?.name || UNKNOWN_KEY] = {
|
2021-12-02 21:53:38 +01:00
|
|
|
balance: 0,
|
|
|
|
currency: order.Account?.currency,
|
2021-08-21 15:03:55 +02:00
|
|
|
current: currentValueOfSymbol,
|
|
|
|
original: originalValueOfSymbol
|
|
|
|
};
|
|
|
|
}
|
2021-07-31 20:45:12 +02:00
|
|
|
}
|
|
|
|
}
|
2021-08-21 15:03:55 +02:00
|
|
|
|
2021-07-31 20:45:12 +02:00
|
|
|
return accounts;
|
|
|
|
}
|
|
|
|
|
2021-10-19 18:27:50 +02:00
|
|
|
private async getUserId(aImpersonationId: string, aUserId: string) {
|
2021-07-31 20:45:12 +02:00
|
|
|
const impersonationUserId =
|
|
|
|
await this.impersonationService.validateImpersonationId(
|
|
|
|
aImpersonationId,
|
2021-10-19 18:27:50 +02:00
|
|
|
aUserId
|
2021-07-31 20:45:12 +02:00
|
|
|
);
|
|
|
|
|
2021-10-19 18:27:50 +02:00
|
|
|
return impersonationUserId || aUserId;
|
2021-07-31 20:45:12 +02:00
|
|
|
}
|
2021-07-31 21:07:04 +02:00
|
|
|
|
|
|
|
private getTotalByType(
|
|
|
|
orders: OrderWithAccount[],
|
2021-09-24 21:09:48 +02:00
|
|
|
currency: string,
|
2021-07-31 21:07:04 +02:00
|
|
|
type: TypeOfOrder
|
|
|
|
) {
|
|
|
|
return orders
|
|
|
|
.filter(
|
|
|
|
(order) => !isAfter(order.date, endOfToday()) && order.type === type
|
|
|
|
)
|
|
|
|
.map((order) => {
|
|
|
|
return this.exchangeRateDataService.toCurrency(
|
|
|
|
order.quantity * order.unitPrice,
|
|
|
|
order.currency,
|
|
|
|
currency
|
|
|
|
);
|
|
|
|
})
|
|
|
|
.reduce((previous, current) => previous + current, 0);
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|