diff --git a/CHANGELOG.md b/CHANGELOG.md index 829c52612..13aee17fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Hid the pagination of tabs +- Improved the classification of assets - Improved the support for future transactions (drafts) - Optimized the accounts table for mobile - Upgraded `chart.js` from version `3.3.2` to `3.5.0` diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 333042bd1..1e954d71f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -38,12 +38,7 @@ import { } from '@ghostfolio/common/types'; import { Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; -import { - Currency, - DataSource, - Prisma, - Type as TypeOfOrder -} from '@prisma/client'; +import { Currency, DataSource, Type as TypeOfOrder } from '@prisma/client'; import Big from 'big.js'; import { endOfToday, @@ -239,6 +234,7 @@ export class PortfolioService { allocationInvestment: item.investment .div(currentPositions.totalInvestment) .toNumber(), + assetClass: symbolProfile.assetClass, countries: symbolProfile.countries, currency: item.currency, exchange: dataProviderResponse.exchange, @@ -252,7 +248,6 @@ export class PortfolioService { sectors: symbolProfile.sectors, symbol: item.symbol, transactionCount: item.transactionCount, - type: dataProviderResponse.type, value: value.toNumber() }; } @@ -498,6 +493,7 @@ export class PortfolioService { positions: positions.map((position) => { return { ...position, + assetClass: symbolProfileMap[position.symbol].assetClass, averagePrice: new Big(position.averagePrice).toNumber(), grossPerformance: position.grossPerformance?.toNumber() ?? null, grossPerformancePercentage: diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 8e7deffc7..cbd1923e5 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -134,18 +134,21 @@ export class DataGatheringService { const currentData = await this.dataProviderService.get(symbols); - for (const [symbol, { currency, dataSource, name }] of Object.entries( - currentData - )) { + for (const [ + symbol, + { assetClass, currency, dataSource, name } + ] of Object.entries(currentData)) { try { await this.prismaService.symbolProfile.upsert({ create: { + assetClass, currency, dataSource, name, symbol }, update: { + assetClass, currency, name }, 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 c9ef1f692..07998d76a 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 @@ -8,7 +8,7 @@ import { } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { AssetClass, DataSource } from '@prisma/client'; import * as bent from 'bent'; import { format } from 'date-fns'; import * as yahooFinance from 'yahoo-finance'; @@ -17,8 +17,7 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface' import { IDataProviderHistoricalResponse, IDataProviderResponse, - MarketState, - Type + MarketState } from '../../interfaces/interfaces'; import { IYahooFinanceHistoricalResponse, @@ -61,6 +60,7 @@ export class YahooFinanceService implements DataProviderInterface { const symbol = convertFromYahooSymbol(yahooSymbol); response[symbol] = { + assetClass: this.parseAssetClass(value.price?.quoteType), currency: parseCurrency(value.price?.currency), dataSource: DataSource.YAHOO, exchange: this.parseExchange(value.price?.exchangeName), @@ -69,8 +69,7 @@ export class YahooFinanceService implements DataProviderInterface { ? MarketState.open : MarketState.closed, marketPrice: value.price?.regularMarketPrice || 0, - name: value.price?.longName || value.price?.shortName || symbol, - type: this.parseType(this.getType(symbol, value)) + name: value.price?.longName || value.price?.shortName || symbol }; const url = value.summaryProfile?.website; @@ -203,14 +202,20 @@ export class YahooFinanceService implements DataProviderInterface { return aSymbol; } - private getType(aSymbol: string, aValue: IYahooFinanceQuoteResponse): Type { - if (isCrypto(aSymbol)) { - return Type.Cryptocurrency; - } else if (aValue.price?.quoteType.toLowerCase() === 'equity') { - return Type.Stock; + private parseAssetClass(aString: string): AssetClass { + let assetClass: AssetClass; + + switch (aString?.toLowerCase()) { + case 'cryptocurrency': + assetClass = AssetClass.CASH; + break; + case 'equity': + case 'etf': + assetClass = AssetClass.EQUITY; + break; } - return aValue.price?.quoteType.toLowerCase(); + return assetClass; } private parseExchange(aString: string): string { @@ -220,18 +225,6 @@ export class YahooFinanceService implements DataProviderInterface { return aString; } - - private parseType(aString: string): Type { - if (aString?.toLowerCase() === 'cryptocurrency') { - return Type.Cryptocurrency; - } else if (aString?.toLowerCase() === 'etf') { - return Type.ETF; - } else if (aString?.toLowerCase() === 'stock') { - return Type.Stock; - } - - return Type.Unknown; - } } export const convertFromYahooSymbol = (aSymbol: string) => { diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index 50fae5559..dac5b8213 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -1,5 +1,10 @@ -import { UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { Account, Currency, DataSource, SymbolProfile } from '@prisma/client'; +import { + Account, + AssetClass, + Currency, + DataSource, + SymbolProfile +} from '@prisma/client'; import { OrderType } from '../../models/order-type'; @@ -9,14 +14,6 @@ export const MarketState = { open: 'open' }; -export const Type = { - Cash: 'Cash', - Cryptocurrency: 'Cryptocurrency', - ETF: 'ETF', - Stock: 'Stock', - Unknown: UNKNOWN_KEY -}; - export interface IOrder { account: Account; currency: Currency; @@ -37,6 +34,7 @@ export interface IDataProviderHistoricalResponse { } export interface IDataProviderResponse { + assetClass?: AssetClass; currency: Currency; dataSource: DataSource; exchange?: string; @@ -45,7 +43,6 @@ export interface IDataProviderResponse { marketPrice: number; marketState: MarketState; name: string; - type?: Type; url?: string; } @@ -56,5 +53,3 @@ export interface IDataGatheringItem { } export type MarketState = typeof MarketState[keyof typeof MarketState]; - -export type Type = typeof Type[keyof typeof Type]; diff --git a/apps/api/src/services/interfaces/symbol-profile.interface.ts b/apps/api/src/services/interfaces/symbol-profile.interface.ts index f7bcc283b..304774d5c 100644 --- a/apps/api/src/services/interfaces/symbol-profile.interface.ts +++ b/apps/api/src/services/interfaces/symbol-profile.interface.ts @@ -1,8 +1,9 @@ import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; -import { Currency, DataSource } from '@prisma/client'; +import { AssetClass, Currency, DataSource } from '@prisma/client'; export interface EnhancedSymbolProfile { + assetClass: AssetClass; createdAt: Date; currency: Currency | null; dataSource: DataSource; diff --git a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts b/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts index bb7ac16ee..9472b1cfa 100644 --- a/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts +++ b/apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts @@ -10,4 +10,4 @@ import { PortfolioProportionChartComponent } from './portfolio-proportion-chart. imports: [CommonModule, NgxSkeletonLoaderModule], providers: [] }) -export class PortfolioProportionChartModule {} +export class GfPortfolioProportionChartModule {} diff --git a/apps/client/src/app/components/positions-table/positions-table.component.html b/apps/client/src/app/components/positions-table/positions-table.component.html index 8e9693a1f..144bfc933 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.html +++ b/apps/client/src/app/components/positions-table/positions-table.component.html @@ -83,10 +83,10 @@ *matRowDef="let row; columns: displayedColumns" mat-row [ngClass]="{ - 'cursor-pointer': !this.ignoreTypes.includes(row.type) + 'cursor-pointer': !this.ignoreAssetClasses.includes(row.assetClass) }" (click)=" - !this.ignoreTypes.includes(row.type) && + !this.ignoreAssetClasses.includes(row.assetClass) && onOpenPositionDialog({ symbol: row.symbol, title: row.name }) " > diff --git a/apps/client/src/app/components/positions-table/positions-table.component.ts b/apps/client/src/app/components/positions-table/positions-table.component.ts index 5ad58f4f2..c83c8e237 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.ts +++ b/apps/client/src/app/components/positions-table/positions-table.component.ts @@ -14,9 +14,8 @@ import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { ActivatedRoute, Router } from '@angular/router'; -import { Type } from '@ghostfolio/api/services/interfaces/interfaces'; import { PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { Order as OrderModel } from '@prisma/client'; +import { AssetClass, Order as OrderModel } from '@prisma/client'; import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -43,7 +42,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { public dataSource: MatTableDataSource = new MatTableDataSource(); public displayedColumns = []; - public ignoreTypes = [Type.Cash]; + public ignoreAssetClasses = [AssetClass.CASH.toString()]; public isLoading = true; public pageSize = 7; public routeQueryParams: Subscription; diff --git a/apps/client/src/app/components/positions/positions.component.ts b/apps/client/src/app/components/positions/positions.component.ts index 6d12ac133..b80700a77 100644 --- a/apps/client/src/app/components/positions/positions.component.ts +++ b/apps/client/src/app/components/positions/positions.component.ts @@ -5,11 +5,9 @@ import { OnChanges, OnInit } from '@angular/core'; -import { - MarketState, - Type -} from '@ghostfolio/api/services/interfaces/interfaces'; +import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { Position } from '@ghostfolio/common/interfaces'; +import { AssetClass } from '@prisma/client'; @Component({ selector: 'gf-positions', @@ -28,7 +26,7 @@ export class PositionsComponent implements OnChanges, OnInit { public positionsRest: Position[] = []; public positionsWithPriority: Position[] = []; - private ignoreTypes = [Type.Cash]; + private ignoreAssetClasses = [AssetClass.CASH.toString()]; public constructor() {} @@ -46,7 +44,7 @@ export class PositionsComponent implements OnChanges, OnInit { this.positionsWithPriority = []; for (const portfolioPosition of this.positions) { - if (this.ignoreTypes.includes(portfolioPosition.type)) { + if (this.ignoreAssetClasses.includes(portfolioPosition.assetClass)) { continue; } diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 6aaf81148..123e073fa 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -116,9 +116,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { for (const [symbol, position] of Object.entries(aPortfolioPositions)) { this.positions[symbol] = { + assetClass: position.assetClass, currency: position.currency, exchange: position.exchange, - type: position.type, value: aPeriod === 'original' ? position.allocationInvestment diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index 05f34d318..d35fb9e3b 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -12,28 +12,6 @@
-
- - - By Type - - - - - - -
@@ -59,7 +37,7 @@
- By Currency + By Asset Class - By Exchange + By Currency