diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/vault-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/vault-controller.test.ts index e53a9b5b76..72ae841616 100644 --- a/indexer/services/comlink/__tests__/controllers/api/v4/vault-controller.test.ts +++ b/indexer/services/comlink/__tests__/controllers/api/v4/vault-controller.test.ts @@ -24,13 +24,15 @@ import Big from 'big.js'; describe('vault-controller#V4', () => { const latestBlockHeight: string = '25'; - const currentBlockHeight: string = '7'; - const twoHourBlockHeight: string = '5'; + const currentBlockHeight: string = '9'; + const twoHourBlockHeight: string = '7'; + const almostTwoDayBlockHeight: string = '5'; const twoDayBlockHeight: string = '3'; const currentTime: DateTime = DateTime.utc().startOf('day').minus({ hour: 5 }); const latestTime: DateTime = currentTime.plus({ second: 5 }); const twoHoursAgo: DateTime = currentTime.minus({ hour: 2 }); const twoDaysAgo: DateTime = currentTime.minus({ day: 2 }); + const almostTwoDaysAgo: DateTime = currentTime.minus({ hour: 47 }); const initialFundingIndex: string = '10000'; const vault1Equity: number = 159500; const vault2Equity: number = 10000; @@ -70,6 +72,11 @@ describe('vault-controller#V4', () => { time: latestTime.toISO(), blockHeight: latestBlockHeight, }), + BlockTable.create({ + ...testConstants.defaultBlock, + time: almostTwoDaysAgo.toISO(), + blockHeight: almostTwoDayBlockHeight, + }), ]); await SubaccountTable.create(testConstants.vaultSubaccount); await SubaccountTable.create({ @@ -152,14 +159,27 @@ describe('vault-controller#V4', () => { }); it.each([ - ['no resolution', '', [1, 2]], - ['daily resolution', '?resolution=day', [1, 2]], - ['hourly resolution', '?resolution=hour', [1, 2, 3]], + ['no resolution', '', [1, 2], [undefined, 6], [9, 10]], + ['daily resolution', '?resolution=day', [1, 2], [undefined, 6], [9, 10]], + [ + 'hourly resolution', + '?resolution=hour', + [1, undefined, 2, 3], + [undefined, 5, 6, 7], + [9, undefined, 10, 11], + ], ])('Get /megavault/historicalPnl with 2 vault subaccounts and main subaccount (%s)', async ( _name: string, queryParam: string, - expectedTicksIndex: number[], + expectedTicksIndex1: (number | undefined)[], + expectedTicksIndex2: (number | undefined)[], + expectedTicksIndexMain: (number | undefined)[], ) => { + const expectedTicksArray: (number | undefined)[][] = [ + expectedTicksIndex1, + expectedTicksIndex2, + expectedTicksIndexMain, + ]; await Promise.all([ VaultTable.create({ ...testConstants.defaultVault, @@ -198,15 +218,33 @@ describe('vault-controller#V4', () => { createdAt: latestTime.toISO(), }; - expect(response.body.megavaultPnl).toHaveLength(expectedTicksIndex.length + 1); + expect(response.body.megavaultPnl).toHaveLength(expectedTicksIndex1.length + 1); expect(response.body.megavaultPnl).toEqual( expect.arrayContaining( - expectedTicksIndex.map((index: number) => { + expectedTicksIndex1.map((_: number | undefined, pos: number) => { + const pnlTickBase: any = { + equity: '0', + totalPnl: '0', + netTransfers: '0', + }; + let expectedTick: PnlTicksFromDatabase; + for (const expectedTicks of expectedTicksArray) { + if (expectedTicks[pos] !== undefined) { + expectedTick = createdPnlTicks[expectedTicks[pos]!]; + pnlTickBase.equity = Big(pnlTickBase.equity).add(expectedTick.equity).toFixed(); + pnlTickBase.totalPnl = Big(pnlTickBase.totalPnl) + .add(expectedTick.totalPnl) + .toFixed(); + pnlTickBase.netTransfers = Big(pnlTickBase.netTransfers) + .add(expectedTick.netTransfers) + .toFixed(); + } + } return expect.objectContaining({ - ...expectedPnlTickBase, - createdAt: createdPnlTicks[index].createdAt, - blockHeight: createdPnlTicks[index].blockHeight, - blockTime: createdPnlTicks[index].blockTime, + ...pnlTickBase, + createdAt: expectedTick!.createdAt, + blockHeight: expectedTick!.blockHeight, + blockTime: expectedTick!.blockTime, }); }).concat([expect.objectContaining(finalTick)]), ), @@ -494,9 +532,9 @@ describe('vault-controller#V4', () => { PnlTicksTable.create({ ...testConstants.defaultPnlTick, subaccountId: testConstants.vaultSubaccountId, - blockTime: twoDaysAgo.toISO(), - createdAt: twoDaysAgo.toISO(), - blockHeight: twoDayBlockHeight, + blockTime: almostTwoDaysAgo.toISO(), + createdAt: almostTwoDaysAgo.toISO(), + blockHeight: almostTwoDayBlockHeight, }), PnlTicksTable.create({ ...testConstants.defaultPnlTick, diff --git a/indexer/services/comlink/src/config.ts b/indexer/services/comlink/src/config.ts index 43d2c81222..1b70d73b4b 100644 --- a/indexer/services/comlink/src/config.ts +++ b/indexer/services/comlink/src/config.ts @@ -61,6 +61,7 @@ export const configSchema = { // Vaults config VAULT_PNL_HISTORY_DAYS: parseInteger({ default: 90 }), + VAULT_PNL_HISTORY_HOURS: parseInteger({ default: 72 }), }; //////////////////////////////////////////////////////////////////////////////// diff --git a/indexer/services/comlink/src/controllers/api/v4/vault-controller.ts b/indexer/services/comlink/src/controllers/api/v4/vault-controller.ts index 6b967f3872..ee4a31fe64 100644 --- a/indexer/services/comlink/src/controllers/api/v4/vault-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/vault-controller.ts @@ -30,6 +30,7 @@ import Big from 'big.js'; import express from 'express'; import { checkSchema, matchedData } from 'express-validator'; import _ from 'lodash'; +import { DateTime } from 'luxon'; import { Controller, Get, Query, Route, } from 'tsoa'; @@ -85,7 +86,7 @@ class VaultController extends Controller { BlockFromDatabase, string, ] = await Promise.all([ - getVaultSubaccountPnlTicks(vaultSubaccountIdsWithMainSubaccount, resolution), + getVaultSubaccountPnlTicks(vaultSubaccountIdsWithMainSubaccount, getResolution(resolution)), getVaultPositions(vaultSubaccounts), BlockTable.getLatest(), getMainSubaccountEquity(), @@ -102,7 +103,7 @@ class VaultController extends Controller { }, mainSubaccountEquity); const pnlTicksWithCurrentTick: PnlTicksFromDatabase[] = getPnlTicksWithCurrentTick( currentEquity, - Array.from(aggregatedPnlTicks.values()), + filterOutIntervalTicks(aggregatedPnlTicks, getResolution(resolution)), latestBlock, ); @@ -128,7 +129,7 @@ class VaultController extends Controller { Map, BlockFromDatabase, ] = await Promise.all([ - getVaultSubaccountPnlTicks(_.keys(vaultSubaccounts), resolution), + getVaultSubaccountPnlTicks(_.keys(vaultSubaccounts), getResolution(resolution)), getVaultPositions(vaultSubaccounts), BlockTable.getLatest(), ]); @@ -294,21 +295,22 @@ router.get( async function getVaultSubaccountPnlTicks( vaultSubaccountIds: string[], - resolution?: PnlTickInterval, + resolution: PnlTickInterval, ): Promise { if (vaultSubaccountIds.length === 0) { return []; } - let pnlTickInterval: PnlTickInterval; - if (resolution === undefined) { - pnlTickInterval = PnlTickInterval.day; + + let windowSeconds: number; + if (resolution === PnlTickInterval.day) { + windowSeconds = config.VAULT_PNL_HISTORY_DAYS * 24 * 60 * 60; // days to seconds } else { - pnlTickInterval = resolution; + windowSeconds = config.VAULT_PNL_HISTORY_HOURS * 60 * 60; // hours to seconds } const pnlTicks: PnlTicksFromDatabase[] = await PnlTicksTable.getPnlTicksAtIntervals( - pnlTickInterval, - config.VAULT_PNL_HISTORY_DAYS * 24 * 60 * 60, + resolution, + windowSeconds, vaultSubaccountIds, ); @@ -461,7 +463,7 @@ function getPnlTicksWithCurrentTick( return []; } const currentTick: PnlTicksFromDatabase = { - ...pnlTicks[pnlTicks.length - 1], + ...(_.maxBy(pnlTicks, 'blockTime')!), equity, blockHeight: latestBlock.blockHeight, blockTime: latestBlock.time, @@ -470,6 +472,57 @@ function getPnlTicksWithCurrentTick( return pnlTicks.concat([currentTick]); } +/** + * Takes in a map of block heights to PnlTicks and filters out the closest pnl tick per interval. + * @param pnlTicksByBlock Map of block number to pnl tick. + * @param resolution Resolution of interval. + * @returns Array of PnlTicksFromDatabase, one per interval. + */ +function filterOutIntervalTicks( + pnlTicksByBlock: Map, + resolution: PnlTickInterval, +): PnlTicksFromDatabase[] { + // Track block to block time. + const blockToBlockTime: Map = new Map(); + // Track start of days to closest block by block time. + const blocksPerInterval: Map = new Map(); + // Track start of days to closest Pnl tick. + const ticksPerInterval: Map = new Map(); + pnlTicksByBlock.forEach((pnlTick: PnlTicksFromDatabase, block: number): void => { + const blockTime: DateTime = DateTime.fromISO(pnlTick.blockTime).toUTC(); + blockToBlockTime.set(block, blockTime); + + const startOfInterval: DateTime = blockTime.toUTC().startOf(resolution); + const startOfIntervalStr: string = startOfInterval.toISO(); + const startOfIntervalBlock: number | undefined = blocksPerInterval.get(startOfIntervalStr); + // No block for the start of interval, set this block as the block for the interval. + if (startOfIntervalBlock === undefined) { + blocksPerInterval.set(startOfIntervalStr, block); + ticksPerInterval.set(startOfIntervalStr, pnlTick); + return; + } + + const startOfDayBlockTime: DateTime | undefined = blockToBlockTime.get(startOfIntervalBlock); + // Invalid block set as start of day block, set this block as the block for the day. + if (startOfDayBlockTime === undefined) { + blocksPerInterval.set(startOfIntervalStr, block); + ticksPerInterval.set(startOfIntervalStr, pnlTick); + return; + } + + // This block is closer to the start of the day, set it as the block for the day. + if (blockTime.diff(startOfInterval) < startOfDayBlockTime.diff(startOfInterval)) { + blocksPerInterval.set(startOfIntervalStr, block); + ticksPerInterval.set(startOfIntervalStr, pnlTick); + } + }); + return Array.from(ticksPerInterval.values()); +} + +function getResolution(resolution: PnlTickInterval = PnlTickInterval.day): PnlTickInterval { + return resolution; +} + async function getVaultMapping(): Promise { const vaults: VaultFromDatabase[] = await VaultTable.findAll( {},