Feature/add support for dividends in Ghostfolio data provider (#4081)
* Add support for dividends
This commit is contained in:
parent
c6525ec0f4
commit
2067e8ea40
@ -0,0 +1,15 @@
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
|
||||
import { IsIn, IsISO8601, IsOptional } from 'class-validator';
|
||||
|
||||
export class GetDividendsDto {
|
||||
@IsISO8601()
|
||||
from: string;
|
||||
|
||||
@IsIn(['day', 'month'] as Granularity[])
|
||||
@IsOptional()
|
||||
granularity: Granularity;
|
||||
|
||||
@IsISO8601()
|
||||
to: string;
|
||||
}
|
@ -3,6 +3,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderGhostfolioStatusResponse,
|
||||
DividendsResponse,
|
||||
HistoricalResponse,
|
||||
LookupResponse,
|
||||
QuotesResponse
|
||||
@ -23,6 +24,7 @@ import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
|
||||
|
||||
import { GetDividendsDto } from './get-dividends.dto';
|
||||
import { GetHistoricalDto } from './get-historical.dto';
|
||||
import { GetQuotesDto } from './get-quotes.dto';
|
||||
import { GhostfolioService } from './ghostfolio.service';
|
||||
@ -34,6 +36,45 @@ export class GhostfolioController {
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
|
||||
@Get('dividends/:symbol')
|
||||
@HasPermission(permissions.enableDataProviderGhostfolio)
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
public async getDividends(
|
||||
@Param('symbol') symbol: string,
|
||||
@Query() query: GetDividendsDto
|
||||
): Promise<DividendsResponse> {
|
||||
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 dividends = await this.ghostfolioService.getDividends({
|
||||
symbol,
|
||||
from: parseDate(query.from),
|
||||
granularity: query.granularity,
|
||||
to: parseDate(query.to)
|
||||
});
|
||||
|
||||
await this.ghostfolioService.incrementDailyRequests({
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
return dividends;
|
||||
} catch {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
||||
StatusCodes.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('historical/:symbol')
|
||||
@HasPermission(permissions.enableDataProviderGhostfolio)
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import {
|
||||
GetDividendsParams,
|
||||
GetHistoricalParams,
|
||||
GetQuotesParams,
|
||||
GetSearchParams
|
||||
@ -15,6 +16,7 @@ import {
|
||||
import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DataProviderInfo,
|
||||
DividendsResponse,
|
||||
HistoricalResponse,
|
||||
LookupItem,
|
||||
LookupResponse,
|
||||
@ -34,6 +36,48 @@ export class GhostfolioService {
|
||||
private readonly propertyService: PropertyService
|
||||
) {}
|
||||
|
||||
public async getDividends({
|
||||
from,
|
||||
granularity,
|
||||
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
|
||||
symbol,
|
||||
to
|
||||
}: GetDividendsParams) {
|
||||
const result: DividendsResponse = { dividends: {} };
|
||||
|
||||
try {
|
||||
const promises: Promise<{
|
||||
[date: string]: IDataProviderHistoricalResponse;
|
||||
}>[] = [];
|
||||
|
||||
for (const dataProviderService of this.getDataProviderServices()) {
|
||||
promises.push(
|
||||
dataProviderService
|
||||
.getDividends({
|
||||
from,
|
||||
granularity,
|
||||
requestTimeout,
|
||||
symbol,
|
||||
to
|
||||
})
|
||||
.then((dividends) => {
|
||||
result.dividends = dividends;
|
||||
|
||||
return dividends;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'GhostfolioService');
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getHistorical({
|
||||
from,
|
||||
granularity,
|
||||
@ -86,10 +130,11 @@ export class GhostfolioService {
|
||||
}
|
||||
|
||||
public async getQuotes({ requestTimeout, symbols }: GetQuotesParams) {
|
||||
const promises: Promise<any>[] = [];
|
||||
const results: QuotesResponse = { quotes: {} };
|
||||
|
||||
try {
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
for (const dataProvider of this.getDataProviderServices()) {
|
||||
const maximumNumberOfSymbolsPerRequest =
|
||||
dataProvider.getMaxNumberOfSymbolsPerRequest?.() ??
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderInfo,
|
||||
DividendsResponse,
|
||||
HistoricalResponse,
|
||||
LookupResponse,
|
||||
QuotesResponse
|
||||
@ -71,8 +72,53 @@ export class GhostfolioService implements DataProviderInterface {
|
||||
};
|
||||
}
|
||||
|
||||
public async getDividends({}: GetDividendsParams) {
|
||||
return {};
|
||||
public async getDividends({
|
||||
from,
|
||||
granularity = 'day',
|
||||
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
|
||||
symbol,
|
||||
to
|
||||
}: GetDividendsParams): Promise<{
|
||||
[date: string]: IDataProviderHistoricalResponse;
|
||||
}> {
|
||||
let response: {
|
||||
[date: string]: IDataProviderHistoricalResponse;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, requestTimeout);
|
||||
|
||||
const { dividends } = await got(
|
||||
`${this.URL}/v1/data-providers/ghostfolio/dividends/${symbol}?from=${format(from, DATE_FORMAT)}&granularity=${granularity}&to=${format(
|
||||
to,
|
||||
DATE_FORMAT
|
||||
)}`,
|
||||
{
|
||||
headers: await this.getRequestHeaders(),
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<DividendsResponse>();
|
||||
|
||||
response = dividends;
|
||||
} catch (error) {
|
||||
let message = error;
|
||||
|
||||
if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) {
|
||||
message = 'RequestError: The daily request limit has been exceeded';
|
||||
} else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) {
|
||||
message =
|
||||
'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.';
|
||||
}
|
||||
|
||||
Logger.error(message, 'GhostfolioService');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async getHistorical({
|
||||
|
@ -21,7 +21,13 @@ export interface DataProviderInterface {
|
||||
|
||||
getDataProviderInfo(): DataProviderInfo;
|
||||
|
||||
getDividends({ from, granularity, symbol, to }: GetDividendsParams): Promise<{
|
||||
getDividends({
|
||||
from,
|
||||
granularity,
|
||||
requestTimeout,
|
||||
symbol,
|
||||
to
|
||||
}: GetDividendsParams): Promise<{
|
||||
[date: string]: IDataProviderHistoricalResponse;
|
||||
}>;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderGhostfolioStatusResponse,
|
||||
DividendsResponse,
|
||||
HistoricalResponse,
|
||||
LookupResponse,
|
||||
QuotesResponse
|
||||
@ -21,6 +22,7 @@ import { map, Observable, Subject, takeUntil } from 'rxjs';
|
||||
templateUrl: './api-page.html'
|
||||
})
|
||||
export class GfApiPageComponent implements OnInit {
|
||||
public dividends$: Observable<DividendsResponse['dividends']>;
|
||||
public historicalData$: Observable<HistoricalResponse['historicalData']>;
|
||||
public quotes$: Observable<QuotesResponse['quotes']>;
|
||||
public status$: Observable<DataProviderGhostfolioStatusResponse>;
|
||||
@ -31,6 +33,7 @@ export class GfApiPageComponent implements OnInit {
|
||||
public constructor(private http: HttpClient) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.dividends$ = this.fetchDividends({ symbol: 'KO' });
|
||||
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL.US' });
|
||||
this.quotes$ = this.fetchQuotes({ symbols: ['AAPL.US', 'VOO.US'] });
|
||||
this.status$ = this.fetchStatus();
|
||||
@ -42,6 +45,24 @@ export class GfApiPageComponent implements OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private fetchDividends({ 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<DividendsResponse>(
|
||||
`/api/v1/data-providers/ghostfolio/dividends/${symbol}`,
|
||||
{ params }
|
||||
)
|
||||
.pipe(
|
||||
map(({ dividends }) => {
|
||||
return dividends;
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
);
|
||||
}
|
||||
|
||||
private fetchHistoricalData({ symbol }: { symbol: string }) {
|
||||
const params = new HttpParams()
|
||||
.set('from', format(startOfYear(new Date()), DATE_FORMAT))
|
||||
|
@ -45,4 +45,18 @@
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-center">Dividends</h2>
|
||||
@if (dividends$) {
|
||||
@let dividends = dividends$ | async;
|
||||
<ul>
|
||||
@for (dividend of dividends | keyvalue; track dividend) {
|
||||
<li>
|
||||
{{ dividend.key }}:
|
||||
{{ dividend.value.marketPrice }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,6 +41,7 @@ import type { Product } from './product';
|
||||
import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
|
||||
import type { BenchmarkResponse } from './responses/benchmark-response.interface';
|
||||
import type { DataProviderGhostfolioStatusResponse } from './responses/data-provider-ghostfolio-status-response.interface';
|
||||
import type { DividendsResponse } from './responses/dividends-response.interface';
|
||||
import type { ResponseError } from './responses/errors.interface';
|
||||
import type { HistoricalResponse } from './responses/historical-response.interface';
|
||||
import type { ImportResponse } from './responses/import-response.interface';
|
||||
@ -79,6 +80,7 @@ export {
|
||||
Coupon,
|
||||
DataProviderGhostfolioStatusResponse,
|
||||
DataProviderInfo,
|
||||
DividendsResponse,
|
||||
EnhancedSymbolProfile,
|
||||
Export,
|
||||
Filter,
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
|
||||
export interface DividendsResponse {
|
||||
dividends: {
|
||||
[date: string]: IDataProviderHistoricalResponse;
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user