Browse Source

change getDetails to portfolio-calculator.ts

Co-authored-by: Thomas <dotsilver@gmail.com>
pull/239/head
Valentin Zickner 4 years ago
committed by Thomas
parent
commit
d23addb673
  1. 7
      apps/api/src/app/core/portfolio-calculator.spec.ts
  2. 7
      apps/api/src/app/core/portfolio-calculator.ts
  3. 9
      apps/api/src/app/portfolio/portfolio.controller.ts
  4. 4
      apps/api/src/app/portfolio/portfolio.module.ts
  5. 191
      apps/api/src/app/portfolio/portfolio.service.ts
  6. 15
      apps/api/src/services/interfaces/symbol-profile.interface.ts
  7. 64
      apps/api/src/services/symbol-profile.service.ts

7
apps/api/src/app/core/portfolio-calculator.spec.ts

@ -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: [
{ {

7
apps/api/src/app/core/portfolio-calculator.ts

@ -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),

9
apps/api/src/app/portfolio/portfolio.controller.ts

@ -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);

4
apps/api/src/app/portfolio/portfolio.module.ts

@ -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
] ]

191
apps/api/src/app/portfolio/portfolio.service.ts

@ -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

@ -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

@ -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…
Cancel
Save