Skip to content

Commit

Permalink
Allow configuring day vault PnL starts. (#2570)
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentwschau authored Nov 14, 2024
1 parent d1c64f6 commit 4633b85
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 20 deletions.
32 changes: 28 additions & 4 deletions indexer/packages/postgres/__tests__/stores/pnl-ticks-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,22 +460,19 @@ describe('PnlTicks store', () => {
interval,
7 * 24 * 60 * 60, // 1 week
[defaultSubaccountId, defaultSubaccountIdWithAlternateAddress],
DateTime.fromISO(createdTicks[8].blockTime).plus({ seconds: 1 }),
);
// See setup function for created ticks.
// Should exclude tick that is within the same hour except the first.
const expectedHourlyTicks: PnlTicksFromDatabase[] = [
createdTicks[8],
createdTicks[7],
createdTicks[5],
createdTicks[3],
createdTicks[2],
createdTicks[0],
];
// Should exclude ticks that is within the same day except for the first.
const expectedDailyTicks: PnlTicksFromDatabase[] = [
createdTicks[8],
createdTicks[7],
createdTicks[3],
createdTicks[2],
];

Expand All @@ -486,6 +483,33 @@ describe('PnlTicks store', () => {
}
});

it('Gets latest pnl ticks for subaccounts before or at given date', async () => {
const createdTicks: PnlTicksFromDatabase[] = await setupIntervalPnlTicks();
const latestTicks: PnlTicksFromDatabase[] = await PnlTicksTable.getLatestPnlTick(
[defaultSubaccountId, defaultSubaccountIdWithAlternateAddress],
DateTime.fromISO(createdTicks[8].blockTime).plus({ seconds: 1 }),
);
expect(latestTicks).toEqual([createdTicks[8], createdTicks[3]]);
});

it('Gets empty pnl ticks for subaccounts before or at date earlier than all pnl data', async () => {
const createdTicks: PnlTicksFromDatabase[] = await setupIntervalPnlTicks();
const latestTicks: PnlTicksFromDatabase[] = await PnlTicksTable.getLatestPnlTick(
[defaultSubaccountId, defaultSubaccountIdWithAlternateAddress],
DateTime.fromISO(createdTicks[0].blockTime).minus({ years: 1 }),
);
expect(latestTicks).toEqual([]);
});

it('Gets empty pnl ticks for subaccounts before or at date if no subaccounts given', async () => {
const createdTicks: PnlTicksFromDatabase[] = await setupIntervalPnlTicks();
const latestTicks: PnlTicksFromDatabase[] = await PnlTicksTable.getLatestPnlTick(
[],
DateTime.fromISO(createdTicks[0].blockTime).plus({ years: 1 }),
);
expect(latestTicks).toEqual([]);
});

});

async function setupRankedPnlTicksData() {
Expand Down
43 changes: 43 additions & 0 deletions indexer/packages/postgres/src/stores/pnl-ticks-table.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'lodash';
import { DateTime } from 'luxon';
import { QueryBuilder } from 'objection';

import {
Expand Down Expand Up @@ -465,7 +466,11 @@ export async function getPnlTicksAtIntervals(
interval: PnlTickInterval,
timeWindowSeconds: number,
subaccountIds: string[],
earliestDate: DateTime,
): Promise <PnlTicksFromDatabase[]> {
if (subaccountIds.length === 0) {
return [];
}
const result: {
rows: PnlTicksFromDatabase[],
} = await knexReadReplica.getConnection().raw(
Expand Down Expand Up @@ -493,6 +498,7 @@ export async function getPnlTicksAtIntervals(
FROM pnl_ticks
WHERE
"subaccountId" IN (${subaccountIds.map((id: string) => { return `'${id}'`; }).join(',')}) AND
"blockTime" >= '${earliestDate.toUTC().toISO()}'::timestamp AND
"blockTime" > NOW() - INTERVAL '${timeWindowSeconds} second'
) AS pnl_intervals
WHERE
Expand All @@ -505,3 +511,40 @@ export async function getPnlTicksAtIntervals(

return result.rows;
}

export async function getLatestPnlTick(
subaccountIds: string[],
beforeOrAt: DateTime,
): Promise <PnlTicksFromDatabase[]> {
if (subaccountIds.length === 0) {
return [];
}
const result: {
rows: PnlTicksFromDatabase[],
} = await knexReadReplica.getConnection().raw(
`
SELECT
DISTINCT ON ("subaccountId")
"id",
"subaccountId",
"equity",
"totalPnl",
"netTransfers",
"createdAt",
"blockHeight",
"blockTime"
FROM
pnl_ticks
WHERE
"subaccountId" in (${subaccountIds.map((id: string) => { return `'${id}'`; }).join(',')}) AND
"blockTime" <= '${beforeOrAt.toUTC().toISO()}'::timestamp
ORDER BY
"subaccountId",
"blockTime" DESC
`,
) as unknown as {
rows: PnlTicksFromDatabase[],
};

return result.rows;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('vault-controller#V4', () => {
const mainVaultEquity: number = 10000;
const vaultPnlHistoryHoursPrev: number = config.VAULT_PNL_HISTORY_HOURS;
const vaultPnlLastPnlWindowPrev: number = config.VAULT_LATEST_PNL_TICK_WINDOW_HOURS;
const vaultPnlStartDatePrev: string = config.VAULT_PNL_START_DATE;

beforeAll(async () => {
await dbHelpers.migrate();
Expand All @@ -58,6 +59,8 @@ describe('vault-controller#V4', () => {
config.VAULT_PNL_HISTORY_HOURS = 168;
// Use last 48 hours to get latest pnl tick for tests.
config.VAULT_LATEST_PNL_TICK_WINDOW_HOURS = 48;
// Use a time before all pnl ticks as the pnl start date.
config.VAULT_PNL_START_DATE = '2020-01-01T00:00:00Z';
await testMocks.seedData();
await perpetualMarketRefresher.updatePerpetualMarkets();
await liquidityTierRefresher.updateLiquidityTiers();
Expand Down Expand Up @@ -126,6 +129,7 @@ describe('vault-controller#V4', () => {
await dbHelpers.clearData();
config.VAULT_PNL_HISTORY_HOURS = vaultPnlHistoryHoursPrev;
config.VAULT_LATEST_PNL_TICK_WINDOW_HOURS = vaultPnlLastPnlWindowPrev;
config.VAULT_PNL_START_DATE = vaultPnlStartDatePrev;
});

it('Get /megavault/historicalPnl with no vault subaccounts', async () => {
Expand All @@ -138,21 +142,32 @@ describe('vault-controller#V4', () => {
});

it.each([
['no resolution', '', [1, 2], 4],
['daily resolution', '?resolution=day', [1, 2], 4],
['hourly resolution', '?resolution=hour', [1, 2, 3, 4], 4],
['no resolution', '', [1, 2], 4, undefined],
['daily resolution', '?resolution=day', [1, 2], 4, undefined],
['hourly resolution', '?resolution=hour', [1, 2, 3, 4], 4, undefined],
['start date adjust PnL', '?resolution=hour', [1, 2, 3, 4], 4, twoDaysAgo.toISO()],
])('Get /megavault/historicalPnl with single vault subaccount (%s)', async (
_name: string,
queryParam: string,
expectedTicksIndex: number[],
finalTickIndex: number,
startDate: string | undefined,
) => {
if (startDate !== undefined) {
config.VAULT_PNL_START_DATE = startDate;
}
await VaultTable.create({
...testConstants.defaultVault,
address: testConstants.defaultSubaccount.address,
clobPairId: testConstants.defaultPerpetualMarket.clobPairId,
});
const createdPnlTicks: PnlTicksFromDatabase[] = await createPnlTicks();
// Adjust PnL by total pnl of start date
if (startDate !== undefined) {
for (const createdPnlTick of createdPnlTicks) {
createdPnlTick.totalPnl = Big(createdPnlTick.totalPnl).sub('10000').toFixed();
}
}
const finalTick: PnlTicksFromDatabase = {
...createdPnlTicks[finalTickIndex],
equity: Big(vault1Equity).toFixed(),
Expand Down
2 changes: 2 additions & 0 deletions indexer/services/comlink/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ export const configSchema = {
// Vaults config
VAULT_PNL_HISTORY_DAYS: parseInteger({ default: 90 }),
VAULT_PNL_HISTORY_HOURS: parseInteger({ default: 72 }),
VAULT_PNL_START_DATE: parseString({ default: '2024-01-01T00:00:00Z' }),
VAULT_LATEST_PNL_TICK_WINDOW_HOURS: parseInteger({ default: 1 }),
VAULT_FETCH_FUNDING_INDEX_BLOCK_WINDOWS: parseInteger({ default: 250_000 }),

};

////////////////////////////////////////////////////////////////////////////////
Expand Down
87 changes: 74 additions & 13 deletions indexer/services/comlink/src/controllers/api/v4/vault-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class VaultController extends Controller {
BlockFromDatabase,
string,
PnlTicksFromDatabase | undefined,
DateTime | undefined
DateTime | undefined,
] = await Promise.all([
getVaultSubaccountPnlTicks(vaultSubaccountIdsWithMainSubaccount, getResolution(resolution)),
getVaultPositions(vaultSubaccounts),
Expand All @@ -114,7 +114,6 @@ class VaultController extends Controller {
`${config.SERVICE_NAME}.${controllerName}.fetch_ticks_positions_equity.timing`,
Date.now() - startTicksPositions,
);

// aggregate pnlTicks for all vault subaccounts grouped by blockHeight
const aggregatedPnlTicks: PnlTicksFromDatabase[] = aggregateVaultPnlTicks(
vaultPnlTicks,
Expand Down Expand Up @@ -337,13 +336,28 @@ async function getVaultSubaccountPnlTicks(
windowSeconds = config.VAULT_PNL_HISTORY_HOURS * 60 * 60; // hours to seconds
}

const pnlTicks: PnlTicksFromDatabase[] = await PnlTicksTable.getPnlTicksAtIntervals(
resolution,
windowSeconds,
vaultSubaccountIds,
);
const [
pnlTicks,
adjustByPnlTicks,
] : [
PnlTicksFromDatabase[],
PnlTicksFromDatabase[],
] = await Promise.all([
PnlTicksTable.getPnlTicksAtIntervals(
resolution,
windowSeconds,
vaultSubaccountIds,
getVautlPnlStartDate(),
),
PnlTicksTable.getLatestPnlTick(
vaultSubaccountIds,
// Add a buffer of 10 minutes to get the first PnL tick for PnL data as PnL ticks aren't
// created exactly on the hour.
getVautlPnlStartDate().plus({ minutes: 10 }),
),
]);

return pnlTicks;
return adjustVaultPnlTicks(pnlTicks, adjustByPnlTicks);
}

async function getVaultPositions(
Expand Down Expand Up @@ -538,14 +552,33 @@ export async function getLatestPnlTick(
vaultSubaccountIds: string[],
vaults: VaultFromDatabase[],
): Promise<PnlTicksFromDatabase | undefined> {
const pnlTicks: PnlTicksFromDatabase[] = await PnlTicksTable.getPnlTicksAtIntervals(
PnlTickInterval.hour,
config.VAULT_LATEST_PNL_TICK_WINDOW_HOURS * 60 * 60,
vaultSubaccountIds,
const [
pnlTicks,
adjustByPnlTicks,
] : [
PnlTicksFromDatabase[],
PnlTicksFromDatabase[],
] = await Promise.all([
PnlTicksTable.getPnlTicksAtIntervals(
PnlTickInterval.hour,
config.VAULT_LATEST_PNL_TICK_WINDOW_HOURS * 60 * 60,
vaultSubaccountIds,
getVautlPnlStartDate(),
),
PnlTicksTable.getLatestPnlTick(
vaultSubaccountIds,
// Add a buffer of 10 minutes to get the first PnL tick for PnL data as PnL ticks aren't
// created exactly on the hour.
getVautlPnlStartDate().plus({ minutes: 10 }),
),
]);
const adjustedPnlTicks: PnlTicksFromDatabase[] = adjustVaultPnlTicks(
pnlTicks,
adjustByPnlTicks,
);
// Aggregate and get pnl tick closest to the hour
const aggregatedTicks: PnlTicksFromDatabase[] = aggregateVaultPnlTicks(
pnlTicks,
adjustedPnlTicks,
vaults,
);
const filteredTicks: PnlTicksFromDatabase[] = filterOutIntervalTicks(
Expand Down Expand Up @@ -708,6 +741,29 @@ function aggregateVaultPnlTicks(
}).map((aggregatedPnlTick: AggregatedPnlTick) => { return aggregatedPnlTick.pnlTick; });
}

function adjustVaultPnlTicks(
pnlTicks: PnlTicksFromDatabase[],
pnlTicksToAdjustBy: PnlTicksFromDatabase[],
): PnlTicksFromDatabase[] {
const subaccountToPnlTick: {[subaccountId: string]: PnlTicksFromDatabase} = {};
for (const pnlTickToAdjustBy of pnlTicksToAdjustBy) {
subaccountToPnlTick[pnlTickToAdjustBy.subaccountId] = pnlTickToAdjustBy;
}

return pnlTicks.map((pnlTick: PnlTicksFromDatabase): PnlTicksFromDatabase => {
const adjustByPnlTick: PnlTicksFromDatabase | undefined = subaccountToPnlTick[
pnlTick.subaccountId
];
if (adjustByPnlTick === undefined) {
return pnlTick;
}
return {
...pnlTick,
totalPnl: Big(pnlTick.totalPnl).sub(Big(adjustByPnlTick.totalPnl)).toFixed(),
};
});
}

async function getVaultMapping(): Promise<VaultMapping> {
const vaults: VaultFromDatabase[] = await VaultTable.findAll(
{},
Expand Down Expand Up @@ -740,4 +796,9 @@ async function getVaultMapping(): Promise<VaultMapping> {
return validVaultMapping;
}

function getVautlPnlStartDate(): DateTime {
const startDate: DateTime = DateTime.fromISO(config.VAULT_PNL_START_DATE).toUTC();
return startDate;
}

export default router;

0 comments on commit 4633b85

Please sign in to comment.