Browse Source

Add historical data endpoint

pull/4016/head
Thomas Kaul 9 months ago
parent
commit
05a572a1d4
  1. 9
      apps/api/src/app/endpoints/data-providers/ghostfolio/get-historical.dto.ts
  2. 42
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  3. 47
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  4. 23
      apps/client/src/app/pages/api/api-page.component.ts
  5. 17
      apps/client/src/app/pages/api/api-page.html
  6. 2
      libs/common/src/lib/interfaces/index.ts
  7. 7
      libs/common/src/lib/interfaces/responses/historical-response.interface.ts

9
apps/api/src/app/endpoints/data-providers/ghostfolio/get-historical.dto.ts

@ -0,0 +1,9 @@
import { IsISO8601 } from 'class-validator';
export class GetHistoricalDto {
@IsISO8601()
from: string;
@IsISO8601()
to: string;
}

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

@ -1,7 +1,9 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { parseDate } from '@ghostfolio/common/helper';
import { import {
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
HistoricalResponse,
LookupResponse, LookupResponse,
QuotesResponse QuotesResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -13,6 +15,7 @@ import {
Get, Get,
HttpException, HttpException,
Inject, Inject,
Param,
Query, Query,
UseGuards UseGuards
} from '@nestjs/common'; } from '@nestjs/common';
@ -20,6 +23,7 @@ import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { getReasonPhrase, StatusCodes } from 'http-status-codes'; import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import { GetHistoricalDto } from './get-historical.dto';
import { GetQuotesDto } from './get-quotes.dto'; import { GetQuotesDto } from './get-quotes.dto';
import { GhostfolioService } from './ghostfolio.service'; import { GhostfolioService } from './ghostfolio.service';
@ -30,7 +34,43 @@ export class GhostfolioController {
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
// TODO: Get historical @Get('historical/:symbol')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getHistorical(
@Param('symbol') symbol: string,
@Query() query: GetHistoricalDto
): Promise<HistoricalResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const historicalData = await this.ghostfolioService.getHistorical({
symbol,
from: parseDate(query.from),
to: parseDate(query.to)
});
await this.ghostfolioService.incrementDailyRequests({
userId: this.request.user.id
});
return historicalData;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
@Get('lookup') @Get('lookup')
@HasPermission(permissions.enableDataProviderGhostfolio) @HasPermission(permissions.enableDataProviderGhostfolio)

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

@ -1,5 +1,6 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
@ -9,6 +10,7 @@ import {
import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config'; import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config';
import { import {
DataProviderInfo, DataProviderInfo,
HistoricalResponse,
LookupItem, LookupItem,
LookupResponse, LookupResponse,
QuotesResponse QuotesResponse
@ -27,6 +29,51 @@ export class GhostfolioService {
private readonly propertyService: PropertyService private readonly propertyService: PropertyService
) {} ) {}
public async getHistorical({
from,
requestTimeout,
to,
symbol
}: {
from: Date;
requestTimeout?: number;
symbol: string;
to: Date;
}) {
const result: HistoricalResponse = { historicalData: {} };
try {
const promises: Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}>[] = [];
for (const dataProviderService of this.getDataProviderServices()) {
promises.push(
dataProviderService
.getHistorical({
from,
requestTimeout,
symbol,
to
})
.then((historicalData) => {
result.historicalData = historicalData[symbol];
return historicalData;
})
);
}
await Promise.all(promises);
return result;
} catch (error) {
Logger.error(error, 'GhostfolioService');
throw error;
}
}
public async getMaxDailyRequests() { public async getMaxDailyRequests() {
return parseInt( return parseInt(
((await this.propertyService.getByKey( ((await this.propertyService.getByKey(

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

@ -1,5 +1,7 @@
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
HistoricalResponse,
LookupResponse, LookupResponse,
QuotesResponse QuotesResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -7,6 +9,7 @@ import {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { format, startOfYear } from 'date-fns';
import { map, Observable, Subject, takeUntil } from 'rxjs'; import { map, Observable, Subject, takeUntil } from 'rxjs';
@Component({ @Component({
@ -18,6 +21,7 @@ import { map, Observable, Subject, takeUntil } from 'rxjs';
templateUrl: './api-page.html' templateUrl: './api-page.html'
}) })
export class GfApiPageComponent implements OnInit { export class GfApiPageComponent implements OnInit {
public historicalData$: Observable<HistoricalResponse['historicalData']>;
public quotes$: Observable<QuotesResponse['quotes']>; public quotes$: Observable<QuotesResponse['quotes']>;
public status$: Observable<DataProviderGhostfolioStatusResponse>; public status$: Observable<DataProviderGhostfolioStatusResponse>;
public symbols$: Observable<LookupResponse['items']>; public symbols$: Observable<LookupResponse['items']>;
@ -27,6 +31,7 @@ export class GfApiPageComponent implements OnInit {
public constructor(private http: HttpClient) {} public constructor(private http: HttpClient) {}
public ngOnInit() { public ngOnInit() {
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL.US' });
this.quotes$ = this.fetchQuotes({ symbols: ['AAPL.US', 'VOO.US'] }); this.quotes$ = this.fetchQuotes({ symbols: ['AAPL.US', 'VOO.US'] });
this.status$ = this.fetchStatus(); this.status$ = this.fetchStatus();
this.symbols$ = this.fetchSymbols({ query: 'apple' }); this.symbols$ = this.fetchSymbols({ query: 'apple' });
@ -37,6 +42,24 @@ export class GfApiPageComponent implements OnInit {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private fetchHistoricalData({ symbol }: { symbol: string }) {
const params = new HttpParams()
.set('from', format(startOfYear(new Date()), DATE_FORMAT))
.set('to', format(new Date(), DATE_FORMAT));
return this.http
.get<HistoricalResponse>(
`/api/v1/data-providers/ghostfolio/historical/${symbol}`,
{ params }
)
.pipe(
map(({ historicalData }) => {
return historicalData;
}),
takeUntil(this.unsubscribeSubject)
);
}
private fetchQuotes({ symbols }: { symbols: string[] }) { private fetchQuotes({ symbols }: { symbols: string[] }) {
const params = new HttpParams().set('symbols', symbols.join(',')); const params = new HttpParams().set('symbols', symbols.join(','));

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

@ -28,4 +28,21 @@
</ul> </ul>
} }
</div> </div>
<div>
<h2 class="text-center">Historical</h2>
@if (historicalData$) {
@let historicalData = historicalData$ | async;
<ul>
@for (
historicalDataItem of historicalData | keyvalue;
track historicalDataItem
) {
<li>
{{ historicalDataItem.key }}:
{{ historicalDataItem.value.marketPrice }}
</li>
}
</ul>
}
</div>
</div> </div>

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

@ -42,6 +42,7 @@ import type { AccountBalancesResponse } from './responses/account-balances-respo
import type { BenchmarkResponse } from './responses/benchmark-response.interface'; import type { BenchmarkResponse } from './responses/benchmark-response.interface';
import type { DataProviderGhostfolioStatusResponse } from './responses/data-provider-ghostfolio-status-response.interface'; import type { DataProviderGhostfolioStatusResponse } from './responses/data-provider-ghostfolio-status-response.interface';
import type { ResponseError } from './responses/errors.interface'; import type { ResponseError } from './responses/errors.interface';
import type { HistoricalResponse } from './responses/historical-response.interface';
import type { ImportResponse } from './responses/import-response.interface'; import type { ImportResponse } from './responses/import-response.interface';
import type { LookupResponse } from './responses/lookup-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface';
@ -83,6 +84,7 @@ export {
Filter, Filter,
FilterGroup, FilterGroup,
HistoricalDataItem, HistoricalDataItem,
HistoricalResponse,
Holding, Holding,
HoldingWithParents, HoldingWithParents,
ImportResponse, ImportResponse,

7
libs/common/src/lib/interfaces/responses/historical-response.interface.ts

@ -0,0 +1,7 @@
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
export interface HistoricalResponse {
historicalData: {
[date: string]: IDataProviderHistoricalResponse;
};
}
Loading…
Cancel
Save