Feature/add support for dividends in Ghostfolio data provider (#4081)

* Add support for dividends
pull/4086/head
Thomas Kaul 3 weeks ago committed by GitHub
parent c6525ec0f4
commit 2067e8ea40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 { parseDate } from '@ghostfolio/common/helper';
import { import {
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
DividendsResponse,
HistoricalResponse, HistoricalResponse,
LookupResponse, LookupResponse,
QuotesResponse QuotesResponse
@ -23,6 +24,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 { GetDividendsDto } from './get-dividends.dto';
import { GetHistoricalDto } from './get-historical.dto'; 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';
@ -34,6 +36,45 @@ export class GhostfolioController {
@Inject(REQUEST) private readonly request: RequestWithUser @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') @Get('historical/:symbol')
@HasPermission(permissions.enableDataProviderGhostfolio) @HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)

@ -1,6 +1,7 @@
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 { import {
GetDividendsParams,
GetHistoricalParams, GetHistoricalParams,
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
@ -15,6 +16,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,
DividendsResponse,
HistoricalResponse, HistoricalResponse,
LookupItem, LookupItem,
LookupResponse, LookupResponse,
@ -34,6 +36,48 @@ export class GhostfolioService {
private readonly propertyService: PropertyService 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({ public async getHistorical({
from, from,
granularity, granularity,
@ -86,10 +130,11 @@ export class GhostfolioService {
} }
public async getQuotes({ requestTimeout, symbols }: GetQuotesParams) { public async getQuotes({ requestTimeout, symbols }: GetQuotesParams) {
const promises: Promise<any>[] = [];
const results: QuotesResponse = { quotes: {} }; const results: QuotesResponse = { quotes: {} };
try { try {
const promises: Promise<any>[] = [];
for (const dataProvider of this.getDataProviderServices()) { for (const dataProvider of this.getDataProviderServices()) {
const maximumNumberOfSymbolsPerRequest = const maximumNumberOfSymbolsPerRequest =
dataProvider.getMaxNumberOfSymbolsPerRequest?.() ?? dataProvider.getMaxNumberOfSymbolsPerRequest?.() ??

@ -19,6 +19,7 @@ import {
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
DataProviderInfo, DataProviderInfo,
DividendsResponse,
HistoricalResponse, HistoricalResponse,
LookupResponse, LookupResponse,
QuotesResponse QuotesResponse
@ -71,8 +72,53 @@ export class GhostfolioService implements DataProviderInterface {
}; };
} }
public async getDividends({}: GetDividendsParams) { public async getDividends({
return {}; 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({ public async getHistorical({

@ -21,7 +21,13 @@ export interface DataProviderInterface {
getDataProviderInfo(): DataProviderInfo; getDataProviderInfo(): DataProviderInfo;
getDividends({ from, granularity, symbol, to }: GetDividendsParams): Promise<{ getDividends({
from,
granularity,
requestTimeout,
symbol,
to
}: GetDividendsParams): Promise<{
[date: string]: IDataProviderHistoricalResponse; [date: string]: IDataProviderHistoricalResponse;
}>; }>;

@ -1,6 +1,7 @@
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
DividendsResponse,
HistoricalResponse, HistoricalResponse,
LookupResponse, LookupResponse,
QuotesResponse QuotesResponse
@ -21,6 +22,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 dividends$: Observable<DividendsResponse['dividends']>;
public historicalData$: Observable<HistoricalResponse['historicalData']>; public historicalData$: Observable<HistoricalResponse['historicalData']>;
public quotes$: Observable<QuotesResponse['quotes']>; public quotes$: Observable<QuotesResponse['quotes']>;
public status$: Observable<DataProviderGhostfolioStatusResponse>; public status$: Observable<DataProviderGhostfolioStatusResponse>;
@ -31,6 +33,7 @@ export class GfApiPageComponent implements OnInit {
public constructor(private http: HttpClient) {} public constructor(private http: HttpClient) {}
public ngOnInit() { public ngOnInit() {
this.dividends$ = this.fetchDividends({ symbol: 'KO' });
this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL.US' }); 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();
@ -42,6 +45,24 @@ export class GfApiPageComponent implements OnInit {
this.unsubscribeSubject.complete(); 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 }) { private fetchHistoricalData({ symbol }: { symbol: string }) {
const params = new HttpParams() const params = new HttpParams()
.set('from', format(startOfYear(new Date()), DATE_FORMAT)) .set('from', format(startOfYear(new Date()), DATE_FORMAT))

@ -45,4 +45,18 @@
</ul> </ul>
} }
</div> </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> </div>

@ -41,6 +41,7 @@ import type { Product } from './product';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface'; import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
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 { DividendsResponse } from './responses/dividends-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 { HistoricalResponse } from './responses/historical-response.interface';
import type { ImportResponse } from './responses/import-response.interface'; import type { ImportResponse } from './responses/import-response.interface';
@ -79,6 +80,7 @@ export {
Coupon, Coupon,
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
DataProviderInfo, DataProviderInfo,
DividendsResponse,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Export, Export,
Filter, Filter,

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