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

Add endpoint to fetch mid prices for perpetual markets #2358

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 2 additions & 41 deletions .github/workflows/indexer-build-and-push-dev-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,14 @@ on: # yamllint disable-line rule:truthy
- main
- 'release/indexer/v[0-9]+.[0-9]+.x' # e.g. release/indexer/v0.1.x
- 'release/indexer/v[0-9]+.x' # e.g. release/indexer/v1.x
- 'adam/ct-1210-new-indexer-endpoint-for-all-mid-market-prices'
# TODO(DEC-837): Customize github build and push to ECR by service with paths

jobs:
# Build and push to dev
call-build-and-push-ecs-services-dev:
name: (Dev) Build and Push ECS Services
uses: ./.github/workflows/indexer-build-and-push-all-ecr-images.yml
with:
ENVIRONMENT: dev
secrets: inherit

# Build and push to dev2
call-build-and-push-ecs-services-dev2:
name: (Dev2) Build and Push ECS Services
uses: ./.github/workflows/indexer-build-and-push-all-ecr-images.yml
with:
ENVIRONMENT: dev2
secrets: inherit

# Build and push to dev3
call-build-and-push-ecs-services-dev3:
name: (Dev3) Build and Push ECS Services
uses: ./.github/workflows/indexer-build-and-push-all-ecr-images.yml
with:
ENVIRONMENT: dev3
secrets: inherit

# Build and push to dev4
call-build-and-push-ecs-services-dev4:
name: (Dev4) Build and Push ECS Services
uses: ./.github/workflows/indexer-build-and-push-all-ecr-images.yml
with:
ENVIRONMENT: dev4
secrets: inherit

# Build and push to dev5
call-build-and-push-ecs-services-dev5:
name: (Dev5) Build and Push ECS Services
uses: ./.github/workflows/indexer-build-and-push-all-ecr-images.yml
with:
ENVIRONMENT: dev5
secrets: inherit

# Build and push to staging
call-build-and-push-ecs-services-staging:
name: (Staging) Build and Push ECS Services
uses: ./.github/workflows/indexer-build-and-push-all-ecr-images.yml
with:
ENVIRONMENT: staging
secrets: inherit
secrets: inherit
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
defaultLiquidityTier2,
defaultMarket,
defaultMarket2,
defaultMarket3,
defaultPerpetualMarket,
defaultPerpetualMarket2,
defaultPerpetualMarket3,
invalidTicker,
} from '../helpers/constants';
import * as MarketTable from '../../src/stores/market-table';
Expand All @@ -21,6 +24,7 @@ describe('PerpetualMarket store', () => {
await Promise.all([
MarketTable.create(defaultMarket),
MarketTable.create(defaultMarket2),
MarketTable.create(defaultMarket3),
]);
await Promise.all([
LiquidityTiersTable.create(defaultLiquidityTier),
Expand Down Expand Up @@ -192,4 +196,55 @@ describe('PerpetualMarket store', () => {
trades24H: 100,
}));
});

it('Successfully finds all PerpetualMarkets with specific tickers', async () => {
await Promise.all([
PerpetualMarketTable.create(defaultPerpetualMarket),
PerpetualMarketTable.create(defaultPerpetualMarket2),
PerpetualMarketTable.create(defaultPerpetualMarket3),
]);

const perpetualMarkets: PerpetualMarketFromDatabase[] = await PerpetualMarketTable.findAll(
{ tickers: ['BTC-USD', 'ETH-USD'] },
[],
{ readReplica: true },
);

expect(perpetualMarkets.length).toEqual(2);
expect(perpetualMarkets[0].ticker).toEqual('BTC-USD');
expect(perpetualMarkets[1].ticker).toEqual('ETH-USD');
});

it('Returns empty array when no PerpetualMarkets match the given tickers', async () => {
const perpetualMarkets: PerpetualMarketFromDatabase[] = await PerpetualMarketTable.findAll(
{ tickers: ['BAD-TICKER'] },
[],
);

expect(perpetualMarkets.length).toEqual(0);
});

it('Successfully combines tickers filter with other filters', async () => {
await Promise.all([
PerpetualMarketTable.create(defaultPerpetualMarket),
PerpetualMarketTable.create(defaultPerpetualMarket2),
PerpetualMarketTable.create({
...defaultPerpetualMarket3,
liquidityTierId: defaultLiquidityTier2.id,
}),
]);

const perpetualMarkets: PerpetualMarketFromDatabase[] = await PerpetualMarketTable.findAll(
{
tickers: ['BTC-USD', 'ETH-USD', 'LINK-USD'],
liquidityTierId: [defaultLiquidityTier.id],
},
[],
{ readReplica: true },
);

expect(perpetualMarkets.length).toEqual(2);
expect(perpetualMarkets[0].ticker).toEqual('BTC-USD');
expect(perpetualMarkets[1].ticker).toEqual('ETH-USD');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export async function findAll(
id,
marketId,
liquidityTierId,
tickers,
limit,
}: PerpetualMarketQueryConfig,
requiredFields: QueryableField[],
Expand Down Expand Up @@ -59,6 +60,10 @@ export async function findAll(
baseQuery = baseQuery.whereIn(PerpetualMarketColumns.marketId, marketId);
}

if (tickers !== undefined) {
baseQuery = baseQuery.whereIn(PerpetualMarketColumns.ticker, tickers);
}

if (liquidityTierId !== undefined) {
baseQuery = baseQuery.whereIn(PerpetualMarketColumns.liquidityTierId, liquidityTierId);
}
Expand Down
2 changes: 2 additions & 0 deletions indexer/packages/postgres/src/types/query-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export enum QueryableField {
GOOD_TIL_BLOCK_BEFORE_OR_AT = 'goodTilBlockBeforeOrAt',
GOOD_TIL_BLOCK_TIME_BEFORE_OR_AT = 'goodTilBlockTimeBeforeOrAt',
TICKER = 'ticker',
TICKERS = 'tickers',
RESOLUTION = 'resolution',
FROM_ISO = 'fromISO',
TO_ISO = 'toISO',
Expand Down Expand Up @@ -149,6 +150,7 @@ export interface OrderQueryConfig extends QueryConfig {
export interface PerpetualMarketQueryConfig extends QueryConfig {
[QueryableField.ID]?: string[],
[QueryableField.MARKET_ID]?: number[],
[QueryableField.TICKERS]?: string[],
[QueryableField.LIQUIDITY_TIER_ID]?: number[],
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ import {
LiquidityTiersTable,
liquidityTierRefresher,
} from '@dydxprotocol-indexer/postgres';
import {
OrderbookMidPricesCache,
} from '@dydxprotocol-indexer/redis';
import { RequestMethod } from '../../../../src/types';
import request from 'supertest';
import { getQueryString, sendRequest } from '../../../helpers/helpers';
import _ from 'lodash';
import { perpetualMarketToResponseObject } from '../../../../src/request-helpers/request-transformer';

jest.mock('@dydxprotocol-indexer/redis', () => ({
...jest.requireActual('@dydxprotocol-indexer/redis'),
OrderbookMidPricesCache: {
getMedianPrice: jest.fn(),
},
}));

describe('perpetual-markets-controller#V4', () => {
beforeAll(async () => {
await dbHelpers.migrate();
Expand All @@ -30,10 +40,12 @@ describe('perpetual-markets-controller#V4', () => {

afterAll(async () => {
await dbHelpers.teardown();
jest.resetAllMocks();
});

afterEach(async () => {
await dbHelpers.clearData();
jest.clearAllMocks();
});

describe('/', () => {
Expand Down Expand Up @@ -128,6 +140,132 @@ describe('perpetual-markets-controller#V4', () => {
}));
});
});

describe('GET /v4/perpetualMarkets/orderbookMidPrices', () => {
it('returns mid prices for all markets when no tickers are specified', async () => {
(OrderbookMidPricesCache.getMedianPrice as jest.Mock).mockImplementation((client, ticker) => {
const prices: {[key: string]: string} = {
'BTC-USD': '30000.5',
'ETH-USD': '2000.25',
'SHIB-USD': '5.75',
};
return Promise.resolve(prices[ticker]);
});

const response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/perpetualMarkets/orderbookMidPrices',
});

expect(response.status).toBe(200);
expect(response.body).toEqual({
'BTC-USD': '30000.5',
'ETH-USD': '2000.25',
'SHIB-USD': '5.75',
});
const numMarkets = (await PerpetualMarketTable.findAll({}, [])).length;
expect(OrderbookMidPricesCache.getMedianPrice).toHaveBeenCalledTimes(numMarkets);
});

it('returns mid prices for multiple specified tickers', async () => {
(OrderbookMidPricesCache.getMedianPrice as jest.Mock).mockImplementation((client, ticker) => {
const prices: {[key: string]: string} = {
'BTC-USD': '30000.5',
'ETH-USD': '2000.25',
};
return Promise.resolve(prices[ticker]);
});

const response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/perpetualMarkets/orderbookMidPrices?tickers=BTC-USD&tickers=ETH-USD',
});

expect(response.status).toBe(200);
expect(response.body).toEqual({
'BTC-USD': '30000.5',
'ETH-USD': '2000.25',
});

expect(OrderbookMidPricesCache.getMedianPrice).toHaveBeenCalledTimes(2);
});

it('returns mid prices for one specified ticker', async () => {
(OrderbookMidPricesCache.getMedianPrice as jest.Mock).mockImplementation((client, ticker) => {
const prices: {[key: string]: string} = {
'BTC-USD': '30000.5',
'ETH-USD': '2000.25',
};
return Promise.resolve(prices[ticker]);
});

const response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/perpetualMarkets/orderbookMidPrices?tickers=BTC-USD',
});

expect(response.status).toBe(200);
expect(response.body).toEqual({
'BTC-USD': '30000.5',
});

expect(OrderbookMidPricesCache.getMedianPrice).toHaveBeenCalledTimes(1);
});

it('omits markets with no mid price', async () => {
(OrderbookMidPricesCache.getMedianPrice as jest.Mock).mockImplementation((client, ticker) => {
const prices: {[key: string]: string | null} = {
'BTC-USD': '30000.5',
'ETH-USD': null,
'SHIB-USD': '5.75',
};
return Promise.resolve(prices[ticker]);
});

const response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/perpetualMarkets/orderbookMidPrices',
});

expect(response.status).toBe(200);
expect(response.body).toEqual({
'BTC-USD': '30000.5',
'SHIB-USD': '5.75',
});
});

it('returns an empty object when no markets have mid prices', async () => {
(OrderbookMidPricesCache.getMedianPrice as jest.Mock).mockResolvedValue(null);

const response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/perpetualMarkets/orderbookMidPrices',
});

expect(response.status).toBe(200);
expect(response.body).toEqual({});
});

it('returns prices only for valid tickers and ignores invalid tickers', async () => {
(OrderbookMidPricesCache.getMedianPrice as jest.Mock).mockImplementation((client, ticker) => {
const prices: {[key: string]: string} = {
'BTC-USD': '30000.5',
};
return Promise.resolve(prices[ticker]);
});

const response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/perpetualMarkets/orderbookMidPrices?tickers=BTC-USD&tickers=INVALID-TICKER',
});

expect(response.status).toBe(200);
expect(response.body).toEqual({
'BTC-USD': '30000.5',
});
expect(OrderbookMidPricesCache.getMedianPrice).toHaveBeenCalledTimes(1);
});
});
});

function expectResponseWithMarkets(
Expand Down
Loading
Loading