|
|
|
@ -50,8 +50,11 @@ import type {
|
|
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
|
|
|
import { REQUEST } from '@nestjs/core';
|
|
|
|
|
import {
|
|
|
|
|
Account,
|
|
|
|
|
AssetClass,
|
|
|
|
|
DataSource,
|
|
|
|
|
Order,
|
|
|
|
|
Platform,
|
|
|
|
|
Prisma,
|
|
|
|
|
Tag,
|
|
|
|
|
Type as TypeOfOrder
|
|
|
|
@ -106,7 +109,8 @@ export class PortfolioService {
|
|
|
|
|
|
|
|
|
|
public async getAccounts(
|
|
|
|
|
aUserId: string,
|
|
|
|
|
aFilters?: Filter[]
|
|
|
|
|
aFilters?: Filter[],
|
|
|
|
|
withExcludedAccounts = false
|
|
|
|
|
): Promise<AccountWithValue[]> {
|
|
|
|
|
const where: Prisma.AccountWhereInput = { userId: aUserId };
|
|
|
|
|
|
|
|
|
@ -120,7 +124,13 @@ export class PortfolioService {
|
|
|
|
|
include: { Order: true, Platform: true },
|
|
|
|
|
orderBy: { name: 'asc' }
|
|
|
|
|
}),
|
|
|
|
|
this.getDetails(aUserId, aUserId, undefined, aFilters)
|
|
|
|
|
this.getDetails(
|
|
|
|
|
aUserId,
|
|
|
|
|
aUserId,
|
|
|
|
|
undefined,
|
|
|
|
|
aFilters,
|
|
|
|
|
withExcludedAccounts
|
|
|
|
|
)
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
|
|
|
@ -160,9 +170,14 @@ export class PortfolioService {
|
|
|
|
|
|
|
|
|
|
public async getAccountsWithAggregations(
|
|
|
|
|
aUserId: string,
|
|
|
|
|
aFilters?: Filter[]
|
|
|
|
|
aFilters?: Filter[],
|
|
|
|
|
withExcludedAccounts = false
|
|
|
|
|
): Promise<Accounts> {
|
|
|
|
|
const accounts = await this.getAccounts(aUserId, aFilters);
|
|
|
|
|
const accounts = await this.getAccounts(
|
|
|
|
|
aUserId,
|
|
|
|
|
aFilters,
|
|
|
|
|
withExcludedAccounts
|
|
|
|
|
);
|
|
|
|
|
let totalBalanceInBaseCurrency = new Big(0);
|
|
|
|
|
let totalValueInBaseCurrency = new Big(0);
|
|
|
|
|
let transactionCount = 0;
|
|
|
|
@ -410,7 +425,8 @@ export class PortfolioService {
|
|
|
|
|
aImpersonationId: string,
|
|
|
|
|
aUserId: string,
|
|
|
|
|
aDateRange: DateRange = 'max',
|
|
|
|
|
aFilters?: Filter[]
|
|
|
|
|
aFilters?: Filter[],
|
|
|
|
|
withExcludedAccounts = false
|
|
|
|
|
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
|
|
|
|
const userId = await this.getUserId(aImpersonationId, aUserId);
|
|
|
|
|
const user = await this.userService.user({ id: userId });
|
|
|
|
@ -426,6 +442,7 @@ export class PortfolioService {
|
|
|
|
|
const { orders, portfolioOrders, transactionPoints } =
|
|
|
|
|
await this.getTransactionPoints({
|
|
|
|
|
userId,
|
|
|
|
|
withExcludedAccounts,
|
|
|
|
|
filters: aFilters
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@ -580,6 +597,7 @@ export class PortfolioService {
|
|
|
|
|
portfolioItemsNow,
|
|
|
|
|
userCurrency,
|
|
|
|
|
userId,
|
|
|
|
|
withExcludedAccounts,
|
|
|
|
|
filters: aFilters
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@ -588,6 +606,7 @@ export class PortfolioService {
|
|
|
|
|
return {
|
|
|
|
|
accounts,
|
|
|
|
|
holdings,
|
|
|
|
|
summary,
|
|
|
|
|
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
|
|
|
|
|
filteredValueInPercentage: summary.netWorth
|
|
|
|
|
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
|
|
|
|
@ -606,7 +625,11 @@ export class PortfolioService {
|
|
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
|
|
|
|
|
|
|
|
|
const orders = (
|
|
|
|
|
await this.orderService.getOrders({ userCurrency, userId })
|
|
|
|
|
await this.orderService.getOrders({
|
|
|
|
|
userCurrency,
|
|
|
|
|
userId,
|
|
|
|
|
withExcludedAccounts: true
|
|
|
|
|
})
|
|
|
|
|
).filter(({ SymbolProfile }) => {
|
|
|
|
|
return (
|
|
|
|
|
SymbolProfile.dataSource === aDataSource &&
|
|
|
|
@ -1181,74 +1204,6 @@ export class PortfolioService {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
|
|
|
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
|
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
|
|
|
|
const user = await this.userService.user({ id: userId });
|
|
|
|
|
|
|
|
|
|
const performanceInformation = await this.getPerformance(aImpersonationId);
|
|
|
|
|
|
|
|
|
|
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
|
|
|
|
|
userId,
|
|
|
|
|
currency: userCurrency
|
|
|
|
|
});
|
|
|
|
|
const orders = await this.orderService.getOrders({
|
|
|
|
|
userCurrency,
|
|
|
|
|
userId
|
|
|
|
|
});
|
|
|
|
|
const dividend = this.getDividend(orders).toNumber();
|
|
|
|
|
const emergencyFund = new Big(
|
|
|
|
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
|
|
|
|
);
|
|
|
|
|
const fees = this.getFees(orders).toNumber();
|
|
|
|
|
const firstOrderDate = orders[0]?.date;
|
|
|
|
|
const items = this.getItems(orders).toNumber();
|
|
|
|
|
|
|
|
|
|
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
|
|
|
|
|
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
|
|
|
|
|
|
|
|
|
|
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
|
|
|
|
|
const committedFunds = new Big(totalBuy).minus(totalSell);
|
|
|
|
|
|
|
|
|
|
const netWorth = new Big(balanceInBaseCurrency)
|
|
|
|
|
.plus(performanceInformation.performance.currentValue)
|
|
|
|
|
.plus(items)
|
|
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
|
|
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
|
|
|
|
|
|
|
|
|
const annualizedPerformancePercent = new PortfolioCalculator({
|
|
|
|
|
currency: userCurrency,
|
|
|
|
|
currentRateService: this.currentRateService,
|
|
|
|
|
orders: []
|
|
|
|
|
})
|
|
|
|
|
.getAnnualizedPerformancePercent({
|
|
|
|
|
daysInMarket,
|
|
|
|
|
netPerformancePercent: new Big(
|
|
|
|
|
performanceInformation.performance.currentNetPerformancePercent
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
?.toNumber();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...performanceInformation.performance,
|
|
|
|
|
annualizedPerformancePercent,
|
|
|
|
|
cash,
|
|
|
|
|
dividend,
|
|
|
|
|
fees,
|
|
|
|
|
firstOrderDate,
|
|
|
|
|
items,
|
|
|
|
|
netWorth,
|
|
|
|
|
totalBuy,
|
|
|
|
|
totalSell,
|
|
|
|
|
committedFunds: committedFunds.toNumber(),
|
|
|
|
|
emergencyFund: emergencyFund.toNumber(),
|
|
|
|
|
ordersCount: orders.filter((order) => {
|
|
|
|
|
return order.type === 'BUY' || order.type === 'SELL';
|
|
|
|
|
}).length
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getCashPositions({
|
|
|
|
|
cashDetails,
|
|
|
|
|
emergencyFund,
|
|
|
|
@ -1424,14 +1379,117 @@ export class PortfolioService {
|
|
|
|
|
return portfolioStart;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getSummary(
|
|
|
|
|
aImpersonationId: string
|
|
|
|
|
): Promise<PortfolioSummary> {
|
|
|
|
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
|
|
|
|
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
|
|
|
|
const user = await this.userService.user({ id: userId });
|
|
|
|
|
|
|
|
|
|
const performanceInformation = await this.getPerformance(aImpersonationId);
|
|
|
|
|
|
|
|
|
|
const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
|
|
|
|
|
userId,
|
|
|
|
|
currency: userCurrency
|
|
|
|
|
});
|
|
|
|
|
const orders = await this.orderService.getOrders({
|
|
|
|
|
userCurrency,
|
|
|
|
|
userId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const excludedActivities = (
|
|
|
|
|
await this.orderService.getOrders({
|
|
|
|
|
userCurrency,
|
|
|
|
|
userId,
|
|
|
|
|
withExcludedAccounts: true
|
|
|
|
|
})
|
|
|
|
|
).filter(({ Account: account }) => {
|
|
|
|
|
return account?.isExcluded ?? false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const dividend = this.getDividend(orders).toNumber();
|
|
|
|
|
const emergencyFund = new Big(
|
|
|
|
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
|
|
|
|
);
|
|
|
|
|
const fees = this.getFees(orders).toNumber();
|
|
|
|
|
const firstOrderDate = orders[0]?.date;
|
|
|
|
|
const items = this.getItems(orders).toNumber();
|
|
|
|
|
|
|
|
|
|
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
|
|
|
|
|
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
|
|
|
|
|
|
|
|
|
|
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
|
|
|
|
|
const committedFunds = new Big(totalBuy).minus(totalSell);
|
|
|
|
|
const totalOfExcludedActivities = new Big(
|
|
|
|
|
this.getTotalByType(excludedActivities, userCurrency, 'BUY')
|
|
|
|
|
).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL'));
|
|
|
|
|
|
|
|
|
|
const cashDetailsWithExcludedAccounts =
|
|
|
|
|
await this.accountService.getCashDetails({
|
|
|
|
|
userId,
|
|
|
|
|
currency: userCurrency,
|
|
|
|
|
withExcludedAccounts: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const excludedBalanceInBaseCurrency = new Big(
|
|
|
|
|
cashDetailsWithExcludedAccounts.balanceInBaseCurrency
|
|
|
|
|
).minus(balanceInBaseCurrency);
|
|
|
|
|
|
|
|
|
|
const excludedAccountsAndActivities = excludedBalanceInBaseCurrency
|
|
|
|
|
.plus(totalOfExcludedActivities)
|
|
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
|
|
const netWorth = new Big(balanceInBaseCurrency)
|
|
|
|
|
.plus(performanceInformation.performance.currentValue)
|
|
|
|
|
.plus(items)
|
|
|
|
|
.plus(excludedAccountsAndActivities)
|
|
|
|
|
.toNumber();
|
|
|
|
|
|
|
|
|
|
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
|
|
|
|
|
|
|
|
|
const annualizedPerformancePercent = new PortfolioCalculator({
|
|
|
|
|
currency: userCurrency,
|
|
|
|
|
currentRateService: this.currentRateService,
|
|
|
|
|
orders: []
|
|
|
|
|
})
|
|
|
|
|
.getAnnualizedPerformancePercent({
|
|
|
|
|
daysInMarket,
|
|
|
|
|
netPerformancePercent: new Big(
|
|
|
|
|
performanceInformation.performance.currentNetPerformancePercent
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
?.toNumber();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...performanceInformation.performance,
|
|
|
|
|
annualizedPerformancePercent,
|
|
|
|
|
cash,
|
|
|
|
|
dividend,
|
|
|
|
|
excludedAccountsAndActivities,
|
|
|
|
|
fees,
|
|
|
|
|
firstOrderDate,
|
|
|
|
|
items,
|
|
|
|
|
netWorth,
|
|
|
|
|
totalBuy,
|
|
|
|
|
totalSell,
|
|
|
|
|
committedFunds: committedFunds.toNumber(),
|
|
|
|
|
emergencyFund: emergencyFund.toNumber(),
|
|
|
|
|
ordersCount: orders.filter((order) => {
|
|
|
|
|
return order.type === 'BUY' || order.type === 'SELL';
|
|
|
|
|
}).length
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getTransactionPoints({
|
|
|
|
|
filters,
|
|
|
|
|
includeDrafts = false,
|
|
|
|
|
userId
|
|
|
|
|
userId,
|
|
|
|
|
withExcludedAccounts
|
|
|
|
|
}: {
|
|
|
|
|
filters?: Filter[];
|
|
|
|
|
includeDrafts?: boolean;
|
|
|
|
|
userId: string;
|
|
|
|
|
withExcludedAccounts?: boolean;
|
|
|
|
|
}): Promise<{
|
|
|
|
|
transactionPoints: TransactionPoint[];
|
|
|
|
|
orders: OrderWithAccount[];
|
|
|
|
@ -1445,6 +1503,7 @@ export class PortfolioService {
|
|
|
|
|
includeDrafts,
|
|
|
|
|
userCurrency,
|
|
|
|
|
userId,
|
|
|
|
|
withExcludedAccounts,
|
|
|
|
|
types: ['BUY', 'SELL']
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@ -1496,17 +1555,22 @@ export class PortfolioService {
|
|
|
|
|
orders,
|
|
|
|
|
portfolioItemsNow,
|
|
|
|
|
userCurrency,
|
|
|
|
|
userId
|
|
|
|
|
userId,
|
|
|
|
|
withExcludedAccounts
|
|
|
|
|
}: {
|
|
|
|
|
filters?: Filter[];
|
|
|
|
|
orders: OrderWithAccount[];
|
|
|
|
|
portfolioItemsNow: { [p: string]: TimelinePosition };
|
|
|
|
|
userCurrency: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
withExcludedAccounts?: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
const accounts: PortfolioDetails['accounts'] = {};
|
|
|
|
|
|
|
|
|
|
let currentAccounts = [];
|
|
|
|
|
let currentAccounts: (Account & {
|
|
|
|
|
Order?: Order[];
|
|
|
|
|
Platform?: Platform;
|
|
|
|
|
})[] = [];
|
|
|
|
|
|
|
|
|
|
if (filters.length === 0) {
|
|
|
|
|
currentAccounts = await this.accountService.getAccounts(userId);
|
|
|
|
@ -1526,6 +1590,10 @@ export class PortfolioService {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentAccounts = currentAccounts.filter((account) => {
|
|
|
|
|
return withExcludedAccounts || account.isExcluded === false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const account of currentAccounts) {
|
|
|
|
|
const ordersByAccount = orders.filter(({ accountId }) => {
|
|
|
|
|
return accountId === account.id;
|
|
|
|
|