Skip to content

[ENG-1734] feat(comlink): add GET /v4/tradeHistory endpoint with cumulative PnL tracking#3323

Merged
davidli1997 merged 19 commits intomainfrom
davidli/trade-history
Mar 3, 2026
Merged

[ENG-1734] feat(comlink): add GET /v4/tradeHistory endpoint with cumulative PnL tracking#3323
davidli1997 merged 19 commits intomainfrom
davidli/trade-history

Conversation

@davidli1997
Copy link
Contributor

@davidli1997 davidli1997 commented Feb 13, 2026

Changelist

  • Extract shared buildClobPairIdToMarket() helper from fills-controller into lib/helpers.ts to avoid duplication
  • Fix flaky test helper randomInt() that caused clientId collisions in integration tests
  • Add new GET /v4/tradeHistory endpoint with two routes:
    • GET / — trade history for a single subaccount
    • GET /parentSubaccountNumber — trade history for a parent subaccount (includes child/isolated subaccounts)
  • Core algorithm in lib/trade-history.ts computes trade history rows on-the-fly from fills:
  • Detects cross-zero orders (e.g., sell 10 when long 5) and splits into CLOSE + OPEN rows
  • 6 trade types: OPEN, EXTEND, PARTIAL_CLOSE, CLOSE, LIQUIDATION_PARTIAL_CLOSE, LIQUIDATION_CLOSE

Example: User Trading Activity on BTC-USD

#  Action       Side  Size  Price
1  Buy          BUY   10    $100
2  Buy more     BUY   10    $120
3  Sell half    SELL  10    $130
4  Sell rest    SELL  10    $90
5  Short        SELL  5     $85
6  Cover short  BUY   5     $80

Trade History Cards Produced (most recent first):

#  action               side  prevSize  additionalSize  executionPrice  entryPrice  netRealizedPnl  netRealizedPnlPercent  positionSide
6  CLOSE                BUY   5         5               80              85          25              0.0588 (5.88%)         null
5  OPEN                 SELL  0         -5              85              85          0               null                   SHORT
4  CLOSE                SELL  10        -10             90              110         0               0 (0%)                 null
3  PARTIAL_CLOSE        SELL  20        -10             130             110         200             0.1818 (18.18%)        LONG
2  EXTEND               BUY   10        10              120             110         0               null                   LONG
1  OPEN                 BUY   0         10              100             100         0               null                   LONG

How the numbers work:

Row 1: Open long 10 @ $100. entryPrice = $100.
Row 2: Extend by 10 @ $120. entryPrice = weighted avg = (10010 + 12010) / 20 = $110.
Row 3: Partial close 10 @ $130. PnL = (130 - 110) * 10 = $200. Cost basis = 110 * 10 = $1,100. Percent = 200/1100 = 18.18%.
Row 4: Close remaining 10 @ $90. PnL = (90 - 110) * 10 = -$200. Cumulative = 200 + (-200) = $0. Cum cost basis = 1100 + 1100 = $2,200. Percent = 0/2200 = 0%. Lifecycle resets.
Row 5: Open short 5 @ $85. Fresh lifecycle -- netRealizedPnl = 0, netRealizedPnlPercent = null.
Row 6: Close short 5 @ $80. PnL = (85 - 80) * 5 = $25. Cost basis = 85 * 5 = $425. Percent = 25/425 = 5.88%.

Key behaviors:

  • entryPrice updates on OPEN (set) and EXTEND (weighted average), stays fixed on partial/full close
  • netRealizedPnl and netRealizedPnlPercent are cumulative within a position lifecycle, reset to 0/null when position goes flat
  • netRealizedPnlPercent = cumPnl / cumCostBasis, where cumCostBasis = sum(entryPrice * closingAmount) across all closes
  • netRealizedPnlPercent is null on OPEN/EXTEND rows that have no prior closes in the lifecycle
  • Frontend can compute per-trade % as (executionPrice - entryPrice) / entryPrice (long) or (entryPrice - executionPrice) / entryPrice (short)

Test Plan

  • staging
  • testnet
  • mainnet

Author/Reviewer Checklist

  • If this PR has changes that result in a different app state given the same prior state and transaction list, manually add the state-breaking label.
  • If the PR has breaking postgres changes to the indexer add the indexer-postgres-breaking label.
  • If this PR isn't state-breaking but has changes that modify behavior in PrepareProposal or ProcessProposal, manually add the label proposal-breaking.
  • If this PR is one of many that implement a specific feature, manually label them all feature:[feature-name].
  • If you wish to for mergify-bot to automatically create a PR to backport your change to a release branch, manually add the label backport/[branch-name].
  • Manually add any of the following labels: refactor, chore, bug.

Summary by CodeRabbit

  • New Features

    • Added /tradeHistory and /tradeHistory/parentSubaccount endpoints returning detailed trade history (actions, execution price, fees, net realized PnL, order IDs, market, margin mode). Supports market/marketType filtering, pagination, limits, and handles liquidation and cross-zero lifecycle cases.
  • Documentation

    • Added API docs and schemas with examples and response shapes for the new endpoints.
  • Tests

    • Comprehensive tests covering sorting, grouping, pagination, validation, error cases, parent/child subaccounts, and deterministic ID generation for stable results.

@davidli1997 davidli1997 requested a review from a team as a code owner February 13, 2026 15:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a trade history feature: new in-memory compute and pagination library, Express v4 controller endpoints for subaccount and parent-subaccount trade history, types and Swagger docs, clobPairId→market helper, fills-controller integration changes, deterministic test helpers, and comprehensive unit + integration tests.

Changes

Cohort / File(s) Summary
Core trade-history logic
indexer/services/comlink/src/lib/trade-history.ts
New module exporting computeTradeHistory and paginateTradeHistory: groups fills, tracks per-order/market lifecycle, handles OPEN/EXTEND/PARTIAL_CLOSE/CLOSE/LIQUIDATION cases, cross-zero transitions, weighted prices, PnL/fee calculations, sorting, and result shaping.
API controller
indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts
New Express router with GET /tradeHistory and GET /tradeHistory/parentSubaccountNumber: validation, clobPairId resolution, batched order-type lookup, DB queries, pagination, limit enforcement, and response shaping.
Types
indexer/services/comlink/src/types.ts
Added TradeHistoryType enum, TradeHistoryRequest / ParentSubaccountTradeHistoryRequest, TradeHistoryResponseObject and TradeHistoryResponse; added optional perpetualMarketType?: PerpetualMarketType on MarketAndType.
Market mapping helper
indexer/services/comlink/src/lib/helpers.ts
Added buildClobPairIdToMarket() to produce clobPairId→market mapping (consumes perpetual market data, marks marketType PERPETUAL).
Fills controller integration
indexer/services/comlink/src/controllers/api/v4/fills-controller.ts
Replaced manual mapping and direct perpetual-market imports with call to buildClobPairIdToMarket(); removed unused imports and types.
Route registration
indexer/services/comlink/src/controllers/api/index-v4.ts
Registered new trade-history router at /tradeHistory.
Public API docs / Swagger
indexer/services/comlink/public/api-documentation.md, indexer/services/comlink/public/swagger.json
Added GET /tradeHistory and GET /tradeHistory/parentSubaccountNumber (and duplicate /parentSubaccount) endpoints, request params, response schemas (TradeHistoryResponseObject, TradeHistoryResponse, enums) and examples; docs contain duplicated schema blocks.
Controller integration tests
indexer/services/comlink/__tests__/controllers/api/v4/trade-history-controller.test.ts
New integration tests covering lifecycle scenarios, filtering, market/marketType validation, pagination, parent-subaccount flows, 404/400 cases, and deterministic seeding/setup/teardown.
Unit tests for logic
indexer/services/comlink/__tests__/lib/trade-history.test.ts
Extensive unit tests for computeTradeHistory and paginateTradeHistory: varied actions, cross-zero handling, liquidation cases, grouping by orderId, weighted entry price, cumulative PnL, pagination boundaries, and multi-market separation.
Test helpers
indexer/services/comlink/__tests__/helpers/helpers.ts
Replaced nondeterministic randomInt with deterministic incremental nextInt (starts at 100000) for stable test IDs/clientId.
CI workflow
.github/workflows/indexer-build-and-push-dev-staging.yml
Added branch trigger davidli/trade-history to push events.
Public swagger exports
indexer/services/comlink/public/swagger.json
Added public schemas: TradeHistoryType, TradeHistoryResponseObject, TradeHistoryResponse and paths /tradeHistory and /tradeHistory/parentSubaccount*.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as TradeHistoryController
    participant Validator as Validation Layer
    participant DB as FillTable/Database
    participant OrderType as OrderType Lookup
    participant MarketMap as ClobPairId→Market
    participant Logic as TradeHistory Logic
    participant Response

    Client->>Controller: GET /v4/tradeHistory?address=A&subaccountNumber=S&limit=10&page=1
    Controller->>Validator: validate params (address, subaccountNumber, market/marketType)
    Validator-->>Controller: OK
    Controller->>DB: query fills for subaccount (ordered by time desc)
    DB-->>Controller: [fills...]
    Controller->>OrderType: batch fetch order types for fill.orderId[]
    OrderType-->>Controller: orderTypeMap
    Controller->>MarketMap: buildClobPairIdToMarket()
    MarketMap-->>Controller: clobPairIdToMarket
    Controller->>Logic: computeTradeHistory(fills, orderTypeMap, clobPairIdToMarket, subaccountMap)
    Note over Logic: group by market/order → compute rows\nhandle cross-zero, liquidation, weighted prices, PnL
    Logic-->>Controller: [tradeHistoryRows...]
    Controller->>Controller: paginateTradeHistory(rows, limit, page)
    Controller->>Response: 200 TradeHistoryResponse (pageSize, offset, totalResults, tradeHistory)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • tqin7
  • shrenujb
  • Kefancao

Poem

🐇 I hopped through fills both near and far,

grouped every order, counted each scar,
PnL and fees in tidy rows,
pages that flip where history flows,
a joyful hop — trade history, hurrah!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a GET /v4/tradeHistory endpoint with cumulative PnL tracking functionality, which aligns with the primary deliverable in the pull request.
Description check ✅ Passed The PR description provides a comprehensive changelist, detailed algorithm explanation with examples, test plan reference, and follows the required template structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch davidli/trade-history

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@indexer/services/comlink/__tests__/lib/trade-history.test.ts`:
- Around line 158-162: Tests disagree on the sign convention for additionalSize:
stick to the documented "signed: positive = increasing, negative = reducing"
contract and make both OPEN short cases produce a negative additionalSize;
update the failing test expecting '5' to expect '-5' (or if the implementation
is wrong, change the code that computes additionalSize so that when
TradeHistoryType.OPEN and PositionSide.SHORT the produced additionalSize is
negative). Ensure the symbol additionalSize (used alongside
TradeHistoryType.OPEN and PositionSide.SHORT) is consistently signed across both
simple OPEN short and cross-zero OPEN short paths.

In `@indexer/services/comlink/public/api-documentation.md`:
- Around line 3667-3669: There are blank lines inside blockquotes in
api-documentation.md (the block that contains the lines starting with "> Example
responses" and "> 200 Response" and similarly at the other occurrence), which
violates MD028; remove the empty lines between the quote markers so each
blockquote contains only contiguous quoted lines (i.e., delete any empty lines
between consecutive ">" lines around the "Example responses" and "200 Response"
blocks and the similar block at the other location).
- Around line 3677-3691: The API docs show the trade history JSON with the field
"market" but the implementation and types use "marketId"; update the
documentation and swagger schema so the response field is named "marketId"
everywhere: replace "market" with "marketId" in the JSON examples in
api-documentation.md (the trade history response example) and update the
corresponding swagger.json response schema entries to use "marketId": string;
ensure the examples and schema keys match the TypeScript response type
(marketId) and run a quick search for any other occurrences of "market" used as
the trade history field to correct them.

In `@indexer/services/comlink/public/swagger.json`:
- Around line 1461-1526: The Swagger schema TradeHistoryResponseObject must be
updated to match the TypeScript interface: rename the existing "market" property
to "marketId" (preserve type string), and add the missing properties "id" (type
string) and "positionSide" (use the same enum/ref used elsewhere for position
side, e.g., the PositionSide schema or a string if that is how it's modelled);
ensure these fields are included in the "properties" section and in the
"required" array if the TypeScript interface marks them required, and keep
"additionalProperties": false.

In `@indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts`:
- Around line 125-134: The FillTable.findAll calls (search invoked via
FillTable.findAll) fetch all matching fills because no limit is passed; to
prevent unbounded memory use, add a server-side cap by supplying a capped limit
parameter (e.g., use a MAX_FILL_FETCH constant) when calling FillTable.findAll
and/or validate the incoming pagination limit so it cannot exceed that cap, and
if neither a limit nor a time-bounded filter (createdBeforeOrAt or
createdBeforeOrAtHeight) is provided, reject the request with a 400 or require a
time window; update both occurrences of FillTable.findAll to use the capped
limit/validation logic.
- Line 151: The TSOA route decorator (`@Get`('/parentSubaccount')) does not match
the actual Express route (/parentSubaccountNumber), causing generated docs to
point to the wrong endpoint; update the TSOA decorator in the
TradeHistoryController (replace `@Get`('/parentSubaccount') with
`@Get`('/parentSubaccountNumber')) so the swagger path
/tradeHistory/parentSubaccountNumber matches the runtime route, and apply the
same fix in the FillsController where the same mismatch exists.

In `@indexer/services/comlink/src/lib/trade-history.ts`:
- Around line 236-309: handleCrossZero returns closeRow then openRow with
identical group.lastCreatedAt, causing V8 stable DESC sort to keep close before
open; fix by giving the open row a synthetic later timestamp (or secondary
tiebreaker) so open sorts after close: in handleCrossZero set openRow.time to a
slightly later value than group.lastCreatedAt (e.g., add 1ms or increment a
numeric timestamp) or alternatively include a deterministic suffix/tiebreaker in
makeRowId and adjust the final sort to consider that suffix as a secondary key;
update handling to preserve original time type and ensure tests/consumers accept
the adjusted time/tiebreaker.
- Around line 84-111: The paginateTradeHistory usage loads all rows into memory;
to prevent unbounded work modify computeTradeHistory and paginateTradeHistory to
accept a lower-time filter (e.g., createdAfter) or enforce a hard cap when
fetching/processing (e.g., MAX_TRADE_ROWS) so you never materialize more than
the cap; update function signatures (computeTradeHistory, paginateTradeHistory)
to accept createdAfter or apply the MAX_TRADE_ROWS cap early (before full
mapping) and ensure the createdBeforeOrAt logic is combined with the new lower
bound or cap so only a bounded window of fills is computed and sliced for
pagination.
- Around line 65-71: The code is using a non-null assertion on
marketInfo.perpetualMarketType when calling processMarketFills, which can pass
undefined as marginMode; update the loop to guard/per-check
marketInfo.perpetualMarketType (the symbol perpetualMarketType on marketInfo)
before calling processMarketFills and either continue the loop or provide a safe
default marginMode; ensure the change is applied where fillsByMarket is iterated
and processMarketFills is invoked (referencing clobPairIdToMarket, marketInfo,
processMarketFills, and orderTypeMap) so you never call processMarketFills with
an undefined perpetualMarketType.
- Around line 55-79: computeTradeHistory currently groups fills only by
clobPairId which causes fills from different subaccounts to be merged for the
same market; change grouping so fills are partitioned by both clobPairId and
subaccountId (or iterate per subaccount) before calling processMarketFills,
e.g., build keys using clobPairId + subaccountId and pass each partition’s fills
along with marketInfo.market and marketInfo.perpetualMarketType to
processMarketFills so each child subaccount’s lifecycle is computed
independently.
🧹 Nitpick comments (2)
indexer/services/comlink/__tests__/controllers/api/v4/trade-history-controller.test.ts (1)

326-345: Consider using exact assertions instead of toBeGreaterThanOrEqual.

toBeGreaterThanOrEqual(2) can mask regressions where extra/duplicate rows are returned. If exactly 2 fills are seeded, assert toHaveLength(2) and toBe(2).

Tighten assertions
-      expect(response.body.tradeHistory.length).toBeGreaterThanOrEqual(2);
-      expect(response.body.totalResults).toBeGreaterThanOrEqual(2);
+      expect(response.body.tradeHistory).toHaveLength(2);
+      expect(response.body.totalResults).toBe(2);
indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts (1)

104-149: Consider extracting shared logic to reduce duplication.

getTradeHistory and getTradeHistoryForParentSubaccount differ only in the FillTable.findAll filter. The rest (clobPairId resolution, orderTypeMap build, compute, paginate) is identical. A shared private method accepting the fill-query filter would cut ~40 lines of duplication.

Also applies to: 152-197

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
indexer/services/comlink/__tests__/lib/trade-history.test.ts (1)

529-575: Consider adding edge-case pagination tests.

The pagination tests cover the main scenarios well. Consider adding tests for boundary conditions like limit exceeding total rows, page = 0, or negative/undefined page values to ensure the implementation handles them gracefully.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/services/comlink/__tests__/lib/trade-history.test.ts` around lines
529 - 575, Add unit tests in the paginateTradeHistory suite to cover edge cases:
(1) limit larger than total rows (e.g., limit=20) should return all rows and
correct totalResults/offset/pageSize; (2) page = 0 should be treated as page 1
(offset 0) or match current implementation—assert expected offset and first
item; (3) negative page values (e.g., page = -1) and undefined page should be
handled predictably—add assertions that they either default to page 1 or
throw/return empty per the implementation. Reference the paginateTradeHistory
function and existing tests in trade-history.test.ts to place these new it(...)
cases alongside the current cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/indexer-build-and-push-dev-staging.yml:
- Line 9: The workflow contains a personal branch trigger
"davidli/trade-history" under the workflow's on: push/branches list which must
be removed; edit the workflow file and delete the "davidli/trade-history" entry
(or replace it with the intended shared branch names like main/staging if
needed) so that CI no longer triggers on your personal feature branch.

---

Duplicate comments:
In `@indexer/services/comlink/__tests__/lib/trade-history.test.ts`:
- Around line 219-227: Update the test expectations to reflect the corrected
sign convention for cross-zero OPEN short trades: in the assertion block
referencing openRow (variables and properties openRow.action, openRow.prevSize,
openRow.additionalSize, openRow.positionSide, openRow.netRealizedPnl,
openRow.netFee), ensure additionalSize is asserted as '-5' (negative for short
direction), positionSide is PositionSide.SHORT, netRealizedPnl is '0' after
lifecycle reset, and netFee matches the computed '0.5' value so the test
consistently follows the "negative = short direction" convention.

---

Nitpick comments:
In `@indexer/services/comlink/__tests__/lib/trade-history.test.ts`:
- Around line 529-575: Add unit tests in the paginateTradeHistory suite to cover
edge cases: (1) limit larger than total rows (e.g., limit=20) should return all
rows and correct totalResults/offset/pageSize; (2) page = 0 should be treated as
page 1 (offset 0) or match current implementation—assert expected offset and
first item; (3) negative page values (e.g., page = -1) and undefined page should
be handled predictably—add assertions that they either default to page 1 or
throw/return empty per the implementation. Reference the paginateTradeHistory
function and existing tests in trade-history.test.ts to place these new it(...)
cases alongside the current cases.

- main
- 'release/indexer/v[0-9]+.[0-9]+.x' # e.g. release/indexer/v0.1.x
- 'release/indexer/v[0-9]+.x' # e.g. release/indexer/v1.x
- davidli/trade-history
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove personal feature branch from CI trigger before merging.

This davidli/trade-history branch trigger is useful during development for staging validation, but it should not be merged into main. It will cause unnecessary CI builds to dev/staging environments if the branch is ever pushed to again, and clutters the workflow config.

-      - davidli/trade-history
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- davidli/trade-history
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/indexer-build-and-push-dev-staging.yml at line 9, The
workflow contains a personal branch trigger "davidli/trade-history" under the
workflow's on: push/branches list which must be removed; edit the workflow file
and delete the "davidli/trade-history" entry (or replace it with the intended
shared branch names like main/staging if needed) so that CI no longer triggers
on your personal feature branch.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts (1)

105-215: Extract shared logic into a private helper to eliminate duplication.

getTradeHistory and getTradeHistoryForParentSubaccount share identical logic for: clobPairId resolution, fill-count guard, buildOrderTypeMap, buildClobPairIdToMarket, computeTradeHistory, paginateTradeHistory, and response assembly. Only fill retrieval and subaccountIdToNumber construction differ. Consider extracting the common tail into a private method:

♻️ Proposed refactor
+  private async buildTradeHistoryResponse(
+    fills: FillFromDatabase[],
+    subaccountIdToNumber: Record<string, number>,
+    limit: number | undefined,
+    page: number | undefined,
+  ): Promise<TradeHistoryResponse> {
+    if (fills.length > TRADE_HISTORY_MAX_FILLS) {
+      throw new BadRequestError(
+        `Too many fills (${fills.length}) to compute trade history. Please filter by market.`,
+      );
+    }
+    const orderTypeMap = await buildOrderTypeMap(fills);
+    const clobPairIdToMarket = buildClobPairIdToMarket();
+    const allRows = computeTradeHistory(fills, orderTypeMap, clobPairIdToMarket, subaccountIdToNumber);
+    const effectiveLimit = limit ?? config.API_LIMIT_V4;
+    const paginated = paginateTradeHistory(allRows, effectiveLimit, page);
+    return {
+      tradeHistory: paginated.tradeHistory,
+      pageSize: paginated.pageSize,
+      totalResults: paginated.totalResults,
+      offset: paginated.offset,
+    };
+  }

Then simplify both public methods to call this.buildTradeHistoryResponse(fills, subaccountIdToNumber, limit, page).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts`
around lines 105 - 215, Both controllers duplicate the tail logic (clobPairId
resolution aside): after retrieving fills they run the same guard,
buildOrderTypeMap, buildClobPairIdToMarket, computeTradeHistory,
paginateTradeHistory and assemble the response; extract that into a private
helper (e.g. private async buildTradeHistoryResponse(fills: Fill[],
subaccountIdToNumber: Record<string,number>, limit?: number, page?: number):
Promise<TradeHistoryResponse>) and have getTradeHistory and
getTradeHistoryForParentSubaccount call it. Move the fills.length >
TRADE_HISTORY_MAX_FILLS guard, calls to buildOrderTypeMap,
buildClobPairIdToMarket, computeTradeHistory, paginateTradeHistory and the
response construction into that helper so only FillTable retrieval and
subaccountId→number mapping remain in each public method.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@indexer/services/comlink/public/swagger.json`:
- Around line 1466-1469: The schema property "subaccountNumber" in this swagger
fragment is declared as "type": "number", "format": "double" but must match the
other occurrences (e.g., PerpetualPositionResponseObject, FillResponseObject,
SubaccountResponseObject) which use "type": "integer", "format": "int32"; change
this property's declaration to "type": "integer" and "format": "int32" so SDK
generators produce an integer field and keep the spec consistent.

In `@indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts`:
- Around line 133-137: Replace the plain Error thrown when fills exceed
TRADE_HISTORY_MAX_FILLS with a client-error type (e.g., BadRequestError) so
handleControllerError maps it to HTTP 400; change the throw in the trade history
check that references fills and TRADE_HISTORY_MAX_FILLS to throw new
BadRequestError(`Too many fills (${fills.length}) to compute trade history.
Please filter by market.`) and add/import BadRequestError from your error
utilities (or introduce a BadRequestError class consistent with project errors).
Apply the identical replacement for the second occurrence mentioned around the
other check (line ~186) so both user-correctable conditions return 400 instead
of 500.

In `@indexer/services/comlink/src/lib/trade-history.ts`:
- Line 75: The code silently maps missing subaccount IDs to subaccount 0 via
"const subaccountNumber = subaccountIdToNumber[subaccountId] ?? 0", which can
misattribute rows; instead detect when subaccountIdToNumber[subaccountId] is
undefined and either throw an error or skip the group. Replace the
nullish-coalescing fallback with an explicit check: if
subaccountIdToNumber[subaccountId] is undefined, either throw a descriptive
error (e.g., including subaccountId) or continue to skip processing that group;
keep references to the same symbols (subaccountIdToNumber and subaccountNumber)
so the intent is clear.

---

Duplicate comments:
In `@indexer/services/comlink/public/api-documentation.md`:
- Around line 3665-3668: Remove the stray blank line inside the blockquote that
triggers MD028 by editing the block containing the lines "Example responses" and
"200 Response" so there are no empty lines between the blockquote markers and
its content; locate the block by searching for the exact strings "Example
responses" and "200 Response" and collapse/remove the inner blank line so the
blockquote is contiguous.
- Around line 3775-3778: There is a blank line inside a blockquote near the
"Example responses" / "200 Response" block causing MD028; edit the blockquote so
there are no empty lines between consecutive '>' lines (remove the stray blank
line(s) following the blockquote marker) so the blockquote content is continuous
and the "Example responses" and "200 Response" lines are directly adjacent to
other quoted lines.

In `@indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts`:
- Around line 124-131: The DB query does not cap results, so add a server-side
limit to the FillTable.findAll calls: include limit: TRADE_HISTORY_MAX_FILLS + 1
in the options object passed to FillTable.findAll (the same options object that
currently contains orderBy: fillOrderBy) so the database stops at the threshold;
apply the identical change to both occurrences of FillTable.findAll referenced
in this diff.

In `@indexer/services/comlink/src/lib/trade-history.ts`:
- Around line 99-126: The function paginateTradeHistory currently paginates over
the full in-memory rows array; to avoid unbounded memory/CPU use, enforce a hard
cap: define a MAX_FETCH_ROWS constant (e.g. 10_000), clamp limit to at most
MAX_FETCH_ROWS, create effectiveRows = rows.slice(0, MAX_FETCH_ROWS) and use
effectiveRows for slicing and page math (tradeHistory, offset, pageSize,
totalResults = effectiveRows.length); update paginateTradeHistory to reference
effectiveRows instead of rows and ensure page is clamped to >=1.

---

Nitpick comments:
In `@indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts`:
- Around line 105-215: Both controllers duplicate the tail logic (clobPairId
resolution aside): after retrieving fills they run the same guard,
buildOrderTypeMap, buildClobPairIdToMarket, computeTradeHistory,
paginateTradeHistory and assemble the response; extract that into a private
helper (e.g. private async buildTradeHistoryResponse(fills: Fill[],
subaccountIdToNumber: Record<string,number>, limit?: number, page?: number):
Promise<TradeHistoryResponse>) and have getTradeHistory and
getTradeHistoryForParentSubaccount call it. Move the fills.length >
TRADE_HISTORY_MAX_FILLS guard, calls to buildOrderTypeMap,
buildClobPairIdToMarket, computeTradeHistory, paginateTradeHistory and the
response construction into that helper so only FillTable retrieval and
subaccountId→number mapping remain in each public method.

@linear
Copy link

linear bot commented Feb 17, 2026

@davidli1997
Copy link
Contributor Author

cc @dwjanus

@davidli1997
Copy link
Contributor Author

https://github.com/Mergifyio backport release/indexer/v9.x

@mergify
Copy link
Contributor

mergify bot commented Mar 3, 2026

backport release/indexer/v9.x

✅ Backports have been created

Details

Cherry-pick of 59cd776 has failed:

On branch mergify/bp/release/indexer/v9.x/pr-3323
Your branch is up to date with 'origin/release/indexer/v9.x'.

You are currently cherry-picking commit 59cd776b.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	new file:   indexer/services/comlink/__tests__/controllers/api/v4/trade-history-controller.test.ts
	modified:   indexer/services/comlink/__tests__/helpers/helpers.ts
	new file:   indexer/services/comlink/__tests__/lib/trade-history.test.ts
	modified:   indexer/services/comlink/public/api-documentation.md
	modified:   indexer/services/comlink/public/swagger.json
	modified:   indexer/services/comlink/src/controllers/api/index-v4.ts
	new file:   indexer/services/comlink/src/controllers/api/v4/trade-history-controller.ts
	modified:   indexer/services/comlink/src/lib/helpers.ts
	new file:   indexer/services/comlink/src/lib/trade-history.ts
	modified:   indexer/services/comlink/src/types.ts

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   indexer/services/comlink/src/controllers/api/v4/fills-controller.ts

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

2 participants