Browse Source

Add quotes endpoint

pull/4016/head
Thomas Kaul 10 months ago
parent
commit
9610e1e627
  1. 10
      apps/api/src/app/endpoints/data-providers/ghostfolio/get-quotes.dto.ts
  2. 41
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  3. 93
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  4. 20
      apps/client/src/app/pages/api/api-page.component.ts
  5. 21
      apps/client/src/app/pages/api/api-page.html
  6. 2
      libs/common/src/lib/interfaces/index.ts
  7. 5
      libs/common/src/lib/interfaces/responses/quotes-response.interface.ts

10
apps/api/src/app/endpoints/data-providers/ghostfolio/get-quotes.dto.ts

@ -0,0 +1,10 @@
import { Transform } from 'class-transformer';
import { IsString } from 'class-validator';
export class GetQuotesDto {
@IsString({ each: true })
@Transform(({ value }) =>
typeof value === 'string' ? value.split(',') : value
)
symbols: string[];
}

41
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -5,7 +5,8 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config';
import {
DataProviderGhostfolioStatusResponse,
LookupResponse
LookupResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types';
@ -22,6 +23,7 @@ import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import { GetQuotesDto } from './get-quotes.dto';
import { GhostfolioService } from './ghostfolio.service';
@Controller('data-providers/ghostfolio')
@ -33,6 +35,8 @@ export class GhostfolioController {
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
// TODO: Get historical
@Get('lookup')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@ -71,6 +75,41 @@ export class GhostfolioController {
}
}
@Get('quotes')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getQuotes(
@Query() query: GetQuotesDto
): Promise<QuotesResponse> {
const maxDailyRequests = await this.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const quotes = await this.ghostfolioService.getQuotes({
symbols: query.symbols
});
await this.incrementDailyRequests({
userId: this.request.user.id
});
return quotes;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get('status')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)

93
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -1,14 +1,21 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import {
DEFAULT_CURRENCY,
DERIVED_CURRENCIES
} from '@ghostfolio/common/config';
import {
DataProviderInfo,
LookupItem,
LookupResponse
LookupResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import Big = require('big.js');
@Injectable()
export class GhostfolioService {
public constructor(
@ -16,6 +23,90 @@ export class GhostfolioService {
private readonly dataProviderService: DataProviderService
) {}
public async getQuotes({
requestTimeout,
symbols
}: {
requestTimeout?: number;
symbols: string[];
}) {
const promises: Promise<any>[] = [];
const results: QuotesResponse = { quotes: {} };
try {
for (const dataProvider of this.getDataProviderServices()) {
const maximumNumberOfSymbolsPerRequest =
dataProvider.getMaxNumberOfSymbolsPerRequest?.() ??
Number.MAX_SAFE_INTEGER;
for (
let i = 0;
i < symbols.length;
i += maximumNumberOfSymbolsPerRequest
) {
const symbolsChunk = symbols.slice(
i,
i + maximumNumberOfSymbolsPerRequest
);
const promise = Promise.resolve(
dataProvider.getQuotes({ requestTimeout, symbols: symbolsChunk })
);
promises.push(
promise.then(async (result) => {
for (const [symbol, dataProviderResponse] of Object.entries(
result
)) {
dataProviderResponse.dataSource = 'GHOSTFOLIO';
if (
[
...DERIVED_CURRENCIES.map(({ currency }) => {
return `${DEFAULT_CURRENCY}${currency}`;
}),
`${DEFAULT_CURRENCY}USX`
].includes(symbol)
) {
continue;
}
results.quotes[symbol] = dataProviderResponse;
for (const {
currency,
factor,
rootCurrency
} of DERIVED_CURRENCIES) {
if (symbol === `${DEFAULT_CURRENCY}${rootCurrency}`) {
results.quotes[`${DEFAULT_CURRENCY}${currency}`] = {
...dataProviderResponse,
currency,
marketPrice: new Big(
result[`${DEFAULT_CURRENCY}${rootCurrency}`].marketPrice
)
.mul(factor)
.toNumber(),
marketState: 'open'
};
}
}
}
})
);
}
await Promise.all(promises);
}
return results;
} catch (error) {
Logger.error(error, 'GhostfolioService');
throw error;
}
}
public async lookup({
includeIndices = false,
query

20
apps/client/src/app/pages/api/api-page.component.ts

@ -1,6 +1,7 @@
import {
DataProviderGhostfolioStatusResponse,
LookupResponse
LookupResponse,
QuotesResponse
} from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
@ -17,6 +18,7 @@ import { map, Observable, Subject, takeUntil } from 'rxjs';
templateUrl: './api-page.html'
})
export class GfApiPageComponent implements OnInit {
public quotes$: Observable<QuotesResponse['quotes']>;
public status$: Observable<DataProviderGhostfolioStatusResponse>;
public symbols$: Observable<LookupResponse['items']>;
@ -25,6 +27,7 @@ export class GfApiPageComponent implements OnInit {
public constructor(private http: HttpClient) {}
public ngOnInit() {
this.quotes$ = this.fetchQuotes({ symbols: ['AAPL.US', 'VOO.US'] });
this.status$ = this.fetchStatus();
this.symbols$ = this.fetchSymbols({ query: 'apple' });
}
@ -34,6 +37,21 @@ export class GfApiPageComponent implements OnInit {
this.unsubscribeSubject.complete();
}
private fetchQuotes({ symbols }: { symbols: string[] }) {
const params = new HttpParams().set('symbols', symbols.join(','));
return this.http
.get<QuotesResponse>('/api/v1/data-providers/ghostfolio/quotes', {
params
})
.pipe(
map(({ quotes }) => {
return quotes;
}),
takeUntil(this.unsubscribeSubject)
);
}
private fetchStatus() {
return this.http
.get<DataProviderGhostfolioStatusResponse>(

21
apps/client/src/app/pages/api/api-page.html

@ -3,12 +3,29 @@
<h2 class="text-center">Status</h2>
<div>{{ status$ | async | json }}</div>
</div>
<div>
<div class="mb-3">
<h2 class="text-center">Lookup</h2>
<ul *ngIf="symbols$ | async as symbols">
@if (symbols$) {
@let symbols = symbols$ | async;
<ul>
@for (item of symbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
}
</div>
<div>
<h2 class="text-center">Quotes</h2>
@if (quotes$) {
@let quotes = quotes$ | async;
<ul>
@for (quote of quotes | keyvalue; track quote) {
<li>
{{ quote.key }}: {{ quote.value.marketPrice }}
{{ quote.value.currency }}
</li>
}
</ul>
}
</div>
</div>

2
libs/common/src/lib/interfaces/index.ts

@ -48,6 +48,7 @@ import type { OAuthResponse } from './responses/oauth-response.interface';
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface';
import type { QuotesResponse } from './responses/quotes-response.interface';
import type { ScraperConfiguration } from './scraper-configuration.interface';
import type { Statistics } from './statistics.interface';
import type { SubscriptionOffer } from './subscription-offer.interface';
@ -107,6 +108,7 @@ export {
Position,
Product,
PublicPortfolioResponse,
QuotesResponse,
ResponseError,
ScraperConfiguration,
Statistics,

5
libs/common/src/lib/interfaces/responses/quotes-response.interface.ts

@ -0,0 +1,5 @@
import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface QuotesResponse {
quotes: { [symbol: string]: IDataProviderResponse };
}
Loading…
Cancel
Save