Browse Source

Implement logic for asset profile gathering

pull/4524/head
Thomas Kaul 4 months ago
parent
commit
529ad22ab9
  1. 16
      apps/api/src/app/admin/admin.controller.ts
  2. 8
      apps/api/src/app/order/order.service.ts
  3. 8
      apps/api/src/services/cron.service.ts
  4. 7
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  5. 6
      apps/api/src/services/data-provider/data-provider.service.ts
  6. 1
      apps/api/src/services/data-provider/errors/asset-profile-delisted.error.ts
  7. 4
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  8. 44
      apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
  9. 8
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  10. 4
      libs/common/src/lib/config.ts

16
apps/api/src/app/admin/admin.controller.ts

@ -9,8 +9,8 @@ import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathe
import { import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_HIGH,
DATA_GATHERING_QUEUE_PRIORITY_MEDIUM, DATA_GATHERING_QUEUE_PRIORITY_MEDIUM,
GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { import {
@ -92,9 +92,9 @@ export class AdminController {
dataSource, dataSource,
symbol symbol
}, },
name: GATHER_ASSET_PROFILE_PROCESS, name: GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
opts: { opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, ...GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
jobId: getAssetProfileIdentifier({ dataSource, symbol }), jobId: getAssetProfileIdentifier({ dataSource, symbol }),
priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
} }
@ -119,9 +119,9 @@ export class AdminController {
dataSource, dataSource,
symbol symbol
}, },
name: GATHER_ASSET_PROFILE_PROCESS, name: GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
opts: { opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, ...GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
jobId: getAssetProfileIdentifier({ dataSource, symbol }), jobId: getAssetProfileIdentifier({ dataSource, symbol }),
priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM priority: DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
} }
@ -142,9 +142,9 @@ export class AdminController {
dataSource, dataSource,
symbol symbol
}, },
name: GATHER_ASSET_PROFILE_PROCESS, name: GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
opts: { opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, ...GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
jobId: getAssetProfileIdentifier({ dataSource, symbol }), jobId: getAssetProfileIdentifier({ dataSource, symbol }),
priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH
} }

8
apps/api/src/app/order/order.service.ts

@ -7,8 +7,8 @@ import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathe
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH, DATA_GATHERING_QUEUE_PRIORITY_HIGH,
GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { import {
@ -144,9 +144,9 @@ export class OrderService {
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol symbol: data.SymbolProfile.connectOrCreate.create.symbol
}, },
name: GATHER_ASSET_PROFILE_PROCESS, name: GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
opts: { opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, ...GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
jobId: getAssetProfileIdentifier({ jobId: getAssetProfileIdentifier({
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol symbol: data.SymbolProfile.connectOrCreate.create.symbol

8
apps/api/src/services/cron.service.ts

@ -1,8 +1,8 @@
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { import {
DATA_GATHERING_QUEUE_PRIORITY_LOW, DATA_GATHERING_QUEUE_PRIORITY_LOW,
GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
GATHER_ASSET_PROFILE_PROCESS_OPTIONS, GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
PROPERTY_IS_DATA_GATHERING_ENABLED PROPERTY_IS_DATA_GATHERING_ENABLED
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
@ -66,9 +66,9 @@ export class CronService {
dataSource, dataSource,
symbol symbol
}, },
name: GATHER_ASSET_PROFILE_PROCESS, name: GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
opts: { opts: {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS, ...GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
jobId: getAssetProfileIdentifier({ dataSource, symbol }), jobId: getAssetProfileIdentifier({ dataSource, symbol }),
priority: DATA_GATHERING_QUEUE_PRIORITY_LOW priority: DATA_GATHERING_QUEUE_PRIORITY_LOW
} }

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

@ -1,4 +1,5 @@
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error';
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 { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
@ -236,8 +237,14 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
response.url = url; response.url = url;
} }
} catch (error) { } catch (error) {
if (error.message === `Quote not found for symbol: ${aSymbol}`) {
throw new AssetProfileDelistedError(
`No data found, ${aSymbol} (${this.getName()}) may be delisted`
);
} else {
Logger.error(error, 'YahooFinanceService'); Logger.error(error, 'YahooFinanceService');
} }
}
return response; return response;
} }

6
apps/api/src/services/data-provider/data-provider.service.ts

@ -114,7 +114,13 @@ export class DataProviderService {
} }
} }
try {
await Promise.all(promises); await Promise.all(promises);
} catch (error) {
Logger.error(error, 'DataProviderService');
throw error;
}
return response; return response;
} }

1
apps/api/src/services/data-provider/errors/asset-profile-delisted.error.ts

@ -1,6 +1,7 @@
export class AssetProfileDelistedError extends Error { export class AssetProfileDelistedError extends Error {
public constructor(message: string) { public constructor(message: string) {
super(message); super(message);
this.name = 'AssetProfileDelistedError'; this.name = 'AssetProfileDelistedError';
} }
} }

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

@ -145,7 +145,9 @@ export class YahooFinanceService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
if (error.message === 'No data found, symbol may be delisted') { if (error.message === 'No data found, symbol may be delisted') {
throw new AssetProfileDelistedError(error.message); throw new AssetProfileDelistedError(
`No data found, ${symbol} (${this.getName()}) may be delisted`
);
} else { } else {
throw new Error( throw new Error(
`Could not get historical market data for ${symbol} (${this.getName()}) from ${format( `Could not get historical market data for ${symbol} (${this.getName()}) from ${format(

44
apps/api/src/services/queues/data-gathering/data-gathering.processor.ts

@ -7,7 +7,7 @@ import {
DATA_GATHERING_QUEUE, DATA_GATHERING_QUEUE,
DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY, DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY,
DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY, DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY,
GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper';
@ -45,28 +45,49 @@ export class DataGatheringProcessor {
DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY.toString(), DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY.toString(),
10 10
), ),
name: GATHER_ASSET_PROFILE_PROCESS name: GATHER_ASSET_PROFILE_PROCESS_JOB_NAME
}) })
public async gatherAssetProfile(job: Job<AssetProfileIdentifier>) { public async gatherAssetProfile(job: Job<AssetProfileIdentifier>) {
const { dataSource, symbol } = job.data;
try { try {
Logger.log( Logger.log(
`Asset profile data gathering has been started for ${job.data.symbol} (${job.data.dataSource})`, `Asset profile data gathering has been started for ${symbol} (${dataSource})`,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS})` `DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
await this.dataGatheringService.gatherAssetProfiles([job.data]); await this.dataGatheringService.gatherAssetProfiles([job.data]);
Logger.log( Logger.log(
`Asset profile data gathering has been completed for ${job.data.symbol} (${job.data.dataSource})`, `Asset profile data gathering has been completed for ${symbol} (${dataSource})`,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS})` `DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
} catch (error) { } catch (error) {
if (error instanceof AssetProfileDelistedError) {
await this.symbolProfileService.updateSymbolProfile(
{
dataSource,
symbol
},
{
isActive: false
}
);
Logger.log(
`Asset profile data gathering has been discarded for ${symbol} (${dataSource})`,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
);
return job.discard();
}
Logger.error( Logger.error(
error, error,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS})` `DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
throw new Error(error); throw error;
} }
} }
@ -157,7 +178,12 @@ export class DataGatheringProcessor {
} }
); );
await job.discard(); Logger.log(
`Historical market data gathering has been discarded for ${symbol} (${dataSource})`,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
);
return job.discard();
} }
Logger.error( Logger.error(

8
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -19,7 +19,9 @@
<button <button
mat-menu-item mat-menu-item
type="button" type="button"
[disabled]="assetProfileForm.dirty" [disabled]="
assetProfileForm.dirty || !assetProfileForm.controls.isActive.value
"
(click)=" (click)="
onGatherSymbol({ dataSource: data.dataSource, symbol: data.symbol }) onGatherSymbol({ dataSource: data.dataSource, symbol: data.symbol })
" "
@ -29,7 +31,9 @@
<button <button
mat-menu-item mat-menu-item
type="button" type="button"
[disabled]="assetProfileForm.dirty" [disabled]="
assetProfileForm.dirty || !assetProfileForm.controls.isActive.value
"
(click)=" (click)="
onGatherProfileDataBySymbol({ onGatherProfileDataBySymbol({
dataSource: data.dataSource, dataSource: data.dataSource,

4
libs/common/src/lib/config.ts

@ -78,8 +78,8 @@ export const DERIVED_CURRENCIES = [
export const EMERGENCY_FUND_TAG_ID = '4452656d-9fa4-4bd0-ba38-70492e31d180'; export const EMERGENCY_FUND_TAG_ID = '4452656d-9fa4-4bd0-ba38-70492e31d180';
export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE'; export const GATHER_ASSET_PROFILE_PROCESS_JOB_NAME = 'GATHER_ASSET_PROFILE';
export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = { export const GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS: JobOptions = {
attempts: 12, attempts: 12,
backoff: { backoff: {
delay: ms('1 minute'), delay: ms('1 minute'),

Loading…
Cancel
Save