Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SERVICES-2541] Timescaledb query for token mini price charts #1441

Open
wants to merge 15 commits into
base: feat/xexchange-v3
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
SERVICES-2541: get single token price candles for the past 7 days
mad2sm0key committed Aug 13, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit daee6890a27bfe3dc5340db56b213b605ed7da4e
15 changes: 15 additions & 0 deletions src/modules/analytics/analytics.resolver.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import { Args, Resolver } from '@nestjs/graphql';
import {
CandleDataModel,
HistoricDataModel,
OhlcvDataModel,
} from 'src/modules/analytics/models/analytics.model';
import { AnalyticsQueryArgs, PriceCandlesQueryArgs } from './models/query.args';
import { AnalyticsAWSGetterService } from './services/analytics.aws.getter.service';
@@ -197,4 +198,18 @@ export class AnalyticsResolver {
args.resolution,
);
}

@Query(() => [OhlcvDataModel])
@UsePipes(
new ValidationPipe({
skipNullProperties: true,
skipMissingProperties: true,
skipUndefinedProperties: true,
}),
)
async tokenPast7dPrice(
@Args('tokenID') tokenID: string,
): Promise<OhlcvDataModel[]> {
return await this.analyticsCompute.tokenPast7dPrice(tokenID);
}
}
29 changes: 29 additions & 0 deletions src/modules/analytics/services/analytics.compute.service.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ import { FarmAbiFactory } from 'src/modules/farm/farm.abi.factory';
import { TokenComputeService } from 'src/modules/tokens/services/token.compute.service';
import { TokenService } from 'src/modules/tokens/services/token.service';
import { ErrorLoggerAsync } from '@multiversx/sdk-nestjs-common';
import { OhlcvDataModel } from '../models/analytics.model';
import moment from 'moment';

@Injectable()
export class AnalyticsComputeService {
@@ -253,6 +255,33 @@ export class AnalyticsComputeService {
});
}

@ErrorLoggerAsync()
@GetOrSetCache({
baseKey: 'analytics',
remoteTtl: Constants.oneMinute() * 30,
localTtl: Constants.oneMinute() * 10,
})
async tokenPast7dPrice(tokenID: string): Promise<OhlcvDataModel[]> {
return await this.computeTokenPast7dPrice(tokenID);
}

async computeTokenPast7dPrice(tokenID: string): Promise<OhlcvDataModel[]> {
const endDate = moment().utc().unix();
const startDate = moment()
.subtract(7, 'days')
.utc()
.startOf('hour')
.unix();

return await this.analyticsQuery.getCandlesWithGapfilling({
series: tokenID,
metric: 'priceUSD',
start: startDate,
end: endDate,
resolution: '4 hours',
});
}

private async fiterPairsByIssuedLpToken(
pairsAddress: string[],
): Promise<string[]> {
Original file line number Diff line number Diff line change
@@ -46,5 +46,13 @@ export interface AnalyticsQueryInterface {
end,
}): Promise<OhlcvDataModel[]>;

getCandlesWithGapfilling({
series,
metric,
resolution,
start,
end,
}): Promise<OhlcvDataModel[]>;

getStartDate(series: string): Promise<string>;
}
8 changes: 8 additions & 0 deletions src/services/analytics/mocks/analytics.query.service.mock.ts
Original file line number Diff line number Diff line change
@@ -52,6 +52,14 @@ export class AnalyticsQueryServiceMock implements AnalyticsQueryInterface {
getCandles({ series, metric, start, end }): Promise<OhlcvDataModel[]> {
throw new Error('Method not implemented.');
}
getCandlesWithGapfilling({
series,
metric,
start,
end,
}): Promise<OhlcvDataModel[]> {
throw new Error('Method not implemented.');
}
getStartDate(series: string): Promise<string> {
throw new Error('Method not implemented.');
}
17 changes: 17 additions & 0 deletions src/services/analytics/services/analytics.query.service.ts
Original file line number Diff line number Diff line change
@@ -116,6 +116,23 @@ export class AnalyticsQueryService implements AnalyticsQueryInterface {
});
}

async getCandlesWithGapfilling({
series,
metric,
resolution,
start,
end,
}): Promise<OhlcvDataModel[]> {
const service = await this.getService();
return await service.getCandlesWithGapfilling({
series,
metric,
resolution,
start,
end,
});
}

async getStartDate(series: string): Promise<string | undefined> {
const service = await this.getService();
return await service.getStartDate(series);
128 changes: 128 additions & 0 deletions src/services/analytics/timescaledb/timescaledb.query.service.ts
Original file line number Diff line number Diff line change
@@ -534,6 +534,58 @@ export class TimescaleDBQueryService implements AnalyticsQueryInterface {
);
}

@TimescaleDBQuery()
async getCandlesWithGapfilling({
series,
metric,
resolution,
start,
end,
}): Promise<OhlcvDataModel[]> {
try {
const candleRepository =
this.getCandleRepositoryByResolutionAndMetric(
resolution,
metric,
);
const startDate = moment.unix(start).utc().toDate();
const endDate = moment.unix(end).utc().toDate();

const queryResult = await candleRepository
.createQueryBuilder()
.select(`time_bucket_gapfill('${resolution}', time) as bucket`)
.addSelect('locf(first(open, time)) as open')
.addSelect('locf(max(high)) as high')
.addSelect('locf(min(low)) as low')
.addSelect('locf(last(close, time)) as close')
.addSelect('locf(sum(volume)) as volume')
.where('series = :series', { series })
.andWhere('time between :startDate and :endDate', {
startDate,
endDate,
})
.groupBy('bucket')
.getRawMany();

return await this.gapfillCandleData(
queryResult,
series,
metric,
startDate,
candleRepository,
);
} catch (error) {
this.logger.error('getTokenPriceCandles', {
series,
resolution,
start,
end,
error,
});
return [];
}
}

@TimescaleDBQuery()
async getCandles({
series,
@@ -721,4 +773,80 @@ export class TimescaleDBQueryService implements AnalyticsQueryInterface {
}),
);
}

private async gapfillCandleData(
data: any[],
series: string,
metric: string,
previousStartDate: Date,
repository: Repository<
| TokenCandlesMinute
| TokenCandlesHourly
| TokenCandlesDaily
| PairFirstTokenCandlesMinute
| PairFirstTokenCandlesHourly
| PairFirstTokenCandlesDaily
| PairSecondTokenCandlesMinute
| PairSecondTokenCandlesHourly
| PairSecondTokenCandlesDaily
>,
): Promise<OhlcvDataModel[]> {
if (!data || data.length === 0) {
return [];
}

if (data[0].open) {
return this.formatCandleData(data);
}

const startDate = await this.getStartDate(series);
const endDate = previousStartDate;

if (!startDate) {
return this.formatCandleData(data);
}

const previousValue = await repository
.createQueryBuilder()
.select('open, high, low, close, volume')
.where('series = :series', { series })
.andWhere('time between :start and :end', {
start: moment(startDate).utc().toDate(),
end: endDate,
})
.orderBy('time', 'DESC')
.limit(1)
.getRawOne();

if (!previousValue) {
return this.formatCandleData(data);
}

return this.formatCandleData(data, previousValue);
}

private formatCandleData(
data: any[],
gapfillValue = {
open: '0',
high: '0',
low: '0',
close: '0',
volume: '0',
},
): OhlcvDataModel[] {
return data.map(
(row) =>
new OhlcvDataModel({
time: row.bucket,
ohlcv: [
row.open ?? gapfillValue.open,
row.high ?? gapfillValue.high,
row.low ?? gapfillValue.low,
row.close ?? gapfillValue.close,
row.volume ?? gapfillValue.volume,
],
}),
);
}
}