From 0264b592b934760a25fd442c39876a89c6388f08 Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Wed, 16 Jun 2021 17:05:43 +0200 Subject: [PATCH] Feature/improve investments by sector (#172) * Improve investments analysis by sector * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/models/portfolio.ts | 14 +++++ .../yahoo-finance/yahoo-finance.service.ts | 63 +------------------ .../api/src/services/interfaces/interfaces.ts | 23 ------- .../tools/analysis/analysis-page.component.ts | 36 ++++++++++- .../pages/tools/analysis/analysis-page.html | 41 +++--------- .../portfolio-position.interface.ts | 4 +- .../src/lib/interfaces/sector.interface.ts | 4 ++ .../migration.sql | 2 + prisma/schema.prisma | 1 + prisma/seed.ts | 18 ++++++ 11 files changed, 91 insertions(+), 121 deletions(-) create mode 100644 libs/common/src/lib/interfaces/sector.interface.ts create mode 100644 prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 2adb77832..1229ca5a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Improved the pie chart: Investments by sector + ## 1.17.0 - 15.06.2021 ### Changed diff --git a/apps/api/src/models/portfolio.ts b/apps/api/src/models/portfolio.ts index 4d0c057e5..d081fd2c5 100644 --- a/apps/api/src/models/portfolio.ts +++ b/apps/api/src/models/portfolio.ts @@ -9,6 +9,7 @@ import { UserWithSettings } from '@ghostfolio/common/interfaces'; import { Country } from '@ghostfolio/common/interfaces/country.interface'; +import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { DateRange, OrderWithAccount } from '@ghostfolio/common/types'; import { Prisma } from '@prisma/client'; import { continents, countries } from 'countries-list'; @@ -210,6 +211,7 @@ export class Portfolio implements PortfolioInterface { symbols.forEach((symbol) => { const accounts: PortfolioPosition['accounts'] = {}; let countriesOfSymbol: Country[]; + let sectorsOfSymbol: Sector[]; const [portfolioItem] = portfolioItems; const ordersBySymbol = this.getOrders().filter((order) => { @@ -264,6 +266,17 @@ export class Portfolio implements PortfolioInterface { weight: weight as number }; }); + + sectorsOfSymbol = ( + (orderOfSymbol.getSymbolProfile()?.sectors as Prisma.JsonArray) ?? [] + ).map((sector) => { + const { name, weight } = sector as Prisma.JsonObject; + + return { + name: (name as string) ?? UNKNOWN_KEY, + weight: weight as number + }; + }); }); let now = portfolioItemsNow.positions[symbol].marketPrice; @@ -318,6 +331,7 @@ export class Portfolio implements PortfolioInterface { grossPerformancePercent: roundTo((now - before) / before, 4), investment: portfolioItem.positions[symbol].investment, quantity: portfolioItem.positions[symbol].quantity, + sectors: sectorsOfSymbol, transactionCount: portfolioItem.positions[symbol].transactionCount, value: this.exchangeRateDataService.toCurrency( portfolioItem.positions[symbol].quantity * now, diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index b22d7e7cf..250909aa0 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -12,9 +12,7 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface' import { IDataProviderHistoricalResponse, IDataProviderResponse, - Industry, MarketState, - Sector, Type } from '../../interfaces/interfaces'; import { @@ -70,16 +68,6 @@ export class YahooFinanceService implements DataProviderInterface { type: this.parseType(this.getType(symbol, value)) }; - const industry = this.parseIndustry(value.summaryProfile?.industry); - if (industry) { - response[symbol].industry = industry; - } - - const sector = this.parseSector(value.summaryProfile?.sector); - if (sector) { - response[symbol].sector = sector; - } - const url = value.summaryProfile?.website; if (url) { response[symbol].url = url; @@ -228,55 +216,6 @@ export class YahooFinanceService implements DataProviderInterface { return aString; } - private parseIndustry(aString: string): Industry { - if (aString === undefined) { - return undefined; - } - - if (aString?.toLowerCase() === 'auto manufacturers') { - return Industry.Automotive; - } else if (aString?.toLowerCase() === 'biotechnology') { - return Industry.Biotechnology; - } else if ( - aString?.toLowerCase() === 'drug manufacturers—specialty & generic' - ) { - return Industry.Pharmaceutical; - } else if ( - aString?.toLowerCase() === 'internet content & information' || - aString?.toLowerCase() === 'internet retail' - ) { - return Industry.Internet; - } else if (aString?.toLowerCase() === 'packaged foods') { - return Industry.Food; - } else if (aString?.toLowerCase() === 'software—application') { - return Industry.Software; - } - - return Industry.Unknown; - } - - private parseSector(aString: string): Sector { - if (aString === undefined) { - return undefined; - } - - if ( - aString?.toLowerCase() === 'consumer cyclical' || - aString?.toLowerCase() === 'consumer defensive' - ) { - return Sector.Consumer; - } else if (aString?.toLowerCase() === 'healthcare') { - return Sector.Healthcare; - } else if ( - aString?.toLowerCase() === 'communication services' || - aString?.toLowerCase() === 'technology' - ) { - return Sector.Technology; - } - - return Sector.Unknown; - } - private parseType(aString: string): Type { if (aString?.toLowerCase() === 'cryptocurrency') { return Type.Cryptocurrency; @@ -291,6 +230,6 @@ export class YahooFinanceService implements DataProviderInterface { } export const convertFromYahooSymbol = (aSymbol: string) => { - let symbol = aSymbol.replace('-', ''); + const symbol = aSymbol.replace('-', ''); return symbol.replace('=X', ''); }; diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index 0547471ae..b909c7e78 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -3,29 +3,12 @@ import { Account, Currency, DataSource, SymbolProfile } from '@prisma/client'; import { OrderType } from '../../models/order-type'; -export const Industry = { - Automotive: 'Automotive', - Biotechnology: 'Biotechnology', - Food: 'Food', - Internet: 'Internet', - Pharmaceutical: 'Pharmaceutical', - Software: 'Software', - Unknown: UNKNOWN_KEY -}; - export const MarketState = { closed: 'closed', delayed: 'delayed', open: 'open' }; -export const Sector = { - Consumer: 'Consumer', - Healthcare: 'Healthcare', - Technology: 'Technology', - Unknown: UNKNOWN_KEY -}; - export const Type = { Cryptocurrency: 'Cryptocurrency', ETF: 'ETF', @@ -55,13 +38,11 @@ export interface IDataProviderResponse { currency: Currency; dataSource: DataSource; exchange?: string; - industry?: Industry; marketChange?: number; marketChangePercent?: number; marketPrice: number; marketState: MarketState; name: string; - sector?: Sector; type?: Type; url?: string; } @@ -72,10 +53,6 @@ export interface IDataGatheringItem { symbol: string; } -export type Industry = typeof Industry[keyof typeof Industry]; - export type MarketState = typeof MarketState[keyof typeof MarketState]; -export type Sector = typeof Sector[keyof typeof Sector]; - export type Type = typeof Type[keyof typeof Type]; diff --git a/apps/client/src/app/pages/tools/analysis/analysis-page.component.ts b/apps/client/src/app/pages/tools/analysis/analysis-page.component.ts index a01097f0f..10bd7832d 100644 --- a/apps/client/src/app/pages/tools/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/tools/analysis/analysis-page.component.ts @@ -9,6 +9,7 @@ import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; +import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -39,6 +40,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { public portfolioPositions: { [symbol: string]: PortfolioPosition }; public positions: { [symbol: string]: any }; public positionsArray: PortfolioPosition[]; + public sectors: { + [name: string]: { name: string; value: number }; + }; public user: User; private unsubscribeSubject = new Subject(); @@ -118,13 +122,17 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { }; this.positions = {}; this.positionsArray = []; + this.sectors = { + [UNKNOWN_KEY]: { + name: UNKNOWN_KEY, + value: 0 + } + }; for (const [symbol, position] of Object.entries(aPortfolioPositions)) { this.positions[symbol] = { currency: position.currency, exchange: position.exchange, - industry: position.industry, - sector: position.sector, type: position.type, value: aPeriod === 'original' @@ -188,6 +196,30 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { ? this.portfolioPositions[symbol].investment : this.portfolioPositions[symbol].value; } + + if (position.sectors.length > 0) { + for (const sector of position.sectors) { + const { name, weight } = sector; + + if (this.sectors[name]?.value) { + this.sectors[name].value += weight * position.value; + } else { + this.sectors[name] = { + name, + value: + weight * + (aPeriod === 'original' + ? this.portfolioPositions[symbol].investment + : this.portfolioPositions[symbol].value) + }; + } + } + } else { + this.sectors[UNKNOWN_KEY].value += + aPeriod === 'original' + ? this.portfolioPositions[symbol].investment + : this.portfolioPositions[symbol].value; + } } } diff --git a/apps/client/src/app/pages/tools/analysis/analysis-page.html b/apps/client/src/app/pages/tools/analysis/analysis-page.html index 4b724cad7..9a4162597 100644 --- a/apps/client/src/app/pages/tools/analysis/analysis-page.html +++ b/apps/client/src/app/pages/tools/analysis/analysis-page.html @@ -61,30 +61,7 @@
- By Sector - - - - - - -
-
- - - By Industry + By Currency @@ -107,7 +83,7 @@
- By Currency + By Exchange - By Exchange + By Sector diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index ad120cbf4..537c091fd 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -2,6 +2,7 @@ import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { Currency } from '@prisma/client'; import { Country } from './country.interface'; +import { Sector } from './sector.interface'; export interface PortfolioPosition { accounts: { @@ -14,7 +15,6 @@ export interface PortfolioPosition { exchange?: string; grossPerformance: number; grossPerformancePercent: number; - industry?: string; investment: number; marketChange?: number; marketChangePercent?: number; @@ -22,7 +22,7 @@ export interface PortfolioPosition { marketState: MarketState; name: string; quantity: number; - sector?: string; + sectors: Sector[]; transactionCount: number; symbol: string; type?: string; diff --git a/libs/common/src/lib/interfaces/sector.interface.ts b/libs/common/src/lib/interfaces/sector.interface.ts new file mode 100644 index 000000000..a17fb9869 --- /dev/null +++ b/libs/common/src/lib/interfaces/sector.interface.ts @@ -0,0 +1,4 @@ +export interface Sector { + name: string; + weight: number; +} diff --git a/prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql b/prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql new file mode 100644 index 000000000..39b7baba7 --- /dev/null +++ b/prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f7ed0bf47..85b89c75e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -120,6 +120,7 @@ model SymbolProfile { name String? Order Order[] updatedAt DateTime @updatedAt + sectors Json? symbol String @@unique([dataSource, symbol]) diff --git a/prisma/seed.ts b/prisma/seed.ts index 057698875..4c47d7386 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -142,18 +142,21 @@ async function main() { countries: [{ code: 'US', weight: 1 }], dataSource: DataSource.YAHOO, id: '2bd26362-136e-411c-b578-334084b4cdcc', + sectors: [{ name: 'Consumer Cyclical', weight: 1 }], symbol: 'AMZN' }, { countries: null, dataSource: DataSource.YAHOO, id: 'fdc42ea6-1321-44f5-9fb0-d7f1f2cf9b1e', + sectors: null, symbol: 'BTCUSD' }, { countries: [{ code: 'US', weight: 1 }], dataSource: DataSource.YAHOO, id: 'd1ee9681-fb21-4f99-a3b7-afd4fc04df2e', + sectors: [{ name: 'Consumer Cyclical', weight: 1 }], symbol: 'TSLA' }, { @@ -164,6 +167,21 @@ async function main() { ], dataSource: DataSource.YAHOO, id: '7d9c8540-061e-4e7e-b019-0d0f4a84e796', + sectors: [ + { name: 'Technology', weight: 0.31393799999999955 }, + { name: 'Consumer Cyclical', weight: 0.149224 }, + { name: 'Financials', weight: 0.11716100000000002 }, + { name: 'Healthcare', weight: 0.13285199999999994 }, + { name: 'Consumer Staples', weight: 0.053919000000000016 }, + { name: 'Energy', weight: 0.025529999999999997 }, + { name: 'Telecommunications', weight: 0.012579 }, + { name: 'Industrials', weight: 0.09526399999999995 }, + { name: 'Utilities', weight: 0.024791999999999988 }, + { name: 'Materials', weight: 0.027664 }, + { name: 'Real Estate', weight: 0.03239999999999998 }, + { name: 'Communication', weight: 0.0036139999999999996 }, + { name: 'Other', weight: 0.000218 } + ], symbol: 'VTI' } ],