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 { 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…
x
Reference in New Issue
Block a user