Skip to content

Commit

Permalink
Add API for parent subaccount perpetual positions
Browse files Browse the repository at this point in the history
Signed-off-by: Shrenuj Bansal <shrenuj@dydx.exchange>
  • Loading branch information
shrenujb committed Mar 29, 2024
1 parent 3011245 commit 817700b
Show file tree
Hide file tree
Showing 6 changed files with 633 additions and 9 deletions.
4 changes: 2 additions & 2 deletions indexer/packages/postgres/__tests__/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export const isolatedPerpetualPosition: PerpetualPositionCreateObject = {
status: PerpetualPositionStatus.OPEN,
size: '10',
maxSize: '25',
entryPrice: '20000',
entryPrice: '1.5',
sumOpen: '10',
sumClose: '0',
createdAt: createdDateTime.toISO(),
Expand Down Expand Up @@ -637,7 +637,7 @@ export const isolatedMarket: MarketCreateObject = {
pair: 'ISO-USD',
exponent: -12,
minPriceChangePpm: 50,
oraclePrice: '0.000000075',
oraclePrice: '1.00',
};

export const isolatedMarket2: MarketCreateObject = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
BlockTable,
dbHelpers,
testMocks,
testConstants,
FundingIndexUpdatesTable,
perpetualMarketRefresher,
PerpetualPositionStatus,
PerpetualPositionTable,
PositionSide,
BlockTable,
FundingIndexUpdatesTable,
PerpetualPositionStatus,
testConstants,
testMocks,
} from '@dydxprotocol-indexer/postgres';
import { PerpetualPositionResponseObject, RequestMethod } from '../../../../src/types';
import request from 'supertest';
Expand Down Expand Up @@ -245,5 +245,241 @@ describe('perpetual-positions-controller#V4', () => {
]),
}));
});

it('Get /perpetualPositions/parentSubaccountNumber gets long/short positions across subaccounts', async () => {
await Promise.all([
PerpetualPositionTable.create(testConstants.defaultPerpetualPosition),
PerpetualPositionTable.create({
...testConstants.isolatedPerpetualPosition,
side: PositionSide.SHORT,
size: '-10',
}),
]);
await Promise.all([
FundingIndexUpdatesTable.create({
...testConstants.isolatedMarketFundingIndexUpdate,
fundingIndex: '10000',
effectiveAtHeight: testConstants.createdHeight,
}),
FundingIndexUpdatesTable.create({
...testConstants.isolatedMarketFundingIndexUpdate,
eventId: testConstants.defaultTendermintEventId2,
effectiveAtHeight: latestHeight,
}),
]);

const parentSubaccountNumber: number = 0;
const response: request.Response = await sendRequest({
type: RequestMethod.GET,
path: `/v4/perpetualPositions/parentSubaccountNumber?address=${testConstants.defaultAddress}` +
`&parentSubaccountNumber=${parentSubaccountNumber}`,
});

const expected: PerpetualPositionResponseObject = {
market: testConstants.defaultPerpetualMarket.ticker,
side: testConstants.defaultPerpetualPosition.side,
status: testConstants.defaultPerpetualPosition.status,
size: testConstants.defaultPerpetualPosition.size,
maxSize: testConstants.defaultPerpetualPosition.maxSize,
entryPrice: getFixedRepresentation(testConstants.defaultPerpetualPosition.entryPrice!),
exitPrice: null,
sumOpen: testConstants.defaultPerpetualPosition.sumOpen!,
sumClose: testConstants.defaultPerpetualPosition.sumClose!,
// For the calculation of the net funding (long position):
// settled funding on position = 200_000, size = 10, latest funding index = 10050
// last updated funding index = 10000
// total funding = 200_000 + (10 * (10000 - 10050)) = 199_500
netFunding: getFixedRepresentation('199500'),
// sumClose=0, so realized Pnl is the same as the net funding of the position.
// Unsettled funding is funding payments that already "happened" but not reflected
// in the subaccount's balance yet, so it's considered a part of realizedPnl.
realizedPnl: getFixedRepresentation('199500'),
// For the calculation of the unrealized pnl (long position):
// index price = 15_000, entry price = 20_000, size = 10
// unrealizedPnl = size * (index price - entry price)
// unrealizedPnl = 10 * (15_000 - 20_000)
unrealizedPnl: getFixedRepresentation('-50000'),
createdAt: testConstants.createdDateTime.toISO(),
closedAt: null,
createdAtHeight: testConstants.createdHeight,
subaccountNumber: testConstants.defaultSubaccount.subaccountNumber,
};
// object for expected 2 which holds an isolated position in an isolated perpetual
// in the isolated subaccount
const expected2: PerpetualPositionResponseObject = {
market: testConstants.isolatedPerpetualMarket.ticker,
side: PositionSide.SHORT,
status: testConstants.isolatedPerpetualPosition.status,
size: '-10',
maxSize: testConstants.isolatedPerpetualPosition.maxSize,
entryPrice: getFixedRepresentation(testConstants.isolatedPerpetualPosition.entryPrice!),
exitPrice: null,
sumOpen: testConstants.isolatedPerpetualPosition.sumOpen!,
sumClose: testConstants.isolatedPerpetualPosition.sumClose!,
// For the calculation of the net funding (short position):
// settled funding on position = 200_000, size = -10, latest funding index = 10200
// last updated funding index = 10000
// total funding = 200_000 + (-10 * (10000 - 10200)) = 202_000
netFunding: getFixedRepresentation('202000'),
// sumClose=0, so realized Pnl is the same as the net funding of the position.
// Unsettled funding is funding payments that already "happened" but not reflected
// in the subaccount's balance yet, so it's considered a part of realizedPnl.
realizedPnl: getFixedRepresentation('202000'),
// For the calculation of the unrealized pnl (short position):
// index price = 1, entry price = 1.5, size = -10
// unrealizedPnl = size * (index price - entry price)
// unrealizedPnl = -10 * (1-1.5)
unrealizedPnl: getFixedRepresentation('5'),
createdAt: testConstants.createdDateTime.toISO(),
closedAt: null,
createdAtHeight: testConstants.createdHeight,
subaccountNumber: testConstants.isolatedSubaccount.subaccountNumber,
};

expect(response.body.positions).toEqual(
expect.arrayContaining([
expect.objectContaining({
...expected,
}),
expect.objectContaining({
...expected2,
}),
]),
);
});

it('Get /perpetualPositions/parentSubaccountNumber gets CLOSED position without adjusting funding', async () => {
await Promise.all([
PerpetualPositionTable.create({
...testConstants.defaultPerpetualPosition,
status: PerpetualPositionStatus.CLOSED,
}),
PerpetualPositionTable.create({
...testConstants.isolatedPerpetualPosition,
side: PositionSide.SHORT,
size: '-10',
}),
]);

const parentSubaccountNumber: number = 0;
const response: request.Response = await sendRequest({
type: RequestMethod.GET,
path: `/v4/perpetualPositions/parentSubaccountNumber?address=${testConstants.defaultAddress}` +
`&parentSubaccountNumber=${parentSubaccountNumber}`,
});

const expected: PerpetualPositionResponseObject = {
market: testConstants.defaultPerpetualMarket.ticker,
side: testConstants.defaultPerpetualPosition.side,
status: PerpetualPositionStatus.CLOSED,
size: testConstants.defaultPerpetualPosition.size,
maxSize: testConstants.defaultPerpetualPosition.maxSize,
entryPrice: getFixedRepresentation(testConstants.defaultPerpetualPosition.entryPrice!),
exitPrice: null,
sumOpen: testConstants.defaultPerpetualPosition.sumOpen!,
sumClose: testConstants.defaultPerpetualPosition.sumClose!,
// CLOSED position should not have funding adjusted
netFunding: getFixedRepresentation(
testConstants.defaultPerpetualPosition.settledFunding,
),
realizedPnl: getFixedRepresentation(
testConstants.defaultPerpetualPosition.settledFunding,
),
// For the calculation of the unrealized pnl (short position):
// index price = 15_000, entry price = 20_000, size = 10
// unrealizedPnl = size * (index price - entry price)
// unrealizedPnl = 10 * (15_000 - 20_000)
unrealizedPnl: getFixedRepresentation('-50000'),
createdAt: testConstants.createdDateTime.toISO(),
closedAt: null,
createdAtHeight: testConstants.createdHeight,
subaccountNumber: testConstants.defaultSubaccount.subaccountNumber,
};
const expected2: PerpetualPositionResponseObject = {
market: testConstants.isolatedPerpetualMarket.ticker,
side: PositionSide.SHORT,
status: testConstants.isolatedPerpetualPosition.status,
size: '-10',
maxSize: testConstants.isolatedPerpetualPosition.maxSize,
entryPrice: getFixedRepresentation(testConstants.isolatedPerpetualPosition.entryPrice!),
exitPrice: null,
sumOpen: testConstants.isolatedPerpetualPosition.sumOpen!,
sumClose: testConstants.isolatedPerpetualPosition.sumClose!,
// CLOSED position should not have funding adjusted
netFunding: getFixedRepresentation(
testConstants.isolatedPerpetualPosition.settledFunding,
),
realizedPnl: getFixedRepresentation(
testConstants.isolatedPerpetualPosition.settledFunding,
),
// For the calculation of the unrealized pnl (short position):
// index price = 1, entry price = 1.5, size = -10
// unrealizedPnl = size * (index price - entry price)
// unrealizedPnl = -10 * (1-1.5)
unrealizedPnl: getFixedRepresentation('5'),
createdAt: testConstants.createdDateTime.toISO(),
closedAt: null,
createdAtHeight: testConstants.createdHeight,
subaccountNumber: testConstants.isolatedSubaccount.subaccountNumber,
};

expect(response.body.positions).toEqual(
expect.arrayContaining([
expect.objectContaining({
...expected,
}),
expect.objectContaining({
...expected2,
}),
]),
);
});

it.each([
[
'invalid status',
{
address: defaultAddress,
parentSubaccountNumber: defaultSubaccountNumber,
status: 'INVALID',
},
'status',
'status must be a valid Position Status (OPEN, etc)',
],
[
'multiple invalid status',
{
address: defaultAddress,
parentSubaccountNumber: defaultSubaccountNumber,
status: 'INVALID,INVALID',
},
'status',
'status must be a valid Position Status (OPEN, etc)',
],
])('Returns 400 when validation fails: %s', async (
_reason: string,
queryParams: {
address?: string,
subaccountNumber?: number,
status?: string,
},
fieldWithError: string,
expectedErrorMsg: string,
) => {
const response: request.Response = await sendRequest({
type: RequestMethod.GET,
path: `/v4/perpetualPositions/parentSubaccountNumber?${getQueryString(queryParams)}`,
expectedStatus: 400,
});

expect(response.body).toEqual(expect.objectContaining({
errors: expect.arrayContaining([
expect.objectContaining({
param: fieldWithError,
msg: expectedErrorMsg,
}),
]),
}));
});
});
});
100 changes: 100 additions & 0 deletions indexer/services/comlink/public/api-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1861,6 +1861,106 @@ fetch('https://dydx-testnet.imperator.co/v4/perpetualPositions?address=string&su
This operation does not require authentication
</aside>

## ListPositionsForParentSubaccount

<a id="opIdListPositionsForParentSubaccount"></a>

> Code samples
```python
import requests
headers = {
'Accept': 'application/json'
}

r = requests.get('https://dydx-testnet.imperator.co/v4/perpetualPositions/parentSubaccountNumber', params={
'address': 'string', 'parentSubaccountNumber': '0'
}, headers = headers)

print(r.json())

```

```javascript

const headers = {
'Accept':'application/json'
};

fetch('https://dydx-testnet.imperator.co/v4/perpetualPositions/parentSubaccountNumber?address=string&parentSubaccountNumber=0',
{
method: 'GET',

headers: headers
})
.then(function(res) {
return res.json();
}).then(function(body) {
console.log(body);
});

```

`GET /perpetualPositions/parentSubaccountNumber`

### Parameters

|Name|In|Type|Required|Description|
|---|---|---|---|---|
|address|query|string|true|none|
|parentSubaccountNumber|query|number(double)|true|none|
|status|query|array[string]|false|none|
|limit|query|number(double)|false|none|
|createdBeforeOrAtHeight|query|number(double)|false|none|
|createdBeforeOrAt|query|[IsoString](#schemaisostring)|false|none|

#### Enumerated Values

|Parameter|Value|
|---|---|
|status|OPEN|
|status|CLOSED|
|status|LIQUIDATED|

> Example responses
> 200 Response
```json
{
"positions": [
{
"market": "string",
"status": "OPEN",
"side": "LONG",
"size": "string",
"maxSize": "string",
"entryPrice": "string",
"realizedPnl": "string",
"createdAt": "string",
"createdAtHeight": "string",
"sumOpen": "string",
"sumClose": "string",
"netFunding": "string",
"unrealizedPnl": "string",
"closedAt": "string",
"exitPrice": "string",
"subaccountNumber": 0
}
]
}
```

### Responses

|Status|Meaning|Description|Schema|
|---|---|---|---|
|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Ok|[PerpetualPositionResponse](#schemaperpetualpositionresponse)|

<aside class="success">
This operation does not require authentication
</aside>

## Get

<a id="opIdGet"></a>
Expand Down
Loading

0 comments on commit 817700b

Please sign in to comment.