Feature/migrate to yahoo finance2 (#722)
* Migrate to yahoo-finance2 * Add support for mutual funds * Add url to symbol profile * Clean up
This commit is contained in:
parent
6a4f1c0188
commit
c02bcd9bd8
15
CHANGELOG.md
15
CHANGELOG.md
@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for mutual funds
|
||||||
|
- Added the url to the symbol profile model
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Migrated from `yahoo-finance` to `yahoo-finance2`
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:migrate`)
|
||||||
|
|
||||||
## 1.120.0 - 25.02.2022
|
## 1.120.0 - 25.02.2022
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -125,19 +125,19 @@ export class ImportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataSource !== 'MANUAL') {
|
if (dataSource !== 'MANUAL') {
|
||||||
const result = await this.dataProviderService.get([
|
const quotes = await this.dataProviderService.getQuotes([
|
||||||
{ dataSource, symbol }
|
{ dataSource, symbol }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (result[symbol] === undefined) {
|
if (quotes[symbol] === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
`orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result[symbol].currency !== currency) {
|
if (quotes[symbol].currency !== currency) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`orders.${index}.currency ("${currency}") does not match with "${result[symbol].currency}"`
|
`orders.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ export class CurrentRateService {
|
|||||||
const today = resetHours(new Date());
|
const today = resetHours(new Date());
|
||||||
promises.push(
|
promises.push(
|
||||||
this.dataProviderService
|
this.dataProviderService
|
||||||
.get(dataGatheringItems)
|
.getQuotes(dataGatheringItems)
|
||||||
.then((dataResultProvider) => {
|
.then((dataResultProvider) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const dataGatheringItem of dataGatheringItems) {
|
for (const dataGatheringItem of dataGatheringItems) {
|
||||||
|
@ -327,7 +327,7 @@ export class PortfolioServiceNew {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.get(dataGatheringItems),
|
this.dataProviderService.getQuotes(dataGatheringItems),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -358,7 +358,6 @@ export class PortfolioServiceNew {
|
|||||||
countries: symbolProfile.countries,
|
countries: symbolProfile.countries,
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
dataSource: symbolProfile.dataSource,
|
dataSource: symbolProfile.dataSource,
|
||||||
exchange: dataProviderResponse.exchange,
|
|
||||||
grossPerformance: item.grossPerformance?.toNumber() ?? 0,
|
grossPerformance: item.grossPerformance?.toNumber() ?? 0,
|
||||||
grossPerformancePercent:
|
grossPerformancePercent:
|
||||||
item.grossPerformancePercentage?.toNumber() ?? 0,
|
item.grossPerformancePercentage?.toNumber() ?? 0,
|
||||||
@ -578,7 +577,7 @@ export class PortfolioServiceNew {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const currentData = await this.dataProviderService.get([
|
const currentData = await this.dataProviderService.getQuotes([
|
||||||
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
||||||
]);
|
]);
|
||||||
const marketPrice = currentData[aSymbol]?.marketPrice;
|
const marketPrice = currentData[aSymbol]?.marketPrice;
|
||||||
@ -679,7 +678,7 @@ export class PortfolioServiceNew {
|
|||||||
const symbols = positions.map((position) => position.symbol);
|
const symbols = positions.map((position) => position.symbol);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.get(dataGatheringItem),
|
this.dataProviderService.getQuotes(dataGatheringItem),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.get(dataGatheringItems),
|
this.dataProviderService.getQuotes(dataGatheringItems),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -346,7 +346,6 @@ export class PortfolioService {
|
|||||||
countries: symbolProfile.countries,
|
countries: symbolProfile.countries,
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
dataSource: symbolProfile.dataSource,
|
dataSource: symbolProfile.dataSource,
|
||||||
exchange: dataProviderResponse.exchange,
|
|
||||||
grossPerformance: item.grossPerformance?.toNumber() ?? 0,
|
grossPerformance: item.grossPerformance?.toNumber() ?? 0,
|
||||||
grossPerformancePercent:
|
grossPerformancePercent:
|
||||||
item.grossPerformancePercentage?.toNumber() ?? 0,
|
item.grossPerformancePercentage?.toNumber() ?? 0,
|
||||||
@ -552,9 +551,10 @@ export class PortfolioService {
|
|||||||
SymbolProfile,
|
SymbolProfile,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
averagePrice: averagePrice.toNumber(),
|
averagePrice: averagePrice.toNumber(),
|
||||||
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
grossPerformancePercent:
|
||||||
|
position.grossPerformancePercentage?.toNumber(),
|
||||||
historicalData: historicalDataArray,
|
historicalData: historicalDataArray,
|
||||||
netPerformancePercent: position.netPerformancePercentage.toNumber(),
|
netPerformancePercent: position.netPerformancePercentage?.toNumber(),
|
||||||
quantity: quantity.toNumber(),
|
quantity: quantity.toNumber(),
|
||||||
value: this.exchangeRateDataService.toCurrency(
|
value: this.exchangeRateDataService.toCurrency(
|
||||||
quantity.mul(marketPrice).toNumber(),
|
quantity.mul(marketPrice).toNumber(),
|
||||||
@ -563,7 +563,7 @@ export class PortfolioService {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const currentData = await this.dataProviderService.get([
|
const currentData = await this.dataProviderService.getQuotes([
|
||||||
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
{ dataSource: DataSource.YAHOO, symbol: aSymbol }
|
||||||
]);
|
]);
|
||||||
const marketPrice = currentData[aSymbol]?.marketPrice;
|
const marketPrice = currentData[aSymbol]?.marketPrice;
|
||||||
@ -660,7 +660,7 @@ export class PortfolioService {
|
|||||||
const symbols = positions.map((position) => position.symbol);
|
const symbols = positions.map((position) => position.symbol);
|
||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.get(dataGatheringItem),
|
this.dataProviderService.getQuotes(dataGatheringItem),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -27,8 +27,10 @@ export class SymbolService {
|
|||||||
dataGatheringItem: IDataGatheringItem;
|
dataGatheringItem: IDataGatheringItem;
|
||||||
includeHistoricalData?: number;
|
includeHistoricalData?: number;
|
||||||
}): Promise<SymbolItem> {
|
}): Promise<SymbolItem> {
|
||||||
const response = await this.dataProviderService.get([dataGatheringItem]);
|
const quotes = await this.dataProviderService.getQuotes([
|
||||||
const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {};
|
dataGatheringItem
|
||||||
|
]);
|
||||||
|
const { currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {};
|
||||||
|
|
||||||
if (dataGatheringItem.dataSource && marketPrice) {
|
if (dataGatheringItem.dataSource && marketPrice) {
|
||||||
let historicalData: HistoricalDataItem[] = [];
|
let historicalData: HistoricalDataItem[] = [];
|
||||||
|
@ -220,32 +220,41 @@ export class DataGatheringService {
|
|||||||
Logger.log('Profile data gathering has been started.');
|
Logger.log('Profile data gathering has been started.');
|
||||||
console.time('data-gathering-profile');
|
console.time('data-gathering-profile');
|
||||||
|
|
||||||
let dataGatheringItems = aDataGatheringItems;
|
let dataGatheringItems = aDataGatheringItems?.filter(
|
||||||
|
(dataGatheringItem) => {
|
||||||
|
return dataGatheringItem.dataSource !== 'MANUAL';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!dataGatheringItems) {
|
if (!dataGatheringItems) {
|
||||||
dataGatheringItems = await this.getSymbolsProfileData();
|
dataGatheringItems = await this.getSymbolsProfileData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentData = await this.dataProviderService.get(dataGatheringItems);
|
const assetProfiles = await this.dataProviderService.getAssetProfiles(
|
||||||
|
dataGatheringItems
|
||||||
|
);
|
||||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||||
dataGatheringItems.map(({ symbol }) => {
|
dataGatheringItems.map(({ symbol }) => {
|
||||||
return symbol;
|
return symbol;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [symbol, response] of Object.entries(currentData)) {
|
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
|
||||||
const symbolMapping = symbolProfiles.find((symbolProfile) => {
|
const symbolMapping = symbolProfiles.find((symbolProfile) => {
|
||||||
return symbolProfile.symbol === symbol;
|
return symbolProfile.symbol === symbol;
|
||||||
})?.symbolMapping;
|
})?.symbolMapping;
|
||||||
|
|
||||||
for (const dataEnhancer of this.dataEnhancers) {
|
for (const dataEnhancer of this.dataEnhancers) {
|
||||||
try {
|
try {
|
||||||
currentData[symbol] = await dataEnhancer.enhance({
|
assetProfiles[symbol] = await dataEnhancer.enhance({
|
||||||
response,
|
response: assetProfile,
|
||||||
symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol
|
symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`Failed to enhance data for symbol ${symbol}`, error);
|
Logger.error(
|
||||||
|
`Failed to enhance data for symbol ${symbol} by ${dataEnhancer.getName()}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,8 +265,9 @@ export class DataGatheringService {
|
|||||||
currency,
|
currency,
|
||||||
dataSource,
|
dataSource,
|
||||||
name,
|
name,
|
||||||
sectors
|
sectors,
|
||||||
} = currentData[symbol];
|
url
|
||||||
|
} = assetProfiles[symbol];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.prismaService.symbolProfile.upsert({
|
await this.prismaService.symbolProfile.upsert({
|
||||||
@ -269,7 +279,8 @@ export class DataGatheringService {
|
|||||||
dataSource,
|
dataSource,
|
||||||
name,
|
name,
|
||||||
sectors,
|
sectors,
|
||||||
symbol
|
symbol,
|
||||||
|
url
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
assetClass,
|
assetClass,
|
||||||
@ -277,7 +288,8 @@ export class DataGatheringService {
|
|||||||
countries,
|
countries,
|
||||||
currency,
|
currency,
|
||||||
name,
|
name,
|
||||||
sectors
|
sectors,
|
||||||
|
url
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
dataSource_symbol: {
|
dataSource_symbol: {
|
||||||
@ -300,6 +312,10 @@ export class DataGatheringService {
|
|||||||
let symbolCounter = 0;
|
let symbolCounter = 0;
|
||||||
|
|
||||||
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
||||||
|
if (dataSource === 'MANUAL') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
this.dataGatheringProgress = symbolCounter / aSymbolsWithStartDate.length;
|
this.dataGatheringProgress = symbolCounter / aSymbolsWithStartDate.length;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -347,7 +363,7 @@ export class DataGatheringService {
|
|||||||
} catch {}
|
} catch {}
|
||||||
} else {
|
} else {
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
`Failed to gather data for symbol ${symbol} at ${format(
|
`Failed to gather data for symbol ${symbol} from ${dataSource} at ${format(
|
||||||
currentDate,
|
currentDate,
|
||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
)}.`
|
)}.`
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { isAfter, isBefore, parse } from 'date-fns';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '../../interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
|
import { isAfter, isBefore, parse } from 'date-fns';
|
||||||
|
|
||||||
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
||||||
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
||||||
|
|
||||||
@ -29,25 +29,23 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
return !!this.configurationService.get('ALPHA_VANTAGE_API_KEY');
|
return !!this.configurationService.get('ALPHA_VANTAGE_API_KEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async getAssetProfile(
|
||||||
aSymbols: string[]
|
aSymbol: string
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<Partial<SymbolProfile>> {
|
||||||
return {};
|
return {
|
||||||
|
dataSource: this.getName()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbols: string[],
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date
|
||||||
): Promise<{
|
): Promise<{
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}> {
|
}> {
|
||||||
if (aSymbols.length <= 0) {
|
const symbol = aSymbol;
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const symbol = aSymbols[0];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historicalData: {
|
const historicalData: {
|
||||||
@ -88,6 +86,12 @@ export class AlphaVantageService implements DataProviderInterface {
|
|||||||
return DataSource.ALPHA_VANTAGE;
|
return DataSource.ALPHA_VANTAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getQuotes(
|
||||||
|
aSymbols: string[]
|
||||||
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
const result = await this.alphaVantage.data.search(aQuery);
|
const result = await this.alphaVantage.data.search(aQuery);
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||||
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import bent from 'bent';
|
import bent from 'bent';
|
||||||
|
|
||||||
const getJSON = bent('json');
|
const getJSON = bent('json');
|
||||||
@ -21,9 +23,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
response,
|
response,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
response: IDataProviderResponse;
|
response: Partial<SymbolProfile>;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}): Promise<IDataProviderResponse> {
|
}): Promise<Partial<SymbolProfile>> {
|
||||||
if (
|
if (
|
||||||
!(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF')
|
!(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF')
|
||||||
) {
|
) {
|
||||||
@ -40,7 +42,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.countries || response.countries.length === 0) {
|
if (
|
||||||
|
!response.countries ||
|
||||||
|
(response.countries as unknown as Country[]).length === 0
|
||||||
|
) {
|
||||||
response.countries = [];
|
response.countries = [];
|
||||||
for (const [name, value] of Object.entries<any>(holdings.countries)) {
|
for (const [name, value] of Object.entries<any>(holdings.countries)) {
|
||||||
let countryCode: string;
|
let countryCode: string;
|
||||||
@ -65,7 +70,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.sectors || response.sectors.length === 0) {
|
if (
|
||||||
|
!response.sectors ||
|
||||||
|
(response.sectors as unknown as Sector[]).length === 0
|
||||||
|
) {
|
||||||
response.sectors = [];
|
response.sectors = [];
|
||||||
for (const [name, value] of Object.entries<any>(holdings.sectors)) {
|
for (const [name, value] of Object.entries<any>(holdings.sectors)) {
|
||||||
response.sectors.push({
|
response.sectors.push({
|
||||||
|
@ -10,7 +10,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|||||||
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 { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
||||||
import { format, isValid } from 'date-fns';
|
import { format, isValid } from 'date-fns';
|
||||||
import { groupBy, isEmpty } from 'lodash';
|
import { groupBy, isEmpty } from 'lodash';
|
||||||
|
|
||||||
@ -23,42 +23,6 @@ export class DataProviderService {
|
|||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(items: IDataGatheringItem[]): Promise<{
|
|
||||||
[symbol: string]: IDataProviderResponse;
|
|
||||||
}> {
|
|
||||||
const response: {
|
|
||||||
[symbol: string]: IDataProviderResponse;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource);
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
for (const [dataSource, dataGatheringItems] of Object.entries(
|
|
||||||
itemsGroupedByDataSource
|
|
||||||
)) {
|
|
||||||
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
|
||||||
return dataGatheringItem.symbol;
|
|
||||||
});
|
|
||||||
|
|
||||||
const promise = Promise.resolve(
|
|
||||||
this.getDataProvider(DataSource[dataSource]).get(symbols)
|
|
||||||
);
|
|
||||||
|
|
||||||
promises.push(
|
|
||||||
promise.then((result) => {
|
|
||||||
for (const [symbol, dataProviderResponse] of Object.entries(result)) {
|
|
||||||
response[symbol] = dataProviderResponse;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aItems: IDataGatheringItem[],
|
aItems: IDataGatheringItem[],
|
||||||
aGranularity: Granularity = 'month',
|
aGranularity: Granularity = 'month',
|
||||||
@ -144,7 +108,7 @@ export class DataProviderService {
|
|||||||
if (dataProvider.canHandle(symbol)) {
|
if (dataProvider.canHandle(symbol)) {
|
||||||
promises.push(
|
promises.push(
|
||||||
dataProvider
|
dataProvider
|
||||||
.getHistorical([symbol], undefined, from, to)
|
.getHistorical(symbol, undefined, from, to)
|
||||||
.then((data) => ({ data: data?.[symbol], symbol }))
|
.then((data) => ({ data: data?.[symbol], symbol }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -158,6 +122,82 @@ export class DataProviderService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPrimaryDataSource(): DataSource {
|
||||||
|
return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAssetProfiles(items: IDataGatheringItem[]): Promise<{
|
||||||
|
[symbol: string]: Partial<SymbolProfile>;
|
||||||
|
}> {
|
||||||
|
const response: {
|
||||||
|
[symbol: string]: Partial<SymbolProfile>;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource);
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (const [dataSource, dataGatheringItems] of Object.entries(
|
||||||
|
itemsGroupedByDataSource
|
||||||
|
)) {
|
||||||
|
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
||||||
|
return dataGatheringItem.symbol;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const symbol of symbols) {
|
||||||
|
const promise = Promise.resolve(
|
||||||
|
this.getDataProvider(DataSource[dataSource]).getAssetProfile(symbol)
|
||||||
|
);
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
promise.then((symbolProfile) => {
|
||||||
|
response[symbol] = symbolProfile;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getQuotes(items: IDataGatheringItem[]): Promise<{
|
||||||
|
[symbol: string]: IDataProviderResponse;
|
||||||
|
}> {
|
||||||
|
const response: {
|
||||||
|
[symbol: string]: IDataProviderResponse;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource);
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (const [dataSource, dataGatheringItems] of Object.entries(
|
||||||
|
itemsGroupedByDataSource
|
||||||
|
)) {
|
||||||
|
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
||||||
|
return dataGatheringItem.symbol;
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = Promise.resolve(
|
||||||
|
this.getDataProvider(DataSource[dataSource]).getQuotes(symbols)
|
||||||
|
);
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
promise.then((result) => {
|
||||||
|
for (const [symbol, dataProviderResponse] of Object.entries(result)) {
|
||||||
|
response[symbol] = dataProviderResponse;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
const promises: Promise<{ items: LookupItem[] }>[] = [];
|
const promises: Promise<{ items: LookupItem[] }>[] = [];
|
||||||
let lookupItems: LookupItem[] = [];
|
let lookupItems: LookupItem[] = [];
|
||||||
@ -184,10 +224,6 @@ export class DataProviderService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPrimaryDataSource(): DataSource {
|
|
||||||
return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')];
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDataProvider(providerName: DataSource) {
|
private getDataProvider(providerName: DataSource) {
|
||||||
for (const dataProviderInterface of this.dataProviderInterfaces) {
|
for (const dataProviderInterface of this.dataProviderInterfaces) {
|
||||||
if (dataProviderInterface.getName() === providerName) {
|
if (dataProviderInterface.getName() === providerName) {
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
} from '@ghostfolio/common/helper';
|
} 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 } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
@ -32,7 +32,58 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
return isGhostfolioScraperApiSymbol(symbol);
|
return isGhostfolioScraperApiSymbol(symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async getAssetProfile(
|
||||||
|
aSymbol: string
|
||||||
|
): Promise<Partial<SymbolProfile>> {
|
||||||
|
return {
|
||||||
|
dataSource: this.getName()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHistorical(
|
||||||
|
aSymbol: string,
|
||||||
|
aGranularity: Granularity = 'day',
|
||||||
|
from: Date,
|
||||||
|
to: Date
|
||||||
|
): Promise<{
|
||||||
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const symbol = aSymbol;
|
||||||
|
|
||||||
|
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
|
||||||
|
[symbol]
|
||||||
|
);
|
||||||
|
const scraperConfiguration = symbolProfile?.scraperConfiguration;
|
||||||
|
|
||||||
|
const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {});
|
||||||
|
|
||||||
|
const html = await get();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const value = this.extractNumberFromString(
|
||||||
|
$(scraperConfiguration?.selector).text()
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
[symbol]: {
|
||||||
|
[format(getYesterday(), DATE_FORMAT)]: {
|
||||||
|
marketPrice: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getName(): DataSource {
|
||||||
|
return DataSource.GHOSTFOLIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getQuotes(
|
||||||
aSymbols: string[]
|
aSymbols: string[]
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
if (aSymbols.length <= 0) {
|
if (aSymbols.length <= 0) {
|
||||||
@ -69,52 +120,6 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
|
||||||
aSymbols: string[],
|
|
||||||
aGranularity: Granularity = 'day',
|
|
||||||
from: Date,
|
|
||||||
to: Date
|
|
||||||
): Promise<{
|
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
|
||||||
}> {
|
|
||||||
if (aSymbols.length <= 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [symbol] = aSymbols;
|
|
||||||
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
|
|
||||||
[symbol]
|
|
||||||
);
|
|
||||||
const scraperConfiguration = symbolProfile?.scraperConfiguration;
|
|
||||||
|
|
||||||
const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {});
|
|
||||||
|
|
||||||
const html = await get();
|
|
||||||
const $ = cheerio.load(html);
|
|
||||||
|
|
||||||
const value = this.extractNumberFromString(
|
|
||||||
$(scraperConfiguration?.selector).text()
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
[symbol]: {
|
|
||||||
[format(getYesterday(), DATE_FORMAT)]: {
|
|
||||||
marketPrice: value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getName(): DataSource {
|
|
||||||
return DataSource.GHOSTFOLIO;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
const items = await this.prismaService.symbolProfile.findMany({
|
const items = await this.prismaService.symbolProfile.findMany({
|
||||||
select: {
|
select: {
|
||||||
|
@ -11,7 +11,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se
|
|||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } 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 } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { GoogleSpreadsheet } from 'google-spreadsheet';
|
import { GoogleSpreadsheet } from 'google-spreadsheet';
|
||||||
|
|
||||||
@ -27,7 +27,62 @@ export class GoogleSheetsService implements DataProviderInterface {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async getAssetProfile(
|
||||||
|
aSymbol: string
|
||||||
|
): Promise<Partial<SymbolProfile>> {
|
||||||
|
return {
|
||||||
|
dataSource: this.getName()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHistorical(
|
||||||
|
aSymbol: string,
|
||||||
|
aGranularity: Granularity = 'day',
|
||||||
|
from: Date,
|
||||||
|
to: Date
|
||||||
|
): Promise<{
|
||||||
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const symbol = aSymbol;
|
||||||
|
|
||||||
|
const sheet = await this.getSheet({
|
||||||
|
symbol,
|
||||||
|
sheetId: this.configurationService.get('GOOGLE_SHEETS_ID')
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = await sheet.getRows();
|
||||||
|
|
||||||
|
const historicalData: {
|
||||||
|
[date: string]: IDataProviderHistoricalResponse;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
rows
|
||||||
|
.filter((row, index) => {
|
||||||
|
return index >= 1;
|
||||||
|
})
|
||||||
|
.forEach((row) => {
|
||||||
|
const date = parseDate(row._rawData[0]);
|
||||||
|
const close = parseFloat(row._rawData[1]);
|
||||||
|
|
||||||
|
historicalData[format(date, DATE_FORMAT)] = { marketPrice: close };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
[symbol]: historicalData
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getName(): DataSource {
|
||||||
|
return DataSource.GOOGLE_SHEETS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getQuotes(
|
||||||
aSymbols: string[]
|
aSymbols: string[]
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
if (aSymbols.length <= 0) {
|
if (aSymbols.length <= 0) {
|
||||||
@ -72,57 +127,6 @@ export class GoogleSheetsService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
|
||||||
aSymbols: string[],
|
|
||||||
aGranularity: Granularity = 'day',
|
|
||||||
from: Date,
|
|
||||||
to: Date
|
|
||||||
): Promise<{
|
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
|
||||||
}> {
|
|
||||||
if (aSymbols.length <= 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [symbol] = aSymbols;
|
|
||||||
|
|
||||||
const sheet = await this.getSheet({
|
|
||||||
symbol,
|
|
||||||
sheetId: this.configurationService.get('GOOGLE_SHEETS_ID')
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = await sheet.getRows();
|
|
||||||
|
|
||||||
const historicalData: {
|
|
||||||
[date: string]: IDataProviderHistoricalResponse;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
rows
|
|
||||||
.filter((row, index) => {
|
|
||||||
return index >= 1;
|
|
||||||
})
|
|
||||||
.forEach((row) => {
|
|
||||||
const date = parseDate(row._rawData[0]);
|
|
||||||
const close = parseFloat(row._rawData[1]);
|
|
||||||
|
|
||||||
historicalData[format(date, DATE_FORMAT)] = { marketPrice: close };
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
[symbol]: historicalData
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getName(): DataSource {
|
|
||||||
return DataSource.GOOGLE_SHEETS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
const items = await this.prismaService.symbolProfile.findMany({
|
const items = await this.prismaService.symbolProfile.findMany({
|
||||||
select: {
|
select: {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { SymbolProfile } from '@prisma/client';
|
||||||
|
|
||||||
export interface DataEnhancerInterface {
|
export interface DataEnhancerInterface {
|
||||||
enhance({
|
enhance({
|
||||||
response,
|
response,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
response: IDataProviderResponse;
|
response: Partial<SymbolProfile>;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}): Promise<IDataProviderResponse>;
|
}): Promise<Partial<SymbolProfile>>;
|
||||||
|
|
||||||
getName(): string;
|
getName(): string;
|
||||||
}
|
}
|
||||||
|
@ -4,23 +4,27 @@ import {
|
|||||||
IDataProviderResponse
|
IDataProviderResponse
|
||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
|
|
||||||
export interface DataProviderInterface {
|
export interface DataProviderInterface {
|
||||||
canHandle(symbol: string): boolean;
|
canHandle(symbol: string): boolean;
|
||||||
|
|
||||||
get(aSymbols: string[]): Promise<{ [symbol: string]: IDataProviderResponse }>;
|
getAssetProfile(aSymbol: string): Promise<Partial<SymbolProfile>>;
|
||||||
|
|
||||||
getHistorical(
|
getHistorical(
|
||||||
aSymbols: string[],
|
aSymbol: string,
|
||||||
aGranularity: Granularity,
|
aGranularity: Granularity,
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date
|
||||||
): Promise<{
|
): Promise<{
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}>;
|
}>; // TODO: Return only one symbol
|
||||||
|
|
||||||
getName(): DataSource;
|
getName(): DataSource;
|
||||||
|
|
||||||
|
getQuotes(
|
||||||
|
aSymbols: string[]
|
||||||
|
): Promise<{ [symbol: string]: IDataProviderResponse }>;
|
||||||
|
|
||||||
search(aQuery: string): Promise<{ items: LookupItem[] }>;
|
search(aQuery: string): Promise<{ items: LookupItem[] }>;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
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 { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ManualService implements DataProviderInterface {
|
export class ManualService implements DataProviderInterface {
|
||||||
@ -16,14 +16,16 @@ export class ManualService implements DataProviderInterface {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async getAssetProfile(
|
||||||
aSymbols: string[]
|
aSymbol: string
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<Partial<SymbolProfile>> {
|
||||||
return {};
|
return {
|
||||||
|
dataSource: this.getName()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbols: string[],
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date
|
||||||
@ -37,6 +39,12 @@ export class ManualService implements DataProviderInterface {
|
|||||||
return DataSource.MANUAL;
|
return DataSource.MANUAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getQuotes(
|
||||||
|
aSymbols: string[]
|
||||||
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
return { items: [] };
|
return { items: [] };
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import {
|
||||||
|
IDataProviderHistoricalResponse,
|
||||||
|
IDataProviderResponse,
|
||||||
|
MarketState
|
||||||
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getToday, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getToday, getYesterday } 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 } from '@prisma/client';
|
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||||
|
|
||||||
import {
|
|
||||||
IDataProviderHistoricalResponse,
|
|
||||||
IDataProviderResponse,
|
|
||||||
MarketState
|
|
||||||
} from '../../interfaces/interfaces';
|
|
||||||
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -29,50 +29,24 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
return !!this.configurationService.get('RAKUTEN_RAPID_API_KEY');
|
return !!this.configurationService.get('RAKUTEN_RAPID_API_KEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async getAssetProfile(
|
||||||
aSymbols: string[]
|
aSymbol: string
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<Partial<SymbolProfile>> {
|
||||||
if (aSymbols.length <= 0) {
|
return {
|
||||||
return {};
|
dataSource: this.getName()
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
|
||||||
const symbol = aSymbols[0];
|
|
||||||
|
|
||||||
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
|
||||||
const fgi = await this.getFearAndGreedIndex();
|
|
||||||
|
|
||||||
return {
|
|
||||||
[ghostfolioFearAndGreedIndexSymbol]: {
|
|
||||||
currency: undefined,
|
|
||||||
dataSource: this.getName(),
|
|
||||||
marketPrice: fgi.now.value,
|
|
||||||
marketState: MarketState.open,
|
|
||||||
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistorical(
|
public async getHistorical(
|
||||||
aSymbols: string[],
|
aSymbol: string,
|
||||||
aGranularity: Granularity = 'day',
|
aGranularity: Granularity = 'day',
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date
|
||||||
): Promise<{
|
): Promise<{
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
}> {
|
}> {
|
||||||
if (aSymbols.length <= 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const symbol = aSymbols[0];
|
const symbol = aSymbol;
|
||||||
|
|
||||||
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||||
const fgi = await this.getFearAndGreedIndex();
|
const fgi = await this.getFearAndGreedIndex();
|
||||||
@ -129,6 +103,35 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
return DataSource.RAKUTEN;
|
return DataSource.RAKUTEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getQuotes(
|
||||||
|
aSymbols: string[]
|
||||||
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
|
if (aSymbols.length <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const symbol = aSymbols[0];
|
||||||
|
|
||||||
|
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||||
|
const fgi = await this.getFearAndGreedIndex();
|
||||||
|
|
||||||
|
return {
|
||||||
|
[ghostfolioFearAndGreedIndexSymbol]: {
|
||||||
|
currency: undefined,
|
||||||
|
dataSource: this.getName(),
|
||||||
|
marketPrice: fgi.now.value,
|
||||||
|
marketState: MarketState.open
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
return { items: [] };
|
return { items: [] };
|
||||||
}
|
}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
export interface IYahooFinanceHistoricalResponse {
|
|
||||||
adjClose: number;
|
|
||||||
close: number;
|
|
||||||
date: Date;
|
|
||||||
high: number;
|
|
||||||
low: number;
|
|
||||||
open: number;
|
|
||||||
symbol: string;
|
|
||||||
volume: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IYahooFinanceQuoteResponse {
|
|
||||||
price: IYahooFinancePrice;
|
|
||||||
summaryProfile: IYahooFinanceSummaryProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IYahooFinancePrice {
|
|
||||||
currency: string;
|
|
||||||
exchangeName: string;
|
|
||||||
longName: string;
|
|
||||||
marketState: string;
|
|
||||||
quoteType: string;
|
|
||||||
regularMarketPrice: number;
|
|
||||||
shortName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IYahooFinanceSummaryProfile {
|
|
||||||
country?: string;
|
|
||||||
industry?: string;
|
|
||||||
sector?: string;
|
|
||||||
website?: string;
|
|
||||||
}
|
|
@ -1,27 +1,27 @@
|
|||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||||
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
|
||||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
|
||||||
import { Granularity } from '@ghostfolio/common/types';
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
|
||||||
import * as bent from 'bent';
|
|
||||||
import Big from 'big.js';
|
|
||||||
import { countries } from 'countries-list';
|
|
||||||
import { addDays, format, isSameDay } from 'date-fns';
|
|
||||||
import * as yahooFinance from 'yahoo-finance';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IDataProviderHistoricalResponse,
|
IDataProviderHistoricalResponse,
|
||||||
IDataProviderResponse,
|
IDataProviderResponse,
|
||||||
MarketState
|
MarketState
|
||||||
} from '../../interfaces/interfaces';
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
|
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||||
|
import { Granularity } from '@ghostfolio/common/types';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
IYahooFinanceHistoricalResponse,
|
AssetClass,
|
||||||
IYahooFinancePrice,
|
AssetSubClass,
|
||||||
IYahooFinanceQuoteResponse
|
DataSource,
|
||||||
} from './interfaces/interfaces';
|
SymbolProfile
|
||||||
|
} from '@prisma/client';
|
||||||
|
import * as bent from 'bent';
|
||||||
|
import Big from 'big.js';
|
||||||
|
import { countries } from 'countries-list';
|
||||||
|
import { addDays, format, isSameDay } from 'date-fns';
|
||||||
|
import yahooFinance2 from 'yahoo-finance2';
|
||||||
|
|
||||||
|
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class YahooFinanceService implements DataProviderInterface {
|
export class YahooFinanceService implements DataProviderInterface {
|
||||||
@ -73,7 +73,123 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return aSymbol;
|
return aSymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(
|
public async getAssetProfile(
|
||||||
|
aSymbol: string
|
||||||
|
): Promise<Partial<SymbolProfile>> {
|
||||||
|
const response: Partial<SymbolProfile> = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||||
|
const assetProfile = await yahooFinance2.quoteSummary(symbol, {
|
||||||
|
modules: ['price', 'summaryProfile']
|
||||||
|
});
|
||||||
|
|
||||||
|
const { assetClass, assetSubClass } = this.parseAssetClass(
|
||||||
|
assetProfile.price
|
||||||
|
);
|
||||||
|
|
||||||
|
response.assetClass = assetClass;
|
||||||
|
response.assetSubClass = assetSubClass;
|
||||||
|
response.currency = assetProfile.price.currency;
|
||||||
|
response.dataSource = this.getName();
|
||||||
|
response.name =
|
||||||
|
assetProfile.price.longName || assetProfile.price.shortName || symbol;
|
||||||
|
response.symbol = aSymbol;
|
||||||
|
|
||||||
|
if (
|
||||||
|
assetSubClass === AssetSubClass.STOCK &&
|
||||||
|
assetProfile.summaryProfile?.country
|
||||||
|
) {
|
||||||
|
// Add country if asset is stock and country available
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [code] = Object.entries(countries).find(([, country]) => {
|
||||||
|
return country.name === assetProfile.summaryProfile?.country;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
response.countries = [{ code, weight: 1 }];
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (assetProfile.summaryProfile?.sector) {
|
||||||
|
response.sectors = [
|
||||||
|
{ name: assetProfile.summaryProfile?.sector, weight: 1 }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = assetProfile.summaryProfile?.website;
|
||||||
|
if (url) {
|
||||||
|
response.url = url;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHistorical(
|
||||||
|
aSymbol: string,
|
||||||
|
aGranularity: Granularity = 'day',
|
||||||
|
from: Date,
|
||||||
|
to: Date
|
||||||
|
): Promise<{
|
||||||
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
|
}> {
|
||||||
|
if (isSameDay(from, to)) {
|
||||||
|
to = addDays(to, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const historicalResult = await yahooFinance2.historical(
|
||||||
|
yahooFinanceSymbol,
|
||||||
|
{
|
||||||
|
interval: '1d',
|
||||||
|
period1: format(from, DATE_FORMAT),
|
||||||
|
period2: format(to, DATE_FORMAT)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: {
|
||||||
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
// Convert symbol back
|
||||||
|
const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
||||||
|
|
||||||
|
response[symbol] = {};
|
||||||
|
|
||||||
|
for (const historicalItem of historicalResult) {
|
||||||
|
let marketPrice = historicalItem.close;
|
||||||
|
|
||||||
|
if (symbol === 'USDGBp') {
|
||||||
|
// Convert GPB to GBp (pence)
|
||||||
|
marketPrice = new Big(marketPrice).mul(100).toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
response[symbol][format(historicalItem.date, DATE_FORMAT)] = {
|
||||||
|
marketPrice,
|
||||||
|
performance: historicalItem.open - historicalItem.close
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
Logger.warn(
|
||||||
|
`Skipping yahooFinance2.getHistorical("${aSymbol}"): [${error.name}] ${error.message}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getName(): DataSource {
|
||||||
|
return DataSource.YAHOO;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getQuotes(
|
||||||
aSymbols: string[]
|
aSymbols: string[]
|
||||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||||
if (aSymbols.length <= 0) {
|
if (aSymbols.length <= 0) {
|
||||||
@ -86,127 +202,33 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
|
|
||||||
const data: {
|
const quotes = await yahooFinance2.quote(yahooFinanceSymbols);
|
||||||
[symbol: string]: IYahooFinanceQuoteResponse;
|
|
||||||
} = await yahooFinance.quote({
|
|
||||||
modules: ['price', 'summaryProfile'],
|
|
||||||
symbols: yahooFinanceSymbols
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [yahooFinanceSymbol, value] of Object.entries(data)) {
|
for (const quote of quotes) {
|
||||||
// Convert symbols back
|
// Convert symbols back
|
||||||
const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
const symbol = this.convertFromYahooFinanceSymbol(quote.symbol);
|
||||||
|
|
||||||
const { assetClass, assetSubClass } = this.parseAssetClass(value.price);
|
|
||||||
|
|
||||||
response[symbol] = {
|
response[symbol] = {
|
||||||
assetClass,
|
currency: quote.currency,
|
||||||
assetSubClass,
|
|
||||||
currency: value.price?.currency,
|
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
exchange: this.parseExchange(value.price?.exchangeName),
|
|
||||||
marketState:
|
marketState:
|
||||||
value.price?.marketState === 'REGULAR' ||
|
quote.marketState === 'REGULAR' ||
|
||||||
this.cryptocurrencyService.isCryptocurrency(symbol)
|
this.cryptocurrencyService.isCryptocurrency(symbol)
|
||||||
? MarketState.open
|
? MarketState.open
|
||||||
: MarketState.closed,
|
: MarketState.closed,
|
||||||
marketPrice: value.price?.regularMarketPrice || 0,
|
marketPrice: quote.regularMarketPrice || 0
|
||||||
name: value.price?.longName || value.price?.shortName || symbol
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (value.price?.currency === 'GBp') {
|
if (symbol === 'USDGBP' && yahooFinanceSymbols.includes('USDGBp=X')) {
|
||||||
// Convert GBp (pence) to GBP
|
// Convert GPB to GBp (pence)
|
||||||
response[symbol].currency = 'GBP';
|
response['USDGBp'] = {
|
||||||
response[symbol].marketPrice = new Big(
|
...response[symbol],
|
||||||
value.price?.regularMarketPrice ?? 0
|
currency: 'GBp',
|
||||||
)
|
marketPrice: new Big(response[symbol].marketPrice)
|
||||||
.div(100)
|
.mul(100)
|
||||||
.toNumber();
|
.toNumber()
|
||||||
}
|
|
||||||
|
|
||||||
// Add country if stock and available
|
|
||||||
if (
|
|
||||||
assetSubClass === AssetSubClass.STOCK &&
|
|
||||||
value.summaryProfile?.country
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const [code] = Object.entries(countries).find(([, country]) => {
|
|
||||||
return country.name === value.summaryProfile?.country;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (code) {
|
|
||||||
response[symbol].countries = [{ code, weight: 1 }];
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (value.summaryProfile?.sector) {
|
|
||||||
response[symbol].sectors = [
|
|
||||||
{ name: value.summaryProfile?.sector, weight: 1 }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add url if available
|
|
||||||
const url = value.summaryProfile?.website;
|
|
||||||
if (url) {
|
|
||||||
response[symbol].url = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(error);
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getHistorical(
|
|
||||||
aSymbols: string[],
|
|
||||||
aGranularity: Granularity = 'day',
|
|
||||||
from: Date,
|
|
||||||
to: Date
|
|
||||||
): Promise<{
|
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
|
||||||
}> {
|
|
||||||
if (aSymbols.length <= 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSameDay(from, to)) {
|
|
||||||
to = addDays(to, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const yahooFinanceSymbols = aSymbols.map((symbol) => {
|
|
||||||
return this.convertToYahooFinanceSymbol(symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const historicalData: {
|
|
||||||
[symbol: string]: IYahooFinanceHistoricalResponse[];
|
|
||||||
} = await yahooFinance.historical({
|
|
||||||
symbols: yahooFinanceSymbols,
|
|
||||||
from: format(from, DATE_FORMAT),
|
|
||||||
to: format(to, DATE_FORMAT)
|
|
||||||
});
|
|
||||||
|
|
||||||
const response: {
|
|
||||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
for (const [yahooFinanceSymbol, timeSeries] of Object.entries(
|
|
||||||
historicalData
|
|
||||||
)) {
|
|
||||||
// Convert symbols back
|
|
||||||
const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol);
|
|
||||||
response[symbol] = {};
|
|
||||||
|
|
||||||
timeSeries.forEach((timeSerie) => {
|
|
||||||
response[symbol][format(timeSerie.date, DATE_FORMAT)] = {
|
|
||||||
marketPrice: timeSerie.close,
|
|
||||||
performance: timeSerie.open - timeSerie.close
|
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -217,10 +239,6 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getName(): DataSource {
|
|
||||||
return DataSource.YAHOO;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||||
const items: LookupItem[] = [];
|
const items: LookupItem[] = [];
|
||||||
|
|
||||||
@ -236,7 +254,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
const searchResult = await get();
|
const searchResult = await get();
|
||||||
|
|
||||||
const symbols: string[] = searchResult.quotes
|
const quotes = searchResult.quotes
|
||||||
.filter((quote) => {
|
.filter((quote) => {
|
||||||
// filter out undefined symbols
|
// filter out undefined symbols
|
||||||
return quote.symbol;
|
return quote.symbol;
|
||||||
@ -247,8 +265,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
this.cryptocurrencyService.isCryptocurrency(
|
this.cryptocurrencyService.isCryptocurrency(
|
||||||
symbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency)
|
symbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency)
|
||||||
)) ||
|
)) ||
|
||||||
quoteType === 'EQUITY' ||
|
['EQUITY', 'ETF', 'MUTUALFUND'].includes(quoteType)
|
||||||
quoteType === 'ETF'
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.filter(({ quoteType, symbol }) => {
|
.filter(({ quoteType, symbol }) => {
|
||||||
@ -259,19 +276,24 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
|
||||||
.map(({ symbol }) => {
|
|
||||||
return symbol;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const marketData = await this.get(symbols);
|
const marketData = await this.getQuotes(
|
||||||
|
quotes.map(({ symbol }) => {
|
||||||
|
return symbol;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (const [symbol, value] of Object.entries(marketData)) {
|
for (const [symbol, value] of Object.entries(marketData)) {
|
||||||
|
const quote = quotes.find((currentQuote: any) => {
|
||||||
|
return currentQuote.symbol === symbol;
|
||||||
|
});
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
symbol,
|
symbol,
|
||||||
currency: value.currency,
|
currency: value.currency,
|
||||||
dataSource: this.getName(),
|
dataSource: this.getName(),
|
||||||
name: value.name
|
name: quote?.longname || quote?.shortname || symbol
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -281,7 +303,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return { items };
|
return { items };
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAssetClass(aPrice: IYahooFinancePrice): {
|
private parseAssetClass(aPrice: any): {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
||||||
} {
|
} {
|
||||||
@ -301,16 +323,12 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
assetClass = AssetClass.EQUITY;
|
assetClass = AssetClass.EQUITY;
|
||||||
assetSubClass = AssetSubClass.ETF;
|
assetSubClass = AssetSubClass.ETF;
|
||||||
break;
|
break;
|
||||||
|
case 'mutualfund':
|
||||||
|
assetClass = AssetClass.EQUITY;
|
||||||
|
assetSubClass = AssetSubClass.MUTUALFUND;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { assetClass, assetSubClass };
|
return { assetClass, assetSubClass };
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseExchange(aString: string): string {
|
|
||||||
if (aString?.toLowerCase() === 'ccc') {
|
|
||||||
return UNKNOWN_KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return aString;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
|
|||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { isEmpty, isNumber, uniq } from 'lodash';
|
import { isNumber, uniq } from 'lodash';
|
||||||
|
|
||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
@ -61,7 +61,7 @@ export class ExchangeRateDataService {
|
|||||||
if (Object.keys(result).length !== this.currencyPairs.length) {
|
if (Object.keys(result).length !== this.currencyPairs.length) {
|
||||||
// Load currencies directly from data provider as a fallback
|
// Load currencies directly from data provider as a fallback
|
||||||
// if historical data is not fully available
|
// if historical data is not fully available
|
||||||
const historicalData = await this.dataProviderService.get(
|
const historicalData = await this.dataProviderService.getQuotes(
|
||||||
this.currencyPairs.map(({ dataSource, symbol }) => {
|
this.currencyPairs.map(({ dataSource, symbol }) => {
|
||||||
return { dataSource, symbol };
|
return { dataSource, symbol };
|
||||||
})
|
})
|
||||||
|
@ -33,19 +33,10 @@ export interface IDataProviderHistoricalResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IDataProviderResponse {
|
export interface IDataProviderResponse {
|
||||||
assetClass?: AssetClass;
|
|
||||||
assetSubClass?: AssetSubClass;
|
|
||||||
countries?: { code: string; weight: number }[];
|
|
||||||
currency: string;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
exchange?: string;
|
|
||||||
marketChange?: number;
|
|
||||||
marketChangePercent?: number;
|
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
marketState: MarketState;
|
marketState: MarketState;
|
||||||
name?: string;
|
|
||||||
sectors?: { name: string; weight: number }[];
|
|
||||||
url?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDataGatheringItem {
|
export interface IDataGatheringItem {
|
||||||
|
@ -39,11 +39,6 @@
|
|||||||
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
|
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<span>{{ position?.symbol | gfSymbol }}</span>
|
<span>{{ position?.symbol | gfSymbol }}</span>
|
||||||
<span
|
|
||||||
*ngIf="position?.exchange && position?.exchange !== unknownKey"
|
|
||||||
class="ml-2 text-muted"
|
|
||||||
>({{ position.exchange }})</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex mt-1">
|
<div class="d-flex mt-1">
|
||||||
<gf-value
|
<gf-value
|
||||||
|
@ -117,7 +117,7 @@
|
|||||||
"tslib": "2.0.0",
|
"tslib": "2.0.0",
|
||||||
"twitter-api-v2": "1.10.3",
|
"twitter-api-v2": "1.10.3",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"yahoo-finance": "0.3.6",
|
"yahoo-finance2": "2.1.9",
|
||||||
"zone.js": "0.11.4"
|
"zone.js": "0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "AssetSubClass" ADD VALUE 'MUTUALFUND';
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;
|
@ -129,6 +129,7 @@ model SymbolProfile {
|
|||||||
sectors Json?
|
sectors Json?
|
||||||
symbol String
|
symbol String
|
||||||
symbolMapping Json?
|
symbolMapping Json?
|
||||||
|
url String?
|
||||||
|
|
||||||
@@unique([dataSource, symbol])
|
@@unique([dataSource, symbol])
|
||||||
}
|
}
|
||||||
@ -178,6 +179,7 @@ enum AssetClass {
|
|||||||
enum AssetSubClass {
|
enum AssetSubClass {
|
||||||
CRYPTOCURRENCY
|
CRYPTOCURRENCY
|
||||||
ETF
|
ETF
|
||||||
|
MUTUALFUND
|
||||||
STOCK
|
STOCK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
112
yarn.lock
112
yarn.lock
@ -5720,6 +5720,16 @@ ajv-keywords@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal "^3.1.3"
|
fast-deep-equal "^3.1.3"
|
||||||
|
|
||||||
|
ajv@8.10.0:
|
||||||
|
version "8.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.10.0.tgz#e573f719bd3af069017e3b66538ab968d040e54d"
|
||||||
|
integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal "^3.1.1"
|
||||||
|
json-schema-traverse "^1.0.0"
|
||||||
|
require-from-string "^2.0.2"
|
||||||
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
ajv@8.6.3:
|
ajv@8.6.3:
|
||||||
version "8.6.3"
|
version "8.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764"
|
||||||
@ -6561,7 +6571,7 @@ blob-util@2.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
|
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
|
||||||
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
|
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
|
||||||
|
|
||||||
bluebird@^3.3.5, bluebird@^3.4.6, bluebird@^3.5.0, bluebird@^3.5.5, bluebird@^3.7.1, bluebird@^3.7.2:
|
bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.1, bluebird@^3.7.2:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
@ -12976,7 +12986,7 @@ lodash.uniq@4.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||||
|
|
||||||
lodash@4.17.21, lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
|
lodash@4.17.21, lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
@ -13507,14 +13517,14 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||||
|
|
||||||
moment-timezone@^0.5.10, moment-timezone@^0.5.x:
|
moment-timezone@^0.5.x:
|
||||||
version "0.5.33"
|
version "0.5.33"
|
||||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c"
|
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c"
|
||||||
integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==
|
integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==
|
||||||
dependencies:
|
dependencies:
|
||||||
moment ">= 2.9.0"
|
moment ">= 2.9.0"
|
||||||
|
|
||||||
"moment@>= 2.9.0", moment@^2.17.1, moment@^2.27.0:
|
"moment@>= 2.9.0", moment@^2.27.0:
|
||||||
version "2.29.1"
|
version "2.29.1"
|
||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||||
@ -13985,11 +13995,6 @@ nx@13.8.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@nrwl/cli" "13.8.1"
|
"@nrwl/cli" "13.8.1"
|
||||||
|
|
||||||
oauth-sign@~0.9.0:
|
|
||||||
version "0.9.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
|
||||||
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
|
||||||
|
|
||||||
oauth@0.9.x:
|
oauth@0.9.x:
|
||||||
version "0.9.15"
|
version "0.9.15"
|
||||||
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
|
||||||
@ -15952,49 +15957,6 @@ request-progress@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
throttleit "^1.0.0"
|
throttleit "^1.0.0"
|
||||||
|
|
||||||
request-promise-core@1.1.4:
|
|
||||||
version "1.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
|
|
||||||
integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
|
|
||||||
dependencies:
|
|
||||||
lodash "^4.17.19"
|
|
||||||
|
|
||||||
request-promise@^4.2.1:
|
|
||||||
version "4.2.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.6.tgz#7e7e5b9578630e6f598e3813c0f8eb342a27f0a2"
|
|
||||||
integrity sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==
|
|
||||||
dependencies:
|
|
||||||
bluebird "^3.5.0"
|
|
||||||
request-promise-core "1.1.4"
|
|
||||||
stealthy-require "^1.1.1"
|
|
||||||
tough-cookie "^2.3.3"
|
|
||||||
|
|
||||||
request@^2.79.0:
|
|
||||||
version "2.88.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
|
||||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
|
||||||
dependencies:
|
|
||||||
aws-sign2 "~0.7.0"
|
|
||||||
aws4 "^1.8.0"
|
|
||||||
caseless "~0.12.0"
|
|
||||||
combined-stream "~1.0.6"
|
|
||||||
extend "~3.0.2"
|
|
||||||
forever-agent "~0.6.1"
|
|
||||||
form-data "~2.3.2"
|
|
||||||
har-validator "~5.1.3"
|
|
||||||
http-signature "~1.2.0"
|
|
||||||
is-typedarray "~1.0.0"
|
|
||||||
isstream "~0.1.2"
|
|
||||||
json-stringify-safe "~5.0.1"
|
|
||||||
mime-types "~2.1.19"
|
|
||||||
oauth-sign "~0.9.0"
|
|
||||||
performance-now "^2.1.0"
|
|
||||||
qs "~6.5.2"
|
|
||||||
safe-buffer "^5.1.2"
|
|
||||||
tough-cookie "~2.5.0"
|
|
||||||
tunnel-agent "^0.6.0"
|
|
||||||
uuid "^3.3.2"
|
|
||||||
|
|
||||||
require-directory@^2.1.1:
|
require-directory@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||||
@ -16884,11 +16846,6 @@ static-extend@^0.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||||
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
||||||
|
|
||||||
stealthy-require@^1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
|
|
||||||
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
|
|
||||||
|
|
||||||
store2@^2.12.0:
|
store2@^2.12.0:
|
||||||
version "2.12.0"
|
version "2.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf"
|
resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf"
|
||||||
@ -17031,11 +16988,6 @@ string.prototype.trimstart@^1.0.4:
|
|||||||
call-bind "^1.0.2"
|
call-bind "^1.0.2"
|
||||||
define-properties "^1.1.3"
|
define-properties "^1.1.3"
|
||||||
|
|
||||||
string@^3.3.3:
|
|
||||||
version "3.3.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/string/-/string-3.3.3.tgz#5ea211cd92d228e184294990a6cc97b366a77cb0"
|
|
||||||
integrity sha1-XqIRzZLSKOGEKUmQpsyXs2anfLA=
|
|
||||||
|
|
||||||
string_decoder@^1.0.0, string_decoder@^1.1.1:
|
string_decoder@^1.0.0, string_decoder@^1.1.1:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||||
@ -17526,14 +17478,6 @@ toidentifier@1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||||
|
|
||||||
tough-cookie@^2.3.2, tough-cookie@^2.3.3, tough-cookie@~2.5.0:
|
|
||||||
version "2.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
|
||||||
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
|
|
||||||
dependencies:
|
|
||||||
psl "^1.1.28"
|
|
||||||
punycode "^2.1.1"
|
|
||||||
|
|
||||||
tough-cookie@^4.0.0:
|
tough-cookie@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
|
||||||
@ -17543,6 +17487,14 @@ tough-cookie@^4.0.0:
|
|||||||
punycode "^2.1.1"
|
punycode "^2.1.1"
|
||||||
universalify "^0.1.2"
|
universalify "^0.1.2"
|
||||||
|
|
||||||
|
tough-cookie@~2.5.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||||
|
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
|
||||||
|
dependencies:
|
||||||
|
psl "^1.1.28"
|
||||||
|
punycode "^2.1.1"
|
||||||
|
|
||||||
tr46@^2.1.0:
|
tr46@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"
|
resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240"
|
||||||
@ -18764,20 +18716,14 @@ y18n@^5.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||||
|
|
||||||
yahoo-finance@0.3.6:
|
yahoo-finance2@2.1.9:
|
||||||
version "0.3.6"
|
version "2.1.9"
|
||||||
resolved "https://registry.yarnpkg.com/yahoo-finance/-/yahoo-finance-0.3.6.tgz#c99fe8ff6c9a80babbb7e75881a244a862f6739f"
|
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.1.9.tgz#28b157e1cddc5b56e6b354f6b00b453a41bbe8a4"
|
||||||
integrity sha512-SyXGhtvJvoU8E7XQJzviCBeuJNAMZoERJLfWwAERfDDgoPCu3/zBDDDt7l8hp3HmtIygLpqGuRJ7jzkip2AcZA==
|
integrity sha512-xLlDqcbK+4Y4oSV7Vq1KcvNcjMuODHQrk2uLyBR4SlXDNjRV7XFpTrwMrDnSLu4pErenj0gXG3ARiCWidFjqzg==
|
||||||
dependencies:
|
dependencies:
|
||||||
bluebird "^3.4.6"
|
ajv "8.10.0"
|
||||||
debug "^2.3.3"
|
ajv-formats "2.1.1"
|
||||||
lodash "^4.17.2"
|
node-fetch "^2.6.1"
|
||||||
moment "^2.17.1"
|
|
||||||
moment-timezone "^0.5.10"
|
|
||||||
request "^2.79.0"
|
|
||||||
request-promise "^4.2.1"
|
|
||||||
string "^3.3.3"
|
|
||||||
tough-cookie "^2.3.2"
|
|
||||||
|
|
||||||
yallist@^3.0.2:
|
yallist@^3.0.2:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user