Feature/replace type with asset class (#274)

* Improved the asset classification
  * Add assetClass to symbolProfile
  * Remove type from position

* Update changelog
pull/279/head
Thomas 3 years ago committed by GitHub
parent 178166d86b
commit 80d043729d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

@ -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) => {

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

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

@ -10,4 +10,4 @@ import { PortfolioProportionChartComponent } from './portfolio-proportion-chart.
imports: [CommonModule, NgxSkeletonLoaderModule],
providers: []
})
export class PortfolioProportionChartModule {}
export class GfPortfolioProportionChartModule {}

@ -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 })
"
></tr>

@ -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<PortfolioPosition> =
new MatTableDataSource();
public displayedColumns = [];
public ignoreTypes = [Type.Cash];
public ignoreAssetClasses = [AssetClass.CASH.toString()];
public isLoading = true;
public pageSize = 7;
public routeQueryParams: Subscription;

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

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

@ -12,28 +12,6 @@
</div>
</div>
<div class="proportion-charts row">
<div class="col-md-6">
<mat-card class="mb-3">
<mat-card-header class="w-100">
<mat-card-title i18n>By Type</mat-card-title>
<gf-toggle
[defaultValue]="period"
[isLoading]="false"
[options]="periodOptions"
(change)="onChangePeriod($event.value)"
></gf-toggle>
</mat-card-header>
<mat-card-content>
<gf-portfolio-proportion-chart
key="type"
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="true"
[locale]="user?.settings?.locale"
[positions]="positions"
></gf-portfolio-proportion-chart>
</mat-card-content>
</mat-card>
</div>
<div class="col-md-6">
<mat-card class="mb-3">
<mat-card-header class="w-100">
@ -59,7 +37,7 @@
<div class="col-md-6">
<mat-card class="mb-3">
<mat-card-header class="w-100">
<mat-card-title i18n>By Currency</mat-card-title>
<mat-card-title i18n>By Asset Class</mat-card-title>
<gf-toggle
[defaultValue]="period"
[isLoading]="false"
@ -69,7 +47,7 @@
</mat-card-header>
<mat-card-content>
<gf-portfolio-proportion-chart
key="currency"
key="assetClass"
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="true"
[locale]="user?.settings?.locale"
@ -81,7 +59,7 @@
<div class="col-md-6">
<mat-card class="mb-3">
<mat-card-header class="w-100">
<mat-card-title i18n>By Exchange</mat-card-title>
<mat-card-title i18n>By Currency</mat-card-title>
<gf-toggle
[defaultValue]="period"
[isLoading]="false"
@ -91,7 +69,7 @@
</mat-card-header>
<mat-card-content>
<gf-portfolio-proportion-chart
key="exchange"
key="currency"
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="true"
[locale]="user?.settings?.locale"

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { PortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
@ -15,11 +15,11 @@ import { AllocationsPageComponent } from './allocations-page.component';
imports: [
AllocationsPageRoutingModule,
CommonModule,
GfPortfolioProportionChartModule,
GfPositionsTableModule,
GfToggleModule,
GfWorldMapChartModule,
MatCardModule,
PortfolioProportionChartModule
MatCardModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

@ -1,5 +1,5 @@
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
import { Currency } from '@prisma/client';
import { AssetClass, Currency } from '@prisma/client';
import { Country } from './country.interface';
import { Sector } from './sector.interface';
@ -10,6 +10,7 @@ export interface PortfolioPosition {
};
allocationCurrent: number;
allocationInvestment: number;
assetClass?: AssetClass;
countries: Country[];
currency: Currency;
exchange?: string;

@ -1,10 +1,8 @@
import {
MarketState,
Type
} from '@ghostfolio/api/services/interfaces/interfaces';
import { Currency } from '@prisma/client';
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
import { AssetClass, Currency } from '@prisma/client';
export interface Position {
assetClass: AssetClass;
averagePrice: number;
currency: Currency;
firstBuyDate: string;
@ -18,6 +16,5 @@ export interface Position {
quantity: number;
symbol: string;
transactionCount: number;
type?: Type;
url?: string;
}

@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "AssetClass" AS ENUM ('CASH', 'COMMODITY', 'EQUITY');
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";

@ -116,14 +116,15 @@ model Settings {
}
model SymbolProfile {
assetClass AssetClass?
countries Json?
createdAt DateTime @default(now())
createdAt DateTime @default(now())
currency Currency?
dataSource DataSource
id String @id @default(uuid())
id String @id @default(uuid())
name String?
Order Order[]
updatedAt DateTime @updatedAt
updatedAt DateTime @updatedAt
sectors Json?
symbol String
@ -166,6 +167,12 @@ enum AccountType {
SECURITIES
}
enum AssetClass {
CASH
COMMODITY
EQUITY
}
enum Currency {
CHF
EUR

Loading…
Cancel
Save