mirror of https://github.com/ghostfolio/ghostfolio
14 changed files with 308 additions and 0 deletions
@ -0,0 +1,43 @@ |
|||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; |
|||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; |
|||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; |
|||
import { permissions } from '@ghostfolio/common/permissions'; |
|||
|
|||
import { |
|||
Controller, |
|||
Get, |
|||
HttpException, |
|||
Query, |
|||
UseGuards |
|||
} from '@nestjs/common'; |
|||
import { AuthGuard } from '@nestjs/passport'; |
|||
import { getReasonPhrase, StatusCodes } from 'http-status-codes'; |
|||
|
|||
import { GhostfolioService } from './ghostfolio.service'; |
|||
|
|||
@Controller('data-providers/ghostfolio') |
|||
export class GhostfolioController { |
|||
public constructor(private readonly ghostfolioService: GhostfolioService) {} |
|||
|
|||
@Get('lookup') |
|||
@HasPermission(permissions.enableDataProviderGhostfolio) |
|||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) |
|||
public async lookupSymbol( |
|||
@Query('includeIndices') includeIndicesParam = 'false', |
|||
@Query('query') query = '' |
|||
): Promise<{ items: LookupItem[] }> { |
|||
const includeIndices = includeIndicesParam === 'true'; |
|||
|
|||
try { |
|||
return this.ghostfolioService.lookup({ |
|||
includeIndices, |
|||
query: query.toLowerCase() |
|||
}); |
|||
} catch { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), |
|||
StatusCodes.INTERNAL_SERVER_ERROR |
|||
); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,83 @@ |
|||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; |
|||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; |
|||
import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service'; |
|||
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service'; |
|||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; |
|||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; |
|||
import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider/eod-historical-data/eod-historical-data.service'; |
|||
import { FinancialModelingPrepService } from '@ghostfolio/api/services/data-provider/financial-modeling-prep/financial-modeling-prep.service'; |
|||
import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service'; |
|||
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; |
|||
import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service'; |
|||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; |
|||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; |
|||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
|||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; |
|||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { GhostfolioController } from './ghostfolio.controller'; |
|||
import { GhostfolioService } from './ghostfolio.service'; |
|||
|
|||
@Module({ |
|||
controllers: [GhostfolioController], |
|||
imports: [ |
|||
CryptocurrencyModule, |
|||
DataProviderModule, |
|||
MarketDataModule, |
|||
PrismaModule, |
|||
PropertyModule, |
|||
RedisCacheModule, |
|||
SymbolProfileModule |
|||
], |
|||
providers: [ |
|||
AlphaVantageService, |
|||
CoinGeckoService, |
|||
ConfigurationService, |
|||
DataProviderService, |
|||
EodHistoricalDataService, |
|||
FinancialModelingPrepService, |
|||
GhostfolioService, |
|||
GoogleSheetsService, |
|||
ManualService, |
|||
RapidApiService, |
|||
YahooFinanceService, |
|||
YahooFinanceDataEnhancerService, |
|||
{ |
|||
inject: [ |
|||
AlphaVantageService, |
|||
CoinGeckoService, |
|||
EodHistoricalDataService, |
|||
FinancialModelingPrepService, |
|||
GoogleSheetsService, |
|||
ManualService, |
|||
RapidApiService, |
|||
YahooFinanceService |
|||
], |
|||
provide: 'DataProviderInterfaces', |
|||
useFactory: ( |
|||
alphaVantageService, |
|||
coinGeckoService, |
|||
eodHistoricalDataService, |
|||
financialModelingPrepService, |
|||
googleSheetsService, |
|||
manualService, |
|||
rapidApiService, |
|||
yahooFinanceService |
|||
) => [ |
|||
alphaVantageService, |
|||
coinGeckoService, |
|||
eodHistoricalDataService, |
|||
financialModelingPrepService, |
|||
googleSheetsService, |
|||
manualService, |
|||
rapidApiService, |
|||
yahooFinanceService |
|||
] |
|||
} |
|||
] |
|||
}) |
|||
export class GhostfolioModule {} |
@ -0,0 +1,93 @@ |
|||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; |
|||
import { DataProviderInfo } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Injectable, Logger } from '@nestjs/common'; |
|||
import { DataSource } from '@prisma/client'; |
|||
|
|||
@Injectable() |
|||
export class GhostfolioService { |
|||
public constructor( |
|||
private readonly configurationService: ConfigurationService, |
|||
private readonly dataProviderService: DataProviderService |
|||
) {} |
|||
|
|||
public async lookup({ |
|||
includeIndices = false, |
|||
query |
|||
}: { |
|||
includeIndices?: boolean; |
|||
query: string; |
|||
}): Promise<{ items: LookupItem[] }> { |
|||
const results: { items: LookupItem[] } = { items: [] }; |
|||
|
|||
if (!query) { |
|||
return results; |
|||
} |
|||
|
|||
try { |
|||
let lookupItems: LookupItem[] = []; |
|||
const promises: Promise<{ items: LookupItem[] }>[] = []; |
|||
|
|||
if (query?.length < 2) { |
|||
return { items: lookupItems }; |
|||
} |
|||
|
|||
for (const dataProviderService of this.getDataProviderServices()) { |
|||
promises.push( |
|||
dataProviderService.search({ |
|||
includeIndices, |
|||
query |
|||
}) |
|||
); |
|||
} |
|||
|
|||
const searchResults = await Promise.all(promises); |
|||
|
|||
searchResults.forEach(({ items }) => { |
|||
if (items?.length > 0) { |
|||
lookupItems = lookupItems.concat(items); |
|||
} |
|||
}); |
|||
|
|||
const filteredItems = lookupItems |
|||
.filter(({ currency }) => { |
|||
// Only allow symbols with supported currency
|
|||
return currency ? true : false; |
|||
}) |
|||
.sort(({ name: name1 }, { name: name2 }) => { |
|||
return name1?.toLowerCase().localeCompare(name2?.toLowerCase()); |
|||
}) |
|||
.map((lookupItem) => { |
|||
lookupItem.dataProviderInfo = this.getDataProviderInfo(); |
|||
lookupItem.dataSource = 'GHOSTFOLIO'; |
|||
|
|||
return lookupItem; |
|||
}); |
|||
|
|||
results.items = filteredItems; |
|||
return results; |
|||
} catch (error) { |
|||
Logger.error(error, 'GhostfolioService'); |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
private getDataProviderInfo(): DataProviderInfo { |
|||
return { |
|||
isPremium: true, |
|||
name: 'Ghostfolio Premium', |
|||
url: 'https://ghostfol.io' |
|||
}; |
|||
} |
|||
|
|||
private getDataProviderServices() { |
|||
return this.configurationService |
|||
.get('DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER') |
|||
.map((dataSource) => { |
|||
return this.dataProviderService.getDataProvider(DataSource[dataSource]); |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,56 @@ |
|||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; |
|||
|
|||
import { CommonModule } from '@angular/common'; |
|||
import { HttpClient, HttpParams } from '@angular/common/http'; |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { map, Observable, Subject, takeUntil } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
host: { class: 'page' }, |
|||
imports: [CommonModule], |
|||
selector: 'gf-api-page', |
|||
standalone: true, |
|||
styleUrls: ['./api-page.scss'], |
|||
templateUrl: './api-page.html' |
|||
}) |
|||
export class GfApiPageComponent implements OnInit { |
|||
public symbols$: Observable<LookupItem[]>; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor(private http: HttpClient) {} |
|||
|
|||
public ngOnInit() { |
|||
this.symbols$ = this.fetchSymbols({ query: 'apple' }); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
|
|||
private fetchSymbols({ |
|||
includeIndices = false, |
|||
query |
|||
}: { |
|||
includeIndices?: boolean; |
|||
query: string; |
|||
}) { |
|||
let params = new HttpParams().set('query', query); |
|||
|
|||
if (includeIndices) { |
|||
params = params.append('includeIndices', includeIndices); |
|||
} |
|||
|
|||
return this.http |
|||
.get<{ |
|||
items: LookupItem[]; |
|||
}>('/api/v1/data-providers/ghostfolio/lookup', { params }) |
|||
.pipe( |
|||
map(({ items }) => { |
|||
return items; |
|||
}), |
|||
takeUntil(this.unsubscribeSubject) |
|||
); |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
<div class="container"> |
|||
<div> |
|||
<h2 class="text-center">Lookup</h2> |
|||
<ul *ngIf="symbols$ | async as symbols"> |
|||
@for (item of symbols; track item.symbol) { |
|||
<li>{{ item.name }} ({{ item.symbol }})</li> |
|||
} |
|||
</ul> |
|||
</div> |
|||
</div> |
@ -0,0 +1,3 @@ |
|||
:host { |
|||
display: block; |
|||
} |
@ -0,0 +1,2 @@ |
|||
-- AlterEnum |
|||
ALTER TYPE "DataSource" ADD VALUE 'GHOSTFOLIO'; |
Loading…
Reference in new issue