Feature/provide data provider info in search (#2958)

* Provide data provider info in search

* Update changelog
This commit is contained in:
Thomas Kaul 2024-02-05 19:55:39 +01:00 committed by GitHub
parent 06ba7a4b1b
commit 893e76f83f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 171 additions and 90 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Extended the assistant by an asset class selector (experimental) - Extended the assistant by an asset class selector (experimental)
- Added the data provider information to the search endpoint
### Changed ### Changed

View File

@ -64,16 +64,13 @@ export class ImportController {
maxActivitiesToImport = Number.MAX_SAFE_INTEGER; maxActivitiesToImport = Number.MAX_SAFE_INTEGER;
} }
const userCurrency = this.request.user.Settings.settings.baseCurrency;
try { try {
const activities = await this.importService.import({ const activities = await this.importService.import({
isDryRun, isDryRun,
maxActivitiesToImport, maxActivitiesToImport,
userCurrency,
accountsDto: importData.accounts ?? [], accountsDto: importData.accounts ?? [],
activitiesDto: importData.activities, activitiesDto: importData.activities,
userId: this.request.user.id user: this.request.user
}); });
return { activities }; return { activities };

View File

@ -21,7 +21,8 @@ import {
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { import {
AccountWithPlatform, AccountWithPlatform,
OrderWithAccount OrderWithAccount,
UserWithSettings
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
@ -138,17 +139,16 @@ export class ImportService {
activitiesDto, activitiesDto,
isDryRun = false, isDryRun = false,
maxActivitiesToImport, maxActivitiesToImport,
userCurrency, user
userId
}: { }: {
accountsDto: Partial<CreateAccountDto>[]; accountsDto: Partial<CreateAccountDto>[];
activitiesDto: Partial<CreateOrderDto>[]; activitiesDto: Partial<CreateOrderDto>[];
isDryRun?: boolean; isDryRun?: boolean;
maxActivitiesToImport: number; maxActivitiesToImport: number;
userCurrency: string; user: UserWithSettings;
userId: string;
}): Promise<Activity[]> { }): Promise<Activity[]> {
const accountIdMapping: { [oldAccountId: string]: string } = {}; const accountIdMapping: { [oldAccountId: string]: string } = {};
const userCurrency = user.Settings.settings.baseCurrency;
if (!isDryRun && accountsDto?.length) { if (!isDryRun && accountsDto?.length) {
const [existingAccounts, existingPlatforms] = await Promise.all([ const [existingAccounts, existingPlatforms] = await Promise.all([
@ -171,7 +171,7 @@ export class ImportService {
); );
// If there is no account or if the account belongs to a different user then create a new account // If there is no account or if the account belongs to a different user then create a new account
if (!accountWithSameId || accountWithSameId.userId !== userId) { if (!accountWithSameId || accountWithSameId.userId !== user.id) {
let oldAccountId: string; let oldAccountId: string;
const platformId = account.platformId; const platformId = account.platformId;
@ -184,7 +184,7 @@ export class ImportService {
let accountObject: Prisma.AccountCreateInput = { let accountObject: Prisma.AccountCreateInput = {
...account, ...account,
User: { connect: { id: userId } } User: { connect: { id: user.id } }
}; };
if ( if (
@ -200,7 +200,7 @@ export class ImportService {
const newAccount = await this.accountService.createAccount( const newAccount = await this.accountService.createAccount(
accountObject, accountObject,
userId user.id
); );
// Store the new to old account ID mappings for updating activities // Store the new to old account ID mappings for updating activities
@ -231,16 +231,17 @@ export class ImportService {
const assetProfiles = await this.validateActivities({ const assetProfiles = await this.validateActivities({
activitiesDto, activitiesDto,
maxActivitiesToImport maxActivitiesToImport,
user
}); });
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({ const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
activitiesDto, activitiesDto,
userCurrency, userCurrency,
userId userId: user.id
}); });
const accounts = (await this.accountService.getAccounts(userId)).map( const accounts = (await this.accountService.getAccounts(user.id)).map(
({ id, name }) => { ({ id, name }) => {
return { id, name }; return { id, name };
} }
@ -345,7 +346,6 @@ export class ImportService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
userId,
accountId: validatedAccount?.id, accountId: validatedAccount?.id,
accountUserId: undefined, accountUserId: undefined,
createdAt: new Date(), createdAt: new Date(),
@ -374,7 +374,8 @@ export class ImportService {
}, },
Account: validatedAccount, Account: validatedAccount,
symbolProfileId: undefined, symbolProfileId: undefined,
updatedAt: new Date() updatedAt: new Date(),
userId: user.id
}; };
} else { } else {
if (error) { if (error) {
@ -388,7 +389,6 @@ export class ImportService {
quantity, quantity,
type, type,
unitPrice, unitPrice,
userId,
accountId: validatedAccount?.id, accountId: validatedAccount?.id,
SymbolProfile: { SymbolProfile: {
connectOrCreate: { connectOrCreate: {
@ -406,7 +406,8 @@ export class ImportService {
} }
}, },
updateAccountBalance: false, updateAccountBalance: false,
User: { connect: { id: userId } } User: { connect: { id: user.id } },
userId: user.id
}); });
} }
@ -553,10 +554,12 @@ export class ImportService {
private async validateActivities({ private async validateActivities({
activitiesDto, activitiesDto,
maxActivitiesToImport maxActivitiesToImport,
user
}: { }: {
activitiesDto: Partial<CreateOrderDto>[]; activitiesDto: Partial<CreateOrderDto>[];
maxActivitiesToImport: number; maxActivitiesToImport: number;
user: UserWithSettings;
}) { }) {
if (activitiesDto?.length > maxActivitiesToImport) { if (activitiesDto?.length > maxActivitiesToImport) {
throw new Error(`Too many activities (${maxActivitiesToImport} at most)`); throw new Error(`Too many activities (${maxActivitiesToImport} at most)`);
@ -583,6 +586,21 @@ export class ImportService {
); );
} }
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic'
) {
const dataProvider = this.dataProviderService.getDataProvider(
DataSource[dataSource]
);
if (dataProvider.getDataProviderInfo().isPremium) {
throw new Error(
`activities.${index}.dataSource ("${dataSource}") is not valid`
);
}
}
const assetProfile = { const assetProfile = {
currency, currency,
...( ...(

View File

@ -1,9 +1,11 @@
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
export interface LookupItem { export interface LookupItem {
assetClass: AssetClass; assetClass: AssetClass;
assetSubClass: AssetSubClass; assetSubClass: AssetSubClass;
currency: string; currency: string;
dataProviderInfo: DataProviderInfo;
dataSource: DataSource; dataSource: DataSource;
name: string; name: string;
symbol: string; symbol: string;

View File

@ -12,6 +12,7 @@ import {
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import * as Alphavantage from 'alphavantage'; import * as Alphavantage from 'alphavantage';
@ -44,6 +45,12 @@ export class AlphaVantageService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -118,6 +125,7 @@ export class AlphaVantageService implements DataProviderInterface {
assetClass: undefined, assetClass: undefined,
assetSubClass: undefined, assetSubClass: undefined,
currency: bestMatch['8. currency'], currency: bestMatch['8. currency'],
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(), dataSource: this.getName(),
name: bestMatch['2. name'], name: bestMatch['2. name'],
symbol: bestMatch['1. symbol'] symbol: bestMatch['1. symbol']

View File

@ -91,6 +91,14 @@ export class CoinGeckoService implements DataProviderInterface {
return response; return response;
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false,
name: 'CoinGecko',
url: 'https://coingecko.com'
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -252,11 +260,4 @@ export class CoinGeckoService implements DataProviderInterface {
return { items }; return { items };
} }
private getDataProviderInfo(): DataProviderInfo {
return {
name: 'CoinGecko',
url: 'https://coingecko.com'
};
}
} }

View File

@ -107,6 +107,31 @@ export class DataProviderService {
return response; return response;
} }
public getDataProvider(providerName: DataSource) {
for (const dataProviderInterface of this.dataProviderInterfaces) {
if (this.dataProviderMapping[dataProviderInterface.getName()]) {
const mappedDataProviderInterface = this.dataProviderInterfaces.find(
(currentDataProviderInterface) => {
return (
currentDataProviderInterface.getName() ===
this.dataProviderMapping[dataProviderInterface.getName()]
);
}
);
if (mappedDataProviderInterface) {
return mappedDataProviderInterface;
}
}
if (dataProviderInterface.getName() === providerName) {
return dataProviderInterface;
}
}
throw new Error('No data provider has been found.');
}
public getDataSourceForExchangeRates(): DataSource { public getDataSourceForExchangeRates(): DataSource {
return DataSource[ return DataSource[
this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES') this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
@ -520,20 +545,15 @@ export class DataProviderService {
return { items: lookupItems }; return { items: lookupItems };
} }
let dataSources = this.configurationService.get('DATA_SOURCES'); let dataProviderServices = this.configurationService
.get('DATA_SOURCES')
if ( .map((dataSource) => {
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && return this.getDataProvider(DataSource[dataSource]);
user.subscription.type === 'Basic'
) {
dataSources = dataSources.filter((dataSource) => {
return !this.isPremiumDataSource(DataSource[dataSource]);
}); });
}
for (const dataSource of dataSources) { for (const dataProviderService of dataProviderServices) {
promises.push( promises.push(
this.getDataProvider(DataSource[dataSource]).search({ dataProviderService.search({
includeIndices, includeIndices,
query query
}) })
@ -555,6 +575,16 @@ export class DataProviderService {
}) })
.sort(({ name: name1 }, { name: name2 }) => { .sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase()); return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
})
.map((lookupItem) => {
if (
!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') ||
user.subscription.type === 'Premium'
) {
lookupItem.dataProviderInfo.isPremium = false;
}
return lookupItem;
}); });
return { return {
@ -562,31 +592,6 @@ export class DataProviderService {
}; };
} }
private getDataProvider(providerName: DataSource) {
for (const dataProviderInterface of this.dataProviderInterfaces) {
if (this.dataProviderMapping[dataProviderInterface.getName()]) {
const mappedDataProviderInterface = this.dataProviderInterfaces.find(
(currentDataProviderInterface) => {
return (
currentDataProviderInterface.getName() ===
this.dataProviderMapping[dataProviderInterface.getName()]
);
}
);
if (mappedDataProviderInterface) {
return mappedDataProviderInterface;
}
}
if (dataProviderInterface.getName() === providerName) {
return dataProviderInterface;
}
}
throw new Error('No data provider has been found.');
}
private hasCurrency({ private hasCurrency({
currency, currency,
dataGatheringItems dataGatheringItems
@ -602,14 +607,6 @@ export class DataProviderService {
}); });
} }
private isPremiumDataSource(aDataSource: DataSource) {
const premiumDataSources: DataSource[] = [
DataSource.EOD_HISTORICAL_DATA,
DataSource.FINANCIAL_MODELING_PREP
];
return premiumDataSources.includes(aDataSource);
}
private transformHistoricalData({ private transformHistoricalData({
allData, allData,
currency, currency,

View File

@ -16,6 +16,7 @@ import {
REPLACE_NAME_PARTS REPLACE_NAME_PARTS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
AssetClass, AssetClass,
@ -58,6 +59,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: true
};
}
public async getDividends({ public async getDividends({
from, from,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
@ -312,7 +319,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
dataSource, dataSource,
name, name,
symbol, symbol,
currency: this.convertCurrency(currency) currency: this.convertCurrency(currency),
dataProviderInfo: this.getDataProviderInfo()
}; };
} }
) )

View File

@ -45,6 +45,14 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: true,
name: 'Financial Modeling Prep',
url: 'https://financialmodelingprep.com/developer/docs'
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -202,11 +210,4 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return { items }; return { items };
} }
private getDataProviderInfo(): DataProviderInfo {
return {
name: 'Financial Modeling Prep',
url: 'https://financialmodelingprep.com/developer/docs'
};
}
} }

View File

@ -14,6 +14,7 @@ import {
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -40,6 +41,12 @@ export class GoogleSheetsService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -177,7 +184,11 @@ export class GoogleSheetsService implements DataProviderInterface {
} }
}); });
return { items }; return {
items: items.map((item) => {
return { ...item, dataProviderInfo: this.getDataProviderInfo() };
})
};
} }
private async getSheet({ private async getSheet({

View File

@ -3,6 +3,7 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
@ -11,6 +12,8 @@ export interface DataProviderInterface {
getAssetProfile(aSymbol: string): Promise<Partial<SymbolProfile>>; getAssetProfile(aSymbol: string): Promise<Partial<SymbolProfile>>;
getDataProviderInfo(): DataProviderInfo;
getDividends({ from, granularity, symbol, to }: GetDividendsParams): Promise<{ getDividends({ from, granularity, symbol, to }: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: IDataProviderHistoricalResponse;
}>; }>;

View File

@ -18,7 +18,10 @@ import {
extractNumberFromString, extractNumberFromString,
getYesterday getYesterday
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { ScraperConfiguration } from '@ghostfolio/common/interfaces'; import {
DataProviderInfo,
ScraperConfiguration
} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
@ -59,6 +62,12 @@ export class ManualService implements DataProviderInterface {
return assetProfile; return assetProfile;
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }
@ -214,7 +223,11 @@ export class ManualService implements DataProviderInterface {
return !isUUID(symbol); return !isUUID(symbol);
}); });
return { items }; return {
items: items.map((item) => {
return { ...item, dataProviderInfo: this.getDataProviderInfo() };
})
};
} }
public async test(scraperConfiguration: ScraperConfiguration) { public async test(scraperConfiguration: ScraperConfiguration) {

View File

@ -13,6 +13,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -37,6 +38,12 @@ export class RapidApiService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {
return {}; return {};
} }

View File

@ -14,6 +14,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { addDays, format, isSameDay } from 'date-fns'; import { addDays, format, isSameDay } from 'date-fns';
@ -47,6 +48,12 @@ export class YahooFinanceService implements DataProviderInterface {
}; };
} }
public getDataProviderInfo(): DataProviderInfo {
return {
isPremium: false
};
}
public async getDividends({ public async getDividends({
from, from,
granularity = 'day', granularity = 'day',
@ -283,6 +290,7 @@ export class YahooFinanceService implements DataProviderInterface {
assetSubClass, assetSubClass,
symbol, symbol,
currency: marketDataItem.currency, currency: marketDataItem.currency,
dataProviderInfo: this.getDataProviderInfo(),
dataSource: this.getName(), dataSource: this.getName(),
name: this.yahooFinanceDataEnhancerService.formatName({ name: this.yahooFinanceDataEnhancerService.formatName({
longName: quote.longname, longName: quote.longname,

View File

@ -1,4 +1,5 @@
export interface DataProviderInfo { export interface DataProviderInfo {
name: string; isPremium: boolean;
url: string; name?: string;
url?: string;
} }

View File

@ -129,8 +129,8 @@
<th *matHeaderCellDef class="px-1" mat-header-cell> <th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Name</ng-container> <ng-container i18n>Name</ng-container>
</th> </th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center"> <div class="align-items-center d-flex line-height-1">
<div> <div>
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span> <span class="text-truncate">{{ element.SymbolProfile?.name }}</span>
<span <span

View File

@ -15,12 +15,15 @@
<mat-option <mat-option
*ngFor="let lookupItem of filteredLookupItems" *ngFor="let lookupItem of filteredLookupItems"
class="line-height-1" class="line-height-1"
[disabled]="lookupItem.dataProviderInfo.isPremium"
[value]="lookupItem" [value]="lookupItem"
> >
<span <span class="align-items-center d-flex line-height-1"
><b>{{ lookupItem.name }}</b></span ><b>{{ lookupItem.name }}</b>
> @if (lookupItem.dataProviderInfo.isPremium) {
<br /> <gf-premium-indicator class="ml-1" [enableLink]="false" />
}
</span>
<small class="text-muted" <small class="text-muted"
>{{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency >{{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency
}}<ng-container *ngIf="lookupItem.assetSubClass"> }}<ng-container *ngIf="lookupItem.assetSubClass">

View File

@ -7,6 +7,7 @@ import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { SymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.component'; import { SymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.component';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
@NgModule({ @NgModule({
declarations: [SymbolAutocompleteComponent], declarations: [SymbolAutocompleteComponent],
@ -14,6 +15,7 @@ import { SymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete/
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
GfPremiumIndicatorModule,
GfSymbolModule, GfSymbolModule,
MatAutocompleteModule, MatAutocompleteModule,
MatFormFieldModule, MatFormFieldModule,