Skip to content

Commit c7f7296

Browse files
authored
Add endpoint for fetching new pnl data (#3098)
1 parent 5ce1301 commit c7f7296

File tree

14 files changed

+2419
-13
lines changed

14 files changed

+2419
-13
lines changed

indexer/packages/postgres/__tests__/stores/pnl-table.test.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,196 @@ describe('Pnl store', () => {
137137
expect(responsePageAllPages.total).toEqual(2);
138138
});
139139

140+
it('Successfully retrieves daily PNL records with latest for current day and earliest for previous days', async () => {
141+
const records = [];
142+
143+
// Day 1 (Jan 1): Create records at 00:00, 06:00, 12:00, 18:00
144+
const day1Date = new Date('2023-01-01T00:00:00.000Z');
145+
for (let i = 0; i < 24; i += 6) {
146+
const date = new Date(day1Date);
147+
date.setUTCHours(i);
148+
records.push({
149+
...defaultPnl,
150+
createdAt: date.toISOString(),
151+
createdAtHeight: (1000 + i).toString(),
152+
equity: (1000 + i).toString(),
153+
});
154+
}
155+
156+
// Day 2 (Jan 2): Create records at 00:00, 06:00, 12:00, 18:00
157+
const day2Date = new Date('2023-01-02T00:00:00.000Z');
158+
for (let i = 0; i < 24; i += 6) {
159+
const date = new Date(day2Date);
160+
date.setUTCHours(i);
161+
records.push({
162+
...defaultPnl,
163+
createdAt: date.toISOString(),
164+
createdAtHeight: (2000 + i).toString(),
165+
equity: (2000 + i).toString(),
166+
});
167+
}
168+
169+
// Day 3 (Jan 3): Create records at 00:00, 06:00, 12:00, 18:00
170+
const day3Date = new Date('2023-01-03T00:00:00.000Z');
171+
for (let i = 0; i < 24; i += 6) {
172+
const date = new Date(day3Date);
173+
date.setUTCHours(i);
174+
records.push({
175+
...defaultPnl,
176+
createdAt: date.toISOString(),
177+
createdAtHeight: (3000 + i).toString(),
178+
equity: (3000 + i).toString(),
179+
});
180+
}
181+
182+
// Insert all records
183+
await Promise.all(records.map((record) => PnlTable.create(record)));
184+
185+
// Get daily records
186+
const dailyResults = await PnlTable.findAllDailyPnl(
187+
{ subaccountId: [defaultSubaccountId] },
188+
[],
189+
{},
190+
);
191+
192+
// We should get exactly 3 records (one for each day)
193+
expect(dailyResults.results.length).toBe(3);
194+
195+
// The first record should be the latest one from day 3 (18:00)
196+
expect(dailyResults.results[0].createdAtHeight).toBe('3018');
197+
expect(dailyResults.results[0].createdAt).toBe('2023-01-03T18:00:00.000Z');
198+
199+
// The second record should be the earliest one from day 2 (00:00)
200+
expect(dailyResults.results[1].createdAtHeight).toBe('2000');
201+
expect(dailyResults.results[1].createdAt).toBe('2023-01-02T00:00:00.000Z');
202+
203+
// The third record should be the earliest one from day 1 (00:00)
204+
expect(dailyResults.results[2].createdAtHeight).toBe('1000');
205+
expect(dailyResults.results[2].createdAt).toBe('2023-01-01T00:00:00.000Z');
206+
207+
// Test with pagination - first page
208+
const dailyPage1 = await PnlTable.findAllDailyPnl(
209+
{
210+
subaccountId: [defaultSubaccountId],
211+
page: 1,
212+
limit: 2,
213+
},
214+
[],
215+
{},
216+
);
217+
218+
expect(dailyPage1.results.length).toBe(2);
219+
expect(dailyPage1.limit).toBe(2);
220+
expect(dailyPage1.offset).toBe(0);
221+
expect(dailyPage1.total).toBe(3);
222+
223+
// First page should have day 3 (latest) and day 2 (earliest)
224+
expect(dailyPage1.results[0].createdAtHeight).toBe('3018');
225+
expect(dailyPage1.results[1].createdAtHeight).toBe('2000');
226+
227+
// Test with pagination - second page
228+
const dailyPage2 = await PnlTable.findAllDailyPnl(
229+
{
230+
subaccountId: [defaultSubaccountId],
231+
page: 2,
232+
limit: 2,
233+
},
234+
[],
235+
{},
236+
);
237+
238+
// The second page should have only day 1
239+
expect(dailyPage2.results.length).toBe(1);
240+
expect(dailyPage2.limit).toBe(2);
241+
expect(dailyPage2.offset).toBe(2);
242+
expect(dailyPage2.total).toBe(3);
243+
expect(dailyPage2.results[0].createdAtHeight).toBe('1000');
244+
245+
// Test with date range filter
246+
const cutoffDate = new Date('2023-01-02T12:00:00.000Z');
247+
248+
const dailyWithDateFilter = await PnlTable.findAllDailyPnl(
249+
{
250+
subaccountId: [defaultSubaccountId],
251+
createdBeforeOrAt: cutoffDate.toISOString(),
252+
},
253+
[],
254+
{},
255+
);
256+
257+
// We should get 2 records: day 1 (earliest) and day 2 (records up to 12:00)
258+
expect(dailyWithDateFilter.results.length).toBe(2);
259+
260+
// Day 2 should be represented by the latest record before our cutoff (12:00)
261+
expect(dailyWithDateFilter.results[0].createdAtHeight).toBe('2012');
262+
expect(dailyWithDateFilter.results[0].createdAt).toBe('2023-01-02T12:00:00.000Z');
263+
264+
// Day 1 should still be the earliest record
265+
expect(dailyWithDateFilter.results[1].createdAtHeight).toBe('1000');
266+
expect(dailyWithDateFilter.results[1].createdAt).toBe('2023-01-01T00:00:00.000Z');
267+
});
268+
269+
it('Successfully handles case where latest record is at midnight (00:00)', async () => {
270+
const records = [];
271+
272+
// Day 1 (Jan 1): Create records at 00:00, 06:00, 12:00, 18:00
273+
const day1Date = new Date('2023-01-01T00:00:00.000Z');
274+
for (let i = 0; i < 24; i += 6) {
275+
const date = new Date(day1Date);
276+
date.setUTCHours(i);
277+
records.push({
278+
...defaultPnl,
279+
createdAt: date.toISOString(),
280+
createdAtHeight: (1000 + i).toString(),
281+
equity: (1000 + i).toString(),
282+
});
283+
}
284+
285+
// Day 2 (Jan 2): Create records at 00:00, 06:00, 12:00, 18:00
286+
const day2Date = new Date('2023-01-02T00:00:00.000Z');
287+
for (let i = 0; i < 24; i += 6) {
288+
const date = new Date(day2Date);
289+
date.setUTCHours(i);
290+
records.push({
291+
...defaultPnl,
292+
createdAt: date.toISOString(),
293+
createdAtHeight: (2000 + i).toString(),
294+
equity: (2000 + i).toString(),
295+
});
296+
}
297+
298+
// Day 3 (Jan 3): Create ONLY a record at 00:00 (to test the case where latest is at midnight)
299+
// Give this record the highest height to ensure it's the latest
300+
records.push({
301+
...defaultPnl,
302+
createdAt: '2023-01-03T00:00:00.000Z',
303+
createdAtHeight: '3500', // Highest height to ensure it's the latest
304+
equity: '3500',
305+
});
306+
307+
// Insert all records
308+
await Promise.all(records.map((record) => PnlTable.create(record)));
309+
310+
// Get daily records
311+
const dailyResults = await PnlTable.findAllDailyPnl(
312+
{ subaccountId: [defaultSubaccountId] },
313+
[],
314+
{},
315+
);
316+
317+
// We should get exactly 3 records (one for each day)
318+
expect(dailyResults.results.length).toBe(3);
319+
320+
// The first record should be the latest one from day 3 (00:00)
321+
expect(dailyResults.results[0].createdAtHeight).toBe('3500');
322+
expect(dailyResults.results[0].createdAt).toBe('2023-01-03T00:00:00.000Z');
323+
324+
// The second record should be the earliest one from day 2 (00:00)
325+
expect(dailyResults.results[1].createdAtHeight).toBe('2000');
326+
expect(dailyResults.results[1].createdAt).toBe('2023-01-02T00:00:00.000Z');
327+
328+
// The third record should be the earliest one from day 1 (00:00)
329+
expect(dailyResults.results[2].createdAtHeight).toBe('1000');
330+
expect(dailyResults.results[2].createdAt).toBe('2023-01-01T00:00:00.000Z');
331+
});
140332
});

indexer/packages/postgres/src/stores/pnl-table.ts

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,20 +150,20 @@ async function handleLimitAndPagination(
150150
let query = baseQuery;
151151

152152
/**
153-
* If a query is made using a page number, then the limit property is used as 'page limit'
154-
*/
153+
* If a query is made using a page number, then the limit property is used as 'page limit'
154+
*/
155155
if (page !== undefined && limit !== undefined) {
156156
/**
157-
* We make sure that the page number is always >= 1
158-
*/
157+
* We make sure that the page number is always >= 1
158+
*/
159159
const currentPage: number = Math.max(1, page);
160160
const offset: number = (currentPage - 1) * limit;
161161

162162
/**
163-
* Ensure sorting is applied to maintain consistent pagination results.
164-
* Also a casting of the ts type is required since the infer of the type
165-
* obtained from the count is not performed.
166-
*/
163+
* Ensure sorting is applied to maintain consistent pagination results.
164+
* Also a casting of the ts type is required since the infer of the type
165+
* obtained from the count is not performed.
166+
*/
167167
const count: { count?: string } = (await query
168168
.clone()
169169
.clearOrder()
@@ -191,3 +191,120 @@ async function handleLimitAndPagination(
191191
results,
192192
};
193193
}
194+
195+
export async function findAllDailyPnl(
196+
{
197+
limit,
198+
subaccountId,
199+
createdBeforeOrAtHeight,
200+
createdBeforeOrAt,
201+
createdOnOrAfterHeight,
202+
createdOnOrAfter,
203+
page,
204+
parentSubaccount,
205+
}: PnlQueryConfig,
206+
requiredFields: QueryableField[],
207+
options: Options = DEFAULT_POSTGRES_OPTIONS,
208+
): Promise<PaginationFromDatabase<PnlFromDatabase>> {
209+
if (parentSubaccount !== undefined && subaccountId !== undefined) {
210+
throw new Error('Cannot specify both parentSubaccount and subaccountId');
211+
}
212+
213+
verifyAllRequiredFields(
214+
{
215+
limit,
216+
subaccountId,
217+
createdBeforeOrAtHeight,
218+
createdBeforeOrAt,
219+
createdOnOrAfterHeight,
220+
createdOnOrAfter,
221+
page,
222+
parentSubaccount,
223+
} as QueryConfig,
224+
requiredFields,
225+
);
226+
227+
let baseQuery: QueryBuilder<PnlModel> = setupBaseQuery<PnlModel>(
228+
PnlModel,
229+
options,
230+
);
231+
232+
if (subaccountId !== undefined) {
233+
baseQuery = baseQuery.whereIn(PnlColumns.subaccountId, subaccountId);
234+
} else if (parentSubaccount !== undefined) {
235+
baseQuery = baseQuery.whereIn(
236+
PnlColumns.subaccountId,
237+
getSubaccountQueryForParent(parentSubaccount),
238+
);
239+
}
240+
241+
if (createdBeforeOrAtHeight !== undefined) {
242+
baseQuery = baseQuery.where(
243+
PnlColumns.createdAtHeight,
244+
'<=',
245+
createdBeforeOrAtHeight,
246+
);
247+
}
248+
249+
if (createdBeforeOrAt !== undefined) {
250+
baseQuery = baseQuery.where(PnlColumns.createdAt, '<=', createdBeforeOrAt);
251+
}
252+
253+
if (createdOnOrAfterHeight !== undefined) {
254+
baseQuery = baseQuery.where(
255+
PnlColumns.createdAtHeight,
256+
'>=',
257+
createdOnOrAfterHeight,
258+
);
259+
}
260+
261+
if (createdOnOrAfter !== undefined) {
262+
baseQuery = baseQuery.where(PnlColumns.createdAt, '>=', createdOnOrAfter);
263+
}
264+
265+
const knex = PnlModel.knex();
266+
// 1. Identify the latest record for each subaccount (with RANK = 1 over entire subaccount)
267+
// 2. For all other records, rank them within their day (RANK ordered by time ascending)
268+
// 3. Select the latest record and earliest records for each other day
269+
const rankQuery = baseQuery.clone()
270+
.select('*')
271+
.select(
272+
knex.raw(`
273+
RANK() OVER (
274+
PARTITION BY "${PnlColumns.subaccountId}"
275+
ORDER BY "${PnlColumns.createdAtHeight}" DESC
276+
) as latest_rank,
277+
DATE_TRUNC('day', "${PnlColumns.createdAt}" AT TIME ZONE 'UTC') as day_date,
278+
RANK() OVER (
279+
PARTITION BY "${PnlColumns.subaccountId}", DATE_TRUNC('day', "${PnlColumns.createdAt}" AT TIME ZONE 'UTC')
280+
ORDER BY "${PnlColumns.createdAt}" ASC
281+
) as earliest_in_day_rank
282+
`),
283+
);
284+
285+
// Now select only records that are either:
286+
// 1. The very latest for their subaccount (latest_rank = 1), OR
287+
// 2. The earliest record for their day (day_rank = 1) but NOT the latest day
288+
const finalQuery = PnlModel.query(Transaction.get(options.txId))
289+
.with('ranked_pnl', rankQuery)
290+
.from(
291+
knex.raw(`
292+
(
293+
SELECT DISTINCT ON ("subaccountId", day_date) *
294+
FROM ranked_pnl
295+
WHERE
296+
-- Either it's the latest record overall
297+
(latest_rank = 1)
298+
OR
299+
-- Or it's the earliest record of a day
300+
(earliest_in_day_rank = 1)
301+
ORDER BY "subaccountId", day_date, latest_rank ASC
302+
) AS unique_daily_records
303+
`),
304+
)
305+
.orderBy(PnlColumns.subaccountId, Ordering.ASC)
306+
.orderBy(PnlColumns.createdAtHeight, Ordering.DESC);
307+
308+
// Apply pagination if needed
309+
return handleLimitAndPagination(finalQuery, limit, page);
310+
}

0 commit comments

Comments
 (0)