change getDetails to portfolio-calculator.ts
Co-authored-by: Thomas <dotsilver@gmail.com>
This commit is contained in:
parent
fb15cebb64
commit
d23addb673
apps/api/src
@ -637,6 +637,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('657.62'),
|
currentValue: new Big('657.62'),
|
||||||
grossPerformance: new Big('-61.84'),
|
grossPerformance: new Big('-61.84'),
|
||||||
grossPerformancePercentage: new Big('-0.08595335390431712673'),
|
grossPerformancePercentage: new Big('-0.08595335390431712673'),
|
||||||
|
totalInvestment: new Big('719.46'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('719.46'),
|
averagePrice: new Big('719.46'),
|
||||||
@ -675,6 +676,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('657.62'),
|
currentValue: new Big('657.62'),
|
||||||
grossPerformance: new Big('-61.84'),
|
grossPerformance: new Big('-61.84'),
|
||||||
grossPerformancePercentage: new Big('-0.08595335390431712673'),
|
grossPerformancePercentage: new Big('-0.08595335390431712673'),
|
||||||
|
totalInvestment: new Big('719.46'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('719.46'),
|
averagePrice: new Big('719.46'),
|
||||||
@ -713,6 +715,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('657.62'),
|
currentValue: new Big('657.62'),
|
||||||
grossPerformance: new Big('-9.04'),
|
grossPerformance: new Big('-9.04'),
|
||||||
grossPerformancePercentage: new Big('-0.01356013560135601356'),
|
grossPerformancePercentage: new Big('-0.01356013560135601356'),
|
||||||
|
totalInvestment: new Big('719.46'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('719.46'),
|
averagePrice: new Big('719.46'),
|
||||||
@ -751,6 +754,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('4871.5'),
|
currentValue: new Big('4871.5'),
|
||||||
grossPerformance: new Big('240.4'),
|
grossPerformance: new Big('240.4'),
|
||||||
grossPerformancePercentage: new Big('0.08839407904876477102'),
|
grossPerformancePercentage: new Big('0.08839407904876477102'),
|
||||||
|
totalInvestment: new Big('4460.95'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('178.438'),
|
averagePrice: new Big('178.438'),
|
||||||
@ -831,6 +835,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('3897.2'),
|
currentValue: new Big('3897.2'),
|
||||||
grossPerformance: new Big('303.2'),
|
grossPerformance: new Big('303.2'),
|
||||||
grossPerformancePercentage: new Big('0.27537838148272398344'),
|
grossPerformancePercentage: new Big('0.27537838148272398344'),
|
||||||
|
totalInvestment: new Big('2923.7'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('146.185'),
|
averagePrice: new Big('146.185'),
|
||||||
@ -904,6 +909,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('1192327.999656600298238721'),
|
currentValue: new Big('1192327.999656600298238721'),
|
||||||
grossPerformance: new Big('92327.999656600898394721'),
|
grossPerformance: new Big('92327.999656600898394721'),
|
||||||
grossPerformancePercentage: new Big('0.09788498099999947809'),
|
grossPerformancePercentage: new Big('0.09788498099999947809'),
|
||||||
|
totalInvestment: new Big('1100000'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542
|
averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542
|
||||||
@ -992,6 +998,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('517'),
|
currentValue: new Big('517'),
|
||||||
grossPerformance: new Big('17'), // 517 - 500
|
grossPerformance: new Big('17'), // 517 - 500
|
||||||
grossPerformancePercentage: new Big('0.034'), // ((200 * 0.025) + (300 * 0.04)) / (200 + 300) = 3.4%
|
grossPerformancePercentage: new Big('0.034'), // ((200 * 0.025) + (300 * 0.04)) / (200 + 300) = 3.4%
|
||||||
|
totalInvestment: new Big('500'),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
|
@ -117,6 +117,7 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance: Big;
|
grossPerformance: Big;
|
||||||
grossPerformancePercentage: Big;
|
grossPerformancePercentage: Big;
|
||||||
currentValue: Big;
|
currentValue: Big;
|
||||||
|
totalInvestment: Big;
|
||||||
}> {
|
}> {
|
||||||
if (!this.transactionPoints?.length) {
|
if (!this.transactionPoints?.length) {
|
||||||
return {
|
return {
|
||||||
@ -124,7 +125,8 @@ export class PortfolioCalculator {
|
|||||||
positions: [],
|
positions: [],
|
||||||
grossPerformance: new Big(0),
|
grossPerformance: new Big(0),
|
||||||
grossPerformancePercentage: new Big(0),
|
grossPerformancePercentage: new Big(0),
|
||||||
currentValue: new Big(0)
|
currentValue: new Big(0),
|
||||||
|
totalInvestment: new Big(0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,6 +379,7 @@ export class PortfolioCalculator {
|
|||||||
) {
|
) {
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
let currentValue = new Big(0);
|
let currentValue = new Big(0);
|
||||||
|
let totalInvestment = new Big(0);
|
||||||
let grossPerformance = new Big(0);
|
let grossPerformance = new Big(0);
|
||||||
let grossPerformancePercentage = new Big(0);
|
let grossPerformancePercentage = new Big(0);
|
||||||
let completeInitialValue = new Big(0);
|
let completeInitialValue = new Big(0);
|
||||||
@ -384,6 +387,7 @@ export class PortfolioCalculator {
|
|||||||
currentValue = currentValue.add(
|
currentValue = currentValue.add(
|
||||||
new Big(currentPosition.marketPrice).mul(currentPosition.quantity)
|
new Big(currentPosition.marketPrice).mul(currentPosition.quantity)
|
||||||
);
|
);
|
||||||
|
totalInvestment = totalInvestment.add(currentPosition.investment);
|
||||||
if (currentPosition.grossPerformance) {
|
if (currentPosition.grossPerformance) {
|
||||||
grossPerformance = grossPerformance.plus(
|
grossPerformance = grossPerformance.plus(
|
||||||
currentPosition.grossPerformance
|
currentPosition.grossPerformance
|
||||||
@ -411,6 +415,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
currentValue,
|
currentValue,
|
||||||
|
totalInvestment,
|
||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage:
|
grossPerformancePercentage:
|
||||||
grossPerformancePercentage.div(completeInitialValue),
|
grossPerformancePercentage.div(completeInitialValue),
|
||||||
|
@ -149,12 +149,11 @@ export class PortfolioController {
|
|||||||
this.request.user.id
|
this.request.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const portfolio = await this.portfolioService.createPortfolio(
|
|
||||||
impersonationUserId || this.request.user.id
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
details = await portfolio.getDetails(range);
|
details = await this.portfolioService.getDetails(
|
||||||
|
impersonationUserId,
|
||||||
|
range
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { PortfolioController } from './portfolio.controller';
|
import { PortfolioController } from './portfolio.controller';
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RedisCacheModule],
|
imports: [RedisCacheModule],
|
||||||
@ -35,12 +36,13 @@ import { PortfolioService } from './portfolio.service';
|
|||||||
ExchangeRateDataService,
|
ExchangeRateDataService,
|
||||||
GhostfolioScraperApiService,
|
GhostfolioScraperApiService,
|
||||||
ImpersonationService,
|
ImpersonationService,
|
||||||
|
MarketDataService,
|
||||||
OrderService,
|
OrderService,
|
||||||
PortfolioService,
|
PortfolioService,
|
||||||
PrismaService,
|
PrismaService,
|
||||||
RakutenRapidApiService,
|
RakutenRapidApiService,
|
||||||
RulesService,
|
RulesService,
|
||||||
MarketDataService,
|
SymbolProfileService,
|
||||||
UserService,
|
UserService,
|
||||||
YahooFinanceService
|
YahooFinanceService
|
||||||
]
|
]
|
||||||
|
@ -11,17 +11,22 @@ import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import { IOrder } from '@ghostfolio/api/services/interfaces/interfaces';
|
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 { RulesService } from '@ghostfolio/api/services/rules.service';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
PortfolioOverview,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
Position
|
PortfolioPosition,
|
||||||
|
Position,
|
||||||
|
TimelinePosition
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
import {
|
||||||
|
DateRange,
|
||||||
|
OrderWithAccount,
|
||||||
|
RequestWithUser
|
||||||
|
} from '@ghostfolio/common/types';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
@ -52,6 +57,10 @@ import {
|
|||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
PortfolioPositionDetail
|
PortfolioPositionDetail
|
||||||
} from './interfaces/portfolio-position-detail.interface';
|
} from './interfaces/portfolio-position-detail.interface';
|
||||||
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||||
|
import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PortfolioService {
|
export class PortfolioService {
|
||||||
@ -65,7 +74,8 @@ export class PortfolioService {
|
|||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly redisCacheService: RedisCacheService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
private readonly rulesService: RulesService,
|
private readonly rulesService: RulesService,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService,
|
||||||
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async createPortfolio(aUserId: string): Promise<Portfolio> {
|
public async createPortfolio(aUserId: string): Promise<Portfolio> {
|
||||||
@ -158,7 +168,7 @@ export class PortfolioService {
|
|||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
|
|
||||||
const transactionPoints = await this.getTransactionPoints(userId);
|
const { transactionPoints } = await this.getTransactionPoints(userId);
|
||||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
if (transactionPoints.length === 0) {
|
if (transactionPoints.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@ -221,19 +231,98 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDetails(
|
||||||
|
aImpersonationId: string,
|
||||||
|
aDateRange: DateRange = 'max'
|
||||||
|
): Promise<{ [symbol: string]: PortfolioPosition }> {
|
||||||
|
const userId = await this.getUserId(aImpersonationId);
|
||||||
|
|
||||||
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
|
this.currentRateService,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
|
||||||
|
const { transactionPoints, orders } = await this.getTransactionPoints(
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (transactionPoints?.length <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||||
|
|
||||||
|
const portfolioStart = parseDate(transactionPoints[0].date);
|
||||||
|
const startDate = this.getStartDate(aDateRange, portfolioStart);
|
||||||
|
const currentPositions = await portfolioCalculator.getCurrentPositions(
|
||||||
|
startDate
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentPositions.hasErrors) {
|
||||||
|
throw new Error('Missing information');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: { [symbol: string]: PortfolioPosition } = {};
|
||||||
|
const totalValue = currentPositions.currentValue;
|
||||||
|
|
||||||
|
const symbols = currentPositions.positions.map(
|
||||||
|
(position) => position.symbol
|
||||||
|
);
|
||||||
|
|
||||||
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
|
this.dataProviderService.get(symbols),
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const accounts = this.getAccounts(orders, portfolioItemsNow, userCurrency);
|
||||||
|
|
||||||
|
for (const item of currentPositions.positions) {
|
||||||
|
const value = item.quantity.mul(item.marketPrice);
|
||||||
|
const symbolProfile = symbolProfileMap[item.symbol];
|
||||||
|
const dataProviderResponse = dataProviderResponses[item.symbol];
|
||||||
|
result[item.symbol] = {
|
||||||
|
accounts,
|
||||||
|
allocationCurrent: value.div(totalValue).toNumber(),
|
||||||
|
allocationInvestment: item.investment
|
||||||
|
.div(currentPositions.totalInvestment)
|
||||||
|
.toNumber(),
|
||||||
|
countries: symbolProfile.countries,
|
||||||
|
currency: item.currency,
|
||||||
|
exchange: dataProviderResponse.exchange,
|
||||||
|
grossPerformance: item.grossPerformance.toNumber(),
|
||||||
|
grossPerformancePercent: item.grossPerformancePercentage.toNumber(),
|
||||||
|
investment: item.investment.toNumber(),
|
||||||
|
marketPrice: item.marketPrice,
|
||||||
|
marketState: dataProviderResponse.marketState,
|
||||||
|
name: item.name,
|
||||||
|
quantity: item.quantity.toNumber(),
|
||||||
|
sectors: symbolProfile.sectors,
|
||||||
|
symbol: item.symbol,
|
||||||
|
transactionCount: item.transactionCount,
|
||||||
|
type: dataProviderResponse.type,
|
||||||
|
value: value.toNumber()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async getPosition(
|
public async getPosition(
|
||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aSymbol: string
|
aSymbol: string
|
||||||
): Promise<PortfolioPositionDetail> {
|
): Promise<PortfolioPositionDetail> {
|
||||||
const impersonationUserId =
|
const userId = await this.getUserId(aImpersonationId);
|
||||||
await this.impersonationService.validateImpersonationId(
|
const portfolio = await this.createPortfolio(userId);
|
||||||
aImpersonationId,
|
|
||||||
this.request.user.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const portfolio = await this.createPortfolio(
|
|
||||||
impersonationUserId || this.request.user.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const position = portfolio.getPositions(new Date())[aSymbol];
|
const position = portfolio.getPositions(new Date())[aSymbol];
|
||||||
|
|
||||||
@ -396,20 +485,14 @@ export class PortfolioService {
|
|||||||
aImpersonationId: string,
|
aImpersonationId: string,
|
||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): Promise<{ hasErrors: boolean; positions: Position[] }> {
|
): Promise<{ hasErrors: boolean; positions: Position[] }> {
|
||||||
const impersonationUserId =
|
const userId = await this.getUserId(aImpersonationId);
|
||||||
await this.impersonationService.validateImpersonationId(
|
|
||||||
aImpersonationId,
|
|
||||||
this.request.user.id
|
|
||||||
);
|
|
||||||
|
|
||||||
const userId = impersonationUserId || this.request.user.id;
|
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
|
|
||||||
const transactionPoints = await this.getTransactionPoints(userId);
|
const { transactionPoints } = await this.getTransactionPoints(userId);
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
if (transactionPoints?.length <= 0) {
|
||||||
return {
|
return {
|
||||||
@ -461,7 +544,7 @@ export class PortfolioService {
|
|||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
|
|
||||||
const transactionPoints = await this.getTransactionPoints(userId);
|
const { transactionPoints } = await this.getTransactionPoints(userId);
|
||||||
|
|
||||||
if (transactionPoints?.length <= 0) {
|
if (transactionPoints?.length <= 0) {
|
||||||
return {
|
return {
|
||||||
@ -521,11 +604,14 @@ export class PortfolioService {
|
|||||||
return portfolioStart;
|
return portfolioStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getTransactionPoints(userId: string) {
|
private async getTransactionPoints(userId: string): Promise<{
|
||||||
|
transactionPoints: TransactionPoint[];
|
||||||
|
orders: OrderWithAccount[];
|
||||||
|
}> {
|
||||||
const orders = await this.getOrders(userId);
|
const orders = await this.getOrders(userId);
|
||||||
|
|
||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
return [];
|
return { transactionPoints: [], orders: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||||
@ -543,7 +629,10 @@ export class PortfolioService {
|
|||||||
this.request.user.Settings.currency
|
this.request.user.Settings.currency
|
||||||
);
|
);
|
||||||
portfolioCalculator.computeTransactionPoints(portfolioOrders);
|
portfolioCalculator.computeTransactionPoints(portfolioOrders);
|
||||||
return portfolioCalculator.getTransactionPoints();
|
return {
|
||||||
|
transactionPoints: portfolioCalculator.getTransactionPoints(),
|
||||||
|
orders
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
|
private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) {
|
||||||
@ -593,6 +682,44 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getAccounts(
|
||||||
|
orders: OrderWithAccount[],
|
||||||
|
portfolioItemsNow: { [p: string]: TimelinePosition },
|
||||||
|
userCurrency
|
||||||
|
) {
|
||||||
|
const accounts: PortfolioPosition['accounts'] = {};
|
||||||
|
for (const order of orders) {
|
||||||
|
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
||||||
|
order.quantity * portfolioItemsNow[order.symbol].marketPrice,
|
||||||
|
order.currency,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
let originalValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
||||||
|
order.quantity * order.unitPrice,
|
||||||
|
order.currency,
|
||||||
|
userCurrency
|
||||||
|
);
|
||||||
|
|
||||||
|
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] = {
|
||||||
|
current: currentValueOfSymbol,
|
||||||
|
original: originalValueOfSymbol
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
private getOrders(aUserId: string) {
|
private getOrders(aUserId: string) {
|
||||||
return this.orderService.orders({
|
return this.orderService.orders({
|
||||||
include: {
|
include: {
|
||||||
@ -605,4 +732,14 @@ export class PortfolioService {
|
|||||||
where: { userId: aUserId }
|
where: { userId: aUserId }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getUserId(aImpersonationId: string) {
|
||||||
|
const impersonationUserId =
|
||||||
|
await this.impersonationService.validateImpersonationId(
|
||||||
|
aImpersonationId,
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return impersonationUserId || this.request.user.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
15
apps/api/src/services/interfaces/symbol-profile.interface.ts
Normal file
15
apps/api/src/services/interfaces/symbol-profile.interface.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface EnhancedSymbolProfile {
|
||||||
|
createdAt: Date;
|
||||||
|
currency: Currency | null;
|
||||||
|
dataSource: DataSource;
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
updatedAt: Date;
|
||||||
|
symbol: string;
|
||||||
|
countries: Country[];
|
||||||
|
sectors: Sector[];
|
||||||
|
}
|
64
apps/api/src/services/symbol-profile.service.ts
Normal file
64
apps/api/src/services/symbol-profile.service.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma, SymbolProfile } from '@prisma/client';
|
||||||
|
import { continents, countries } from 'countries-list';
|
||||||
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||||
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SymbolProfileService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
public async getSymbolProfiles(
|
||||||
|
symbols: string[]
|
||||||
|
): Promise<EnhancedSymbolProfile[]> {
|
||||||
|
return this.prisma.symbolProfile
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
symbol: {
|
||||||
|
in: symbols
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSymbols(symbolProfiles: SymbolProfile[]): EnhancedSymbolProfile[] {
|
||||||
|
return symbolProfiles.map((symbolProfile) => ({
|
||||||
|
...symbolProfile,
|
||||||
|
countries: this.getCountries(symbolProfile),
|
||||||
|
sectors: this.getSectors(symbolProfile)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCountries(symbolProfile: SymbolProfile): Country[] {
|
||||||
|
return ((symbolProfile?.countries as Prisma.JsonArray) ?? []).map(
|
||||||
|
(country) => {
|
||||||
|
const { code, weight } = country as Prisma.JsonObject;
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: code as string,
|
||||||
|
continent:
|
||||||
|
continents[countries[code as string]?.continent] ?? UNKNOWN_KEY,
|
||||||
|
name: countries[code as string]?.name ?? UNKNOWN_KEY,
|
||||||
|
weight: weight as number
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSectors(symbolProfile: SymbolProfile): Sector[] {
|
||||||
|
return ((symbolProfile?.sectors as Prisma.JsonArray) ?? []).map(
|
||||||
|
(sector) => {
|
||||||
|
const { name, weight } = sector as Prisma.JsonObject;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: (name as string) ?? UNKNOWN_KEY,
|
||||||
|
weight: weight as number
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user