Feature/extend data source eod historical data by asset class and isin (#1791)

* Extend EodHistoricalDataService

* asset and asset sub class
* isin

* Update changelog
pull/1793/head
Thomas Kaul 2 years ago committed by GitHub
parent 5db2faa17d
commit c8ca82b803
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added support for asset and asset sub class to the `EOD_HISTORICAL_DATA` data source type
- Added `isin` to the asset profile model
### Changed ### Changed
- Improved the language localization for _Gather Data_ - Improved the language localization for _Gather Data_

@ -254,6 +254,7 @@ export class ImportService {
countries: null, countries: null,
createdAt: undefined, createdAt: undefined,
id: undefined, id: undefined,
isin: null,
name: null, name: null,
scraperConfiguration: null, scraperConfiguration: null,
sectors: null, sectors: null,

@ -1,15 +1,18 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Controller, Controller,
Get, Get,
HttpException, HttpException,
Inject,
Param, Param,
Query, Query,
UseGuards, UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -21,7 +24,10 @@ import { SymbolService } from './symbol.service';
@Controller('symbol') @Controller('symbol')
export class SymbolController { export class SymbolController {
public constructor(private readonly symbolService: SymbolService) {} public constructor(
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolService: SymbolService
) {}
/** /**
* Must be before /:symbol * Must be before /:symbol
@ -33,7 +39,10 @@ export class SymbolController {
@Query() { query = '' } @Query() { query = '' }
): Promise<{ items: LookupItem[] }> { ): Promise<{ items: LookupItem[] }> {
try { try {
return this.symbolService.lookup(query.toLowerCase()); return this.symbolService.lookup({
query: query.toLowerCase(),
user: this.request.user
});
} catch { } catch {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),

@ -5,7 +5,10 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import {
HistoricalDataItem,
UserWithSettings
} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { format, subDays } from 'date-fns'; import { format, subDays } from 'date-fns';
@ -79,15 +82,24 @@ export class SymbolService {
}; };
} }
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> { public async lookup({
query,
user
}: {
query: string;
user: UserWithSettings;
}): Promise<{ items: LookupItem[] }> {
const results: { items: LookupItem[] } = { items: [] }; const results: { items: LookupItem[] } = { items: [] };
if (!aQuery) { if (!query) {
return results; return results;
} }
try { try {
const { items } = await this.dataProviderService.search(aQuery); const { items } = await this.dataProviderService.search({
query,
user
});
results.items = items; results.items = items;
return results; return results;
} catch (error) { } catch (error) {

@ -152,10 +152,11 @@ export class DataGatheringService {
countries, countries,
currency, currency,
dataSource, dataSource,
isin,
name, name,
sectors, sectors,
url url
} = assetProfiles[symbol]; } = assetProfile;
try { try {
await this.prismaService.symbolProfile.upsert({ await this.prismaService.symbolProfile.upsert({
@ -165,6 +166,7 @@ export class DataGatheringService {
countries, countries,
currency, currency,
dataSource, dataSource,
isin,
name, name,
sectors, sectors,
symbol, symbol,
@ -175,6 +177,7 @@ export class DataGatheringService {
assetSubClass, assetSubClass,
countries, countries,
currency, currency,
isin,
name, name,
sectors, sectors,
url url

@ -8,6 +8,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { UserWithSettings } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource, MarketData, SymbolProfile } from '@prisma/client'; import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
@ -260,18 +261,33 @@ export class DataProviderService {
return response; return response;
} }
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search({
query,
user
}: {
query: string;
user: UserWithSettings;
}): Promise<{ items: LookupItem[] }> {
const promises: Promise<{ items: LookupItem[] }>[] = []; const promises: Promise<{ items: LookupItem[] }>[] = [];
let lookupItems: LookupItem[] = []; let lookupItems: LookupItem[] = [];
if (aQuery?.length < 2) { if (query?.length < 2) {
return { items: lookupItems }; return { items: lookupItems };
} }
for (const dataSource of this.configurationService.get('DATA_SOURCES')) { let dataSources = this.configurationService.get('DATA_SOURCES');
promises.push(
this.getDataProvider(DataSource[dataSource]).search(aQuery) if (
); this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic'
) {
dataSources = dataSources.filter((dataSource) => {
return !this.isPremiumDataSource(DataSource[dataSource]);
});
}
for (const dataSource of dataSources) {
promises.push(this.getDataProvider(DataSource[dataSource]).search(query));
} }
const searchResults = await Promise.all(promises); const searchResults = await Promise.all(promises);
@ -305,4 +321,9 @@ export class DataProviderService {
throw new Error('No data provider has been found.'); throw new Error('No data provider has been found.');
} }
private isPremiumDataSource(aDataSource: DataSource) {
const premiumDataSources: DataSource[] = [DataSource.EOD_HISTORICAL_DATA];
return premiumDataSources.includes(aDataSource);
}
} }

@ -8,7 +8,12 @@ import {
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import {
AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
} from '@prisma/client';
import bent from 'bent'; import bent from 'bent';
import { format, isToday } from 'date-fns'; import { format, isToday } from 'date-fns';
@ -30,12 +35,15 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getAssetProfile( public async getAssetProfile(
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
const { items } = await this.search(aSymbol); const [searchResult] = await this.getSearchResult(aSymbol);
return { return {
currency: items[0]?.currency, assetClass: searchResult?.assetClass,
assetSubClass: searchResult?.assetSubClass,
currency: searchResult?.currency,
dataSource: this.getName(), dataSource: this.getName(),
name: items[0]?.name isin: searchResult?.isin,
name: searchResult?.name
}; };
} }
@ -156,7 +164,27 @@ export class EodHistoricalDataService implements DataProviderInterface {
} }
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
let items: LookupItem[] = []; const searchResult = await this.getSearchResult(aQuery);
return {
items: searchResult
.filter(({ symbol }) => {
return !symbol.toLowerCase().endsWith('forex');
})
.map(({ currency, dataSource, name, symbol }) => {
return { currency, dataSource, name, symbol };
})
};
}
private async getSearchResult(aQuery: string): Promise<
(LookupItem & {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
isin: string;
})[]
> {
let searchResult = [];
try { try {
const get = bent( const get = bent(
@ -167,10 +195,25 @@ export class EodHistoricalDataService implements DataProviderInterface {
); );
const response = await get(); const response = await get();
items = response.map( searchResult = response.map(
({ Code, Currency: currency, Exchange, Name: name }) => { ({
Code,
Currency: currency,
Exchange,
ISIN: isin,
Name: name,
Type
}) => {
const { assetClass, assetSubClass } = this.parseAssetClass({
Exchange,
Type
});
return { return {
assetClass,
assetSubClass,
currency, currency,
isin,
name, name,
dataSource: this.getName(), dataSource: this.getName(),
symbol: `${Code}.${Exchange}` symbol: `${Code}.${Exchange}`
@ -181,6 +224,41 @@ export class EodHistoricalDataService implements DataProviderInterface {
Logger.error(error, 'EodHistoricalDataService'); Logger.error(error, 'EodHistoricalDataService');
} }
return { items }; return searchResult;
}
private parseAssetClass({
Exchange,
Type
}: {
Exchange: string;
Type: string;
}): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {
let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
switch (Type?.toLowerCase()) {
case 'common stock':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
break;
case 'currency':
assetClass = AssetClass.CASH;
if (Exchange?.toLowerCase() === 'cc') {
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
}
break;
case 'etf':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
break;
}
return { assetClass, assetSubClass };
} }
} }

@ -19,16 +19,20 @@
</li> </li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Performance Benchmarks</span> <span i18n>Portfolio Allocations</span>
</li> </li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Allocations</span> <span i18n>Performance Benchmarks</span>
</li> </li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>FIRE Calculator</span> <span i18n>FIRE Calculator</span>
</li> </li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Professional Data Provider</span>
</li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>and more Features...</span> <span i18n>and more Features...</span>

@ -280,6 +280,13 @@
<ion-icon name="information-circle-outline"></ion-icon> <ion-icon name="information-circle-outline"></ion-icon>
</span> </span>
</li> </li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span i18n>Professional Data Provider</span>
</li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon <ion-icon
class="mr-1" class="mr-1"

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "isin" TEXT;

@ -119,6 +119,7 @@ model SymbolProfile {
currency String currency String
dataSource DataSource dataSource DataSource
id String @id @default(uuid()) id String @id @default(uuid())
isin String?
name String? name String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
scraperConfiguration Json? scraperConfiguration Json?

Loading…
Cancel
Save