You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

179 lines
5.2 KiB

import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import {
AssetProfileIdentifier,
DataProviderInfo,
ResponseError
} from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { isBefore, isToday } from 'date-fns';
import { isEmpty, uniqBy } from 'lodash';
import { GetValueObject } from './interfaces/get-value-object.interface';
import { GetValuesObject } from './interfaces/get-values-object.interface';
import { GetValuesParams } from './interfaces/get-values-params.interface';
@Injectable()
export class CurrentRateService {
private static readonly MARKET_DATA_PAGE_SIZE = 50000;
public constructor(
private readonly activitiesService: ActivitiesService,
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@LogPerformance
// TODO: Pass user instead of using this.request.user
public async getValues({
dataGatheringItems,
dateQuery
}: GetValuesParams): Promise<GetValuesObject> {
const dataProviderInfos: DataProviderInfo[] = [];
const includesToday =
(!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) &&
(!dateQuery.gte || isBefore(dateQuery.gte, new Date())) &&
(!dateQuery.in || this.containsToday(dateQuery.in));
const quoteErrors: ResponseError['errors'] = [];
const today = new Date();
const values: GetValueObject[] = [];
if (includesToday) {
const quotesBySymbol = await this.dataProviderService.getQuotes({
items: dataGatheringItems,
user: this.request?.user
});
for (const { dataSource, symbol } of dataGatheringItems) {
const quote = quotesBySymbol[symbol];
if (quote?.dataProviderInfo) {
dataProviderInfos.push(quote.dataProviderInfo);
}
if (quote?.marketPrice) {
values.push({
dataSource,
symbol,
date: today,
marketPrice: quote.marketPrice
});
} else {
quoteErrors.push({
dataSource,
symbol
});
}
}
}
const assetProfileIdentifiers: AssetProfileIdentifier[] =
dataGatheringItems.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
});
const marketDataCount = await this.marketDataService.getRangeCount({
assetProfileIdentifiers,
dateQuery
});
for (
let i = 0;
i < marketDataCount;
i += CurrentRateService.MARKET_DATA_PAGE_SIZE
) {
// Use page size to limit the number of records fetched at once
const data = await this.marketDataService.getRange({
assetProfileIdentifiers,
dateQuery,
skip: i,
take: CurrentRateService.MARKET_DATA_PAGE_SIZE
});
values.push(
...data.map(({ dataSource, date, marketPrice, symbol }) => ({
dataSource,
date,
marketPrice,
symbol
}))
);
}
const response: GetValuesObject = {
dataProviderInfos,
errors: quoteErrors.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
}),
values: uniqBy(values, ({ date, symbol }) => {
return `${date}-${symbol}`;
})
};
if (!isEmpty(quoteErrors)) {
for (const { dataSource, symbol } of quoteErrors) {
try {
// If missing quote, fallback to the latest available historical market price
let value: GetValueObject = response.values.find((currentValue) => {
return currentValue.symbol === symbol && isToday(currentValue.date);
});
if (!value) {
// Fallback to unit price of latest activity
const latestActivity =
await this.activitiesService.getLatestActivity({
dataSource,
symbol
});
value = {
dataSource,
symbol,
date: today,
marketPrice: latestActivity?.unitPrice ?? 0
};
response.values.push(value);
}
const [latestValue] = response.values
.filter((currentValue) => {
return currentValue.symbol === symbol && currentValue.marketPrice;
})
.sort((a, b) => {
if (a.date < b.date) {
return 1;
}
if (a.date > b.date) {
return -1;
}
return 0;
});
value.marketPrice = latestValue.marketPrice;
} catch {}
}
}
return response;
}
private containsToday(dates: Date[]): boolean {
for (const date of dates) {
if (isToday(date)) {
return true;
}
}
return false;
}
}