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