Feature/extend data source eod historical data by asset class and isin (#1791)
* Extend EodHistoricalDataService * asset and asset sub class * isin * Update changelog
This commit is contained in:
parent
5db2faa17d
commit
c8ca82b803
@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,17 +17,21 @@
|
|||||||
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
|
||||||
<span i18n>Portfolio Summary</span>
|
<span i18n>Portfolio Summary</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>Portfolio Allocations</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>Performance Benchmarks</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>Allocations</span>
|
<span i18n>FIRE Calculator</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>Professional Data Provider</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>
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user