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

[TRA-568] Initial vault PnL endpoints. #2121

Merged
merged 10 commits into from
Aug 22, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
defaultSubaccountIdWithAlternateAddress,
defaultSubaccountWithAlternateAddress,
defaultWallet2,
vaultSubaccount,
vaultSubaccountId,
vaultWallet,
} from '../helpers/constants';
Expand Down Expand Up @@ -421,7 +420,6 @@ describe('PnlTicks store', () => {
await setupRankedPnlTicksData();

await WalletTable.create(vaultWallet);
await SubaccountTable.create(vaultSubaccount);
await PnlTicksTable.create({
subaccountId: vaultSubaccountId,
equity: '100',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import {
dbHelpers,
testConstants,
testMocks,
PnlTicksCreateObject,
PnlTicksTable,
perpetualMarketRefresher,
BlockTable,
liquidityTierRefresher,
SubaccountTable,
} from '@dydxprotocol-indexer/postgres';
import { PnlTicksResponseObject, RequestMethod, VaultHistoricalPnl } from '../../../../src/types';
import request from 'supertest';
import { sendRequest } from '../../../helpers/helpers';
import config from '../../../../src/config';

describe('vault-controller#V4', () => {
const experimentVaultsPrevVal: string = config.EXPERIMENT_VAULTS;
const experimentVaultMarketsPrevVal: string = config.EXPERIMENT_VAULT_MARKETS;
const blockHeight: string = '3';

beforeAll(async () => {
await dbHelpers.migrate();
});

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

describe('GET /v1', () => {
beforeEach(async () => {
config.EXPERIMENT_VAULTS = testConstants.defaultPnlTick.subaccountId;
config.EXPERIMENT_VAULT_MARKETS = testConstants.defaultPerpetualMarket.clobPairId;
await testMocks.seedData();
await perpetualMarketRefresher.updatePerpetualMarkets();
await liquidityTierRefresher.updateLiquidityTiers();
await BlockTable.create({
...testConstants.defaultBlock,
blockHeight,
});
await SubaccountTable.create(testConstants.vaultSubaccount);
});

afterEach(async () => {
config.EXPERIMENT_VAULTS = experimentVaultsPrevVal;
config.EXPERIMENT_VAULT_MARKETS = experimentVaultMarketsPrevVal;
await dbHelpers.clearData();
});

it('Get /megavault/historicalPnl with single vault subaccount', async () => {
const createdAt: string = '2000-05-25T00:00:00.000Z';
const pnlTick2: PnlTicksCreateObject = {
...testConstants.defaultPnlTick,
createdAt,
blockHeight,
};
await Promise.all([
PnlTicksTable.create(testConstants.defaultPnlTick),
PnlTicksTable.create(pnlTick2),
]);

const response: request.Response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/vault/v1/megavault/historicalPnl',
});

const expectedPnlTickResponse: PnlTicksResponseObject = {
...testConstants.defaultPnlTick,
id: PnlTicksTable.uuid(
testConstants.defaultPnlTick.subaccountId,
testConstants.defaultPnlTick.createdAt,
),
};

const expectedPnlTick2Response: any = {
...testConstants.defaultPnlTick,
createdAt,
blockHeight,
id: PnlTicksTable.uuid(
testConstants.defaultPnlTick.subaccountId,
createdAt,
),
};

expect(response.body.megavaultPnl).toEqual(
expect.arrayContaining([
expect.objectContaining({
...expectedPnlTick2Response,
}),
expect.objectContaining({
...expectedPnlTickResponse,
}),
]),
);
});

it('Get /megavault/historicalPnl with 2 vault subaccounts', async () => {
config.EXPERIMENT_VAULTS = [
testConstants.defaultPnlTick.subaccountId,
testConstants.vaultSubaccountId,
].join(',');
config.EXPERIMENT_VAULT_MARKETS = [
testConstants.defaultPerpetualMarket.clobPairId,
testConstants.defaultPerpetualMarket2.clobPairId,
].join(',');

const pnlTick2: PnlTicksCreateObject = {
...testConstants.defaultPnlTick,
subaccountId: testConstants.vaultSubaccountId,
};
await Promise.all([
PnlTicksTable.create(testConstants.defaultPnlTick),
PnlTicksTable.create(pnlTick2),
]);

const response: request.Response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/vault/v1/megavault/historicalPnl',
});

const expectedPnlTickResponse: any = {
// id and subaccountId don't matter
equity: (parseFloat(testConstants.defaultPnlTick.equity) +
parseFloat(pnlTick2.equity)).toString(),
totalPnl: (parseFloat(testConstants.defaultPnlTick.totalPnl) +
parseFloat(pnlTick2.totalPnl)).toString(),
netTransfers: (parseFloat(testConstants.defaultPnlTick.netTransfers) +
parseFloat(pnlTick2.netTransfers)).toString(),
createdAt: testConstants.defaultPnlTick.createdAt,
blockHeight: testConstants.defaultPnlTick.blockHeight,
blockTime: testConstants.defaultPnlTick.blockTime,
};

expect(response.body.megavaultPnl).toEqual(
expect.arrayContaining([
expect.objectContaining({
...expectedPnlTickResponse,
}),
]),
);
});

it('Get /vaults/historicalPnl with single vault subaccount', async () => {
const createdAt: string = '2000-05-25T00:00:00.000Z';
const pnlTick2: PnlTicksCreateObject = {
...testConstants.defaultPnlTick,
createdAt,
blockHeight,
};
await Promise.all([
PnlTicksTable.create(testConstants.defaultPnlTick),
PnlTicksTable.create(pnlTick2),
]);

const response: request.Response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/vault/v1/vaults/historicalPnl',
});

const expectedPnlTickResponse: PnlTicksResponseObject = {
...testConstants.defaultPnlTick,
id: PnlTicksTable.uuid(
testConstants.defaultPnlTick.subaccountId,
testConstants.defaultPnlTick.createdAt,
),
};

const expectedPnlTick2Response: any = {
...testConstants.defaultPnlTick,
createdAt,
blockHeight,
id: PnlTicksTable.uuid(
testConstants.defaultPnlTick.subaccountId,
createdAt,
),
};

expect(response.body.vaultsPnl).toHaveLength(1);

expect(response.body.vaultsPnl[0]).toEqual({
ticker: testConstants.defaultPerpetualMarket.ticker,
historicalPnl: expect.arrayContaining([
expect.objectContaining({
...expectedPnlTick2Response,
}),
expect.objectContaining({
...expectedPnlTickResponse,
}),
]),
});
});

it('Get /vault/v1/vaults/historicalPnl with 2 vault subaccounts', async () => {
config.EXPERIMENT_VAULTS = [
testConstants.defaultPnlTick.subaccountId,
testConstants.vaultSubaccountId,
].join(',');
config.EXPERIMENT_VAULT_MARKETS = [
testConstants.defaultPerpetualMarket.clobPairId,
testConstants.defaultPerpetualMarket2.clobPairId,
].join(',');

const pnlTick2: PnlTicksCreateObject = {
...testConstants.defaultPnlTick,
subaccountId: testConstants.vaultSubaccountId,
};
await Promise.all([
PnlTicksTable.create(testConstants.defaultPnlTick),
PnlTicksTable.create(pnlTick2),
]);

const response: request.Response = await sendRequest({
type: RequestMethod.GET,
path: '/v4/vault/v1/vaults/historicalPnl',
});

const expectedVaultPnl: VaultHistoricalPnl = {
ticker: testConstants.defaultPerpetualMarket.ticker,
historicalPnl: [
{
...testConstants.defaultPnlTick,
id: PnlTicksTable.uuid(
testConstants.defaultPnlTick.subaccountId,
testConstants.defaultPnlTick.createdAt,
),
},
],
};

const expectedVaultPnl2: VaultHistoricalPnl = {
ticker: testConstants.defaultPerpetualMarket2.ticker,
historicalPnl: [
{
...pnlTick2,
id: PnlTicksTable.uuid(
pnlTick2.subaccountId,
pnlTick2.createdAt,
),
},
],
};

expect(response.body.vaultsPnl).toEqual(
expect.arrayContaining([
expect.objectContaining({
...expectedVaultPnl,
}),
expect.objectContaining({
...expectedVaultPnl2,
}),
]),
);
});
});
});
70 changes: 70 additions & 0 deletions indexer/services/comlink/__tests__/lib/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
LiquidityTiersFromDatabase,
LiquidityTiersTable,
liquidityTierRefresher,
PnlTicksFromDatabase,
PnlTicksTable,
} from '@dydxprotocol-indexer/postgres';
import {
adjustUSDCAssetPosition,
Expand All @@ -41,6 +43,7 @@ import {
getPerpetualPositionsWithUpdatedFunding,
initializePerpetualPositionsWithFunding,
getChildSubaccountNums,
aggregatePnlTicks,
} from '../../src/lib/helpers';
import _ from 'lodash';
import Big from 'big.js';
Expand Down Expand Up @@ -721,4 +724,71 @@ describe('helpers', () => {
expect(() => getChildSubaccountNums(128)).toThrowError('Parent subaccount number must be less than 128');
});
});

describe('aggregatePnlTicks', () => {
it('aggregates single pnl tick', () => {
const pnlTick: PnlTicksFromDatabase = {
...testConstants.defaultPnlTick,
id: PnlTicksTable.uuid(
testConstants.defaultPnlTick.subaccountId,
testConstants.defaultPnlTick.createdAt,
),
};

const aggregatedPnlTicks: Map<number, PnlTicksFromDatabase> = aggregatePnlTicks([pnlTick]);
expect(
aggregatedPnlTicks.get(parseInt(pnlTick.blockHeight, 10)),
).toEqual(expect.objectContaining({ ...testConstants.defaultPnlTick }));
});

it('aggregates multiple pnl ticks same height', () => {
const pnlTick: PnlTicksFromDatabase = {
...testConstants.defaultPnlTick,
id: PnlTicksTable.uuid(
testConstants.defaultPnlTick.subaccountId,
testConstants.defaultPnlTick.createdAt,
),
};
const pnlTick2: PnlTicksFromDatabase = {
...testConstants.defaultPnlTick,
id: PnlTicksTable.uuid(
testConstants.defaultSubaccountId2,
testConstants.defaultPnlTick.createdAt,
),
};
const blockHeight2: string = '80';
const pnlTick3: PnlTicksFromDatabase = {
...testConstants.defaultPnlTick,
id: PnlTicksTable.uuid(
testConstants.defaultPnlTick.subaccountId,
testConstants.defaultPnlTick.createdAt,
),
blockHeight: blockHeight2,
};

const aggregatedPnlTicks: Map<number, PnlTicksFromDatabase> = aggregatePnlTicks(
[pnlTick, pnlTick2, pnlTick3],
);
// Combined pnl tick at initial block height.
expect(
aggregatedPnlTicks.get(parseInt(pnlTick.blockHeight, 10)),
).toEqual(expect.objectContaining({
equity: (parseFloat(testConstants.defaultPnlTick.equity) +
parseFloat(pnlTick2.equity)).toString(),
totalPnl: (parseFloat(testConstants.defaultPnlTick.totalPnl) +
parseFloat(pnlTick2.totalPnl)).toString(),
netTransfers: (parseFloat(testConstants.defaultPnlTick.netTransfers) +
parseFloat(pnlTick2.netTransfers)).toString(),
createdAt: testConstants.defaultPnlTick.createdAt,
blockHeight: testConstants.defaultPnlTick.blockHeight,
blockTime: testConstants.defaultPnlTick.blockTime,
}));
// Single pnl tick at second block height.
expect(
aggregatedPnlTicks.get(parseInt(blockHeight2, 10)),
).toEqual(expect.objectContaining({
...pnlTick3,
}));
});
});
});
Loading
Loading