Browse Source

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 4 years ago
committed by GitHub
parent
commit
80d043729d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 10
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 9
      apps/api/src/services/data-gathering.service.ts
  4. 39
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  5. 21
      apps/api/src/services/interfaces/interfaces.ts
  6. 3
      apps/api/src/services/interfaces/symbol-profile.interface.ts
  7. 2
      apps/client/src/app/components/portfolio-proportion-chart/portfolio-proportion-chart.module.ts
  8. 4
      apps/client/src/app/components/positions-table/positions-table.component.html
  9. 5
      apps/client/src/app/components/positions-table/positions-table.component.ts
  10. 10
      apps/client/src/app/components/positions/positions.component.ts
  11. 2
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  12. 30
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  13. 6
      apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts
  14. 3
      libs/common/src/lib/interfaces/portfolio-position.interface.ts
  15. 9
      libs/common/src/lib/interfaces/position.interface.ts
  16. 5
      prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql
  17. 13
      prisma/schema.prisma

1
CHANGELOG.md

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Hid the pagination of tabs - Hid the pagination of tabs
- Improved the classification of assets
- Improved the support for future transactions (drafts) - Improved the support for future transactions (drafts)
- Optimized the accounts table for mobile - Optimized the accounts table for mobile
- Upgraded `chart.js` from version `3.3.2` to `3.5.0` - Upgraded `chart.js` from version `3.3.2` to `3.5.0`

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

@ -38,12 +38,7 @@ import {
} from '@ghostfolio/common/types'; } 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 { import { Currency, DataSource, Type as TypeOfOrder } from '@prisma/client';
Currency,
DataSource,
Prisma,
Type as TypeOfOrder
} from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
endOfToday, endOfToday,
@ -239,6 +234,7 @@ export class PortfolioService {
allocationInvestment: item.investment allocationInvestment: item.investment
.div(currentPositions.totalInvestment) .div(currentPositions.totalInvestment)
.toNumber(), .toNumber(),
assetClass: symbolProfile.assetClass,
countries: symbolProfile.countries, countries: symbolProfile.countries,
currency: item.currency, currency: item.currency,
exchange: dataProviderResponse.exchange, exchange: dataProviderResponse.exchange,
@ -252,7 +248,6 @@ export class PortfolioService {
sectors: symbolProfile.sectors, sectors: symbolProfile.sectors,
symbol: item.symbol, symbol: item.symbol,
transactionCount: item.transactionCount, transactionCount: item.transactionCount,
type: dataProviderResponse.type,
value: value.toNumber() value: value.toNumber()
}; };
} }
@ -498,6 +493,7 @@ export class PortfolioService {
positions: positions.map((position) => { positions: positions.map((position) => {
return { return {
...position, ...position,
assetClass: symbolProfileMap[position.symbol].assetClass,
averagePrice: new Big(position.averagePrice).toNumber(), averagePrice: new Big(position.averagePrice).toNumber(),
grossPerformance: position.grossPerformance?.toNumber() ?? null, grossPerformance: position.grossPerformance?.toNumber() ?? null,
grossPerformancePercentage: grossPerformancePercentage:

9
apps/api/src/services/data-gathering.service.ts

@ -134,18 +134,21 @@ export class DataGatheringService {
const currentData = await this.dataProviderService.get(symbols); const currentData = await this.dataProviderService.get(symbols);
for (const [symbol, { currency, dataSource, name }] of Object.entries( for (const [
currentData symbol,
)) { { assetClass, currency, dataSource, name }
] of Object.entries(currentData)) {
try { try {
await this.prismaService.symbolProfile.upsert({ await this.prismaService.symbolProfile.upsert({
create: { create: {
assetClass,
currency, currency,
dataSource, dataSource,
name, name,
symbol symbol
}, },
update: { update: {
assetClass,
currency, currency,
name name
}, },

39
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -8,7 +8,7 @@ import {
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { AssetClass, DataSource } from '@prisma/client';
import * as bent from 'bent'; import * as bent from 'bent';
import { format } from 'date-fns'; import { format } from 'date-fns';
import * as yahooFinance from 'yahoo-finance'; import * as yahooFinance from 'yahoo-finance';
@ -17,8 +17,7 @@ import { DataProviderInterface } from '../../interfaces/data-provider.interface'
import { import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse, IDataProviderResponse,
MarketState, MarketState
Type
} from '../../interfaces/interfaces'; } from '../../interfaces/interfaces';
import { import {
IYahooFinanceHistoricalResponse, IYahooFinanceHistoricalResponse,
@ -61,6 +60,7 @@ export class YahooFinanceService implements DataProviderInterface {
const symbol = convertFromYahooSymbol(yahooSymbol); const symbol = convertFromYahooSymbol(yahooSymbol);
response[symbol] = { response[symbol] = {
assetClass: this.parseAssetClass(value.price?.quoteType),
currency: parseCurrency(value.price?.currency), currency: parseCurrency(value.price?.currency),
dataSource: DataSource.YAHOO, dataSource: DataSource.YAHOO,
exchange: this.parseExchange(value.price?.exchangeName), exchange: this.parseExchange(value.price?.exchangeName),
@ -69,8 +69,7 @@ export class YahooFinanceService implements DataProviderInterface {
? MarketState.open ? MarketState.open
: MarketState.closed, : MarketState.closed,
marketPrice: value.price?.regularMarketPrice || 0, marketPrice: value.price?.regularMarketPrice || 0,
name: value.price?.longName || value.price?.shortName || symbol, name: value.price?.longName || value.price?.shortName || symbol
type: this.parseType(this.getType(symbol, value))
}; };
const url = value.summaryProfile?.website; const url = value.summaryProfile?.website;
@ -203,14 +202,20 @@ export class YahooFinanceService implements DataProviderInterface {
return aSymbol; return aSymbol;
} }
private getType(aSymbol: string, aValue: IYahooFinanceQuoteResponse): Type { private parseAssetClass(aString: string): AssetClass {
if (isCrypto(aSymbol)) { let assetClass: AssetClass;
return Type.Cryptocurrency;
} else if (aValue.price?.quoteType.toLowerCase() === 'equity') { switch (aString?.toLowerCase()) {
return Type.Stock; 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 { private parseExchange(aString: string): string {
@ -220,18 +225,6 @@ export class YahooFinanceService implements DataProviderInterface {
return aString; 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) => { export const convertFromYahooSymbol = (aSymbol: string) => {

21
apps/api/src/services/interfaces/interfaces.ts

@ -1,5 +1,10 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import {
import { Account, Currency, DataSource, SymbolProfile } from '@prisma/client'; Account,
AssetClass,
Currency,
DataSource,
SymbolProfile
} from '@prisma/client';
import { OrderType } from '../../models/order-type'; import { OrderType } from '../../models/order-type';
@ -9,14 +14,6 @@ export const MarketState = {
open: 'open' open: 'open'
}; };
export const Type = {
Cash: 'Cash',
Cryptocurrency: 'Cryptocurrency',
ETF: 'ETF',
Stock: 'Stock',
Unknown: UNKNOWN_KEY
};
export interface IOrder { export interface IOrder {
account: Account; account: Account;
currency: Currency; currency: Currency;
@ -37,6 +34,7 @@ export interface IDataProviderHistoricalResponse {
} }
export interface IDataProviderResponse { export interface IDataProviderResponse {
assetClass?: AssetClass;
currency: Currency; currency: Currency;
dataSource: DataSource; dataSource: DataSource;
exchange?: string; exchange?: string;
@ -45,7 +43,6 @@ export interface IDataProviderResponse {
marketPrice: number; marketPrice: number;
marketState: MarketState; marketState: MarketState;
name: string; name: string;
type?: Type;
url?: string; url?: string;
} }
@ -56,5 +53,3 @@ export interface IDataGatheringItem {
} }
export type MarketState = typeof MarketState[keyof typeof MarketState]; export type MarketState = typeof MarketState[keyof typeof MarketState];
export type Type = typeof Type[keyof typeof Type];

3
apps/api/src/services/interfaces/symbol-profile.interface.ts

@ -1,8 +1,9 @@
import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.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 { export interface EnhancedSymbolProfile {
assetClass: AssetClass;
createdAt: Date; createdAt: Date;
currency: Currency | null; currency: Currency | null;
dataSource: DataSource; dataSource: DataSource;

2
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], imports: [CommonModule, NgxSkeletonLoaderModule],
providers: [] providers: []
}) })
export class PortfolioProportionChartModule {} export class GfPortfolioProportionChartModule {}

4
apps/client/src/app/components/positions-table/positions-table.component.html

@ -83,10 +83,10 @@
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
mat-row mat-row
[ngClass]="{ [ngClass]="{
'cursor-pointer': !this.ignoreTypes.includes(row.type) 'cursor-pointer': !this.ignoreAssetClasses.includes(row.assetClass)
}" }"
(click)=" (click)="
!this.ignoreTypes.includes(row.type) && !this.ignoreAssetClasses.includes(row.assetClass) &&
onOpenPositionDialog({ symbol: row.symbol, title: row.name }) onOpenPositionDialog({ symbol: row.symbol, title: row.name })
" "
></tr> ></tr>

5
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 { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Type } from '@ghostfolio/api/services/interfaces/interfaces';
import { PortfolioPosition } from '@ghostfolio/common/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 { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -43,7 +42,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
public dataSource: MatTableDataSource<PortfolioPosition> = public dataSource: MatTableDataSource<PortfolioPosition> =
new MatTableDataSource(); new MatTableDataSource();
public displayedColumns = []; public displayedColumns = [];
public ignoreTypes = [Type.Cash]; public ignoreAssetClasses = [AssetClass.CASH.toString()];
public isLoading = true; public isLoading = true;
public pageSize = 7; public pageSize = 7;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;

10
apps/client/src/app/components/positions/positions.component.ts

@ -5,11 +5,9 @@ import {
OnChanges, OnChanges,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
MarketState,
Type
} from '@ghostfolio/api/services/interfaces/interfaces';
import { Position } from '@ghostfolio/common/interfaces'; import { Position } from '@ghostfolio/common/interfaces';
import { AssetClass } from '@prisma/client';
@Component({ @Component({
selector: 'gf-positions', selector: 'gf-positions',
@ -28,7 +26,7 @@ export class PositionsComponent implements OnChanges, OnInit {
public positionsRest: Position[] = []; public positionsRest: Position[] = [];
public positionsWithPriority: Position[] = []; public positionsWithPriority: Position[] = [];
private ignoreTypes = [Type.Cash]; private ignoreAssetClasses = [AssetClass.CASH.toString()];
public constructor() {} public constructor() {}
@ -46,7 +44,7 @@ export class PositionsComponent implements OnChanges, OnInit {
this.positionsWithPriority = []; this.positionsWithPriority = [];
for (const portfolioPosition of this.positions) { for (const portfolioPosition of this.positions) {
if (this.ignoreTypes.includes(portfolioPosition.type)) { if (this.ignoreAssetClasses.includes(portfolioPosition.assetClass)) {
continue; continue;
} }

2
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)) { for (const [symbol, position] of Object.entries(aPortfolioPositions)) {
this.positions[symbol] = { this.positions[symbol] = {
assetClass: position.assetClass,
currency: position.currency, currency: position.currency,
exchange: position.exchange, exchange: position.exchange,
type: position.type,
value: value:
aPeriod === 'original' aPeriod === 'original'
? position.allocationInvestment ? position.allocationInvestment

30
apps/client/src/app/pages/portfolio/allocations/allocations-page.html

@ -12,28 +12,6 @@
</div> </div>
</div> </div>
<div class="proportion-charts row"> <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"> <div class="col-md-6">
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-header class="w-100"> <mat-card-header class="w-100">
@ -59,7 +37,7 @@
<div class="col-md-6"> <div class="col-md-6">
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-header class="w-100"> <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 <gf-toggle
[defaultValue]="period" [defaultValue]="period"
[isLoading]="false" [isLoading]="false"
@ -69,7 +47,7 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
key="currency" key="assetClass"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="true" [isInPercent]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
@ -81,7 +59,7 @@
<div class="col-md-6"> <div class="col-md-6">
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-header class="w-100"> <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 <gf-toggle
[defaultValue]="period" [defaultValue]="period"
[isLoading]="false" [isLoading]="false"
@ -91,7 +69,7 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
key="exchange" key="currency"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="true" [isInPercent]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"

6
apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; 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 { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.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: [ imports: [
AllocationsPageRoutingModule, AllocationsPageRoutingModule,
CommonModule, CommonModule,
GfPortfolioProportionChartModule,
GfPositionsTableModule, GfPositionsTableModule,
GfToggleModule, GfToggleModule,
GfWorldMapChartModule, GfWorldMapChartModule,
MatCardModule, MatCardModule
PortfolioProportionChartModule
], ],
providers: [], providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]

3
libs/common/src/lib/interfaces/portfolio-position.interface.ts

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

9
libs/common/src/lib/interfaces/position.interface.ts

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

5
prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql

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

13
prisma/schema.prisma

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

Loading…
Cancel
Save