From 3c9199b0bbefd22f8f6f61bcf2e7bc132cf96dd7 Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Thu, 2 Apr 2026 09:32:11 +0200 Subject: [PATCH 1/2] feat(accounts): add getTokensPnlHistory endpoint to retrieve token PnL history with optional date range and interval; enhance portfolio service to support per-token PnL breakdown --- .../controllers/accounts.controller.ts | 34 +++++++++++++++++++ src/account/services/portfolio.service.ts | 7 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/account/controllers/accounts.controller.ts b/src/account/controllers/accounts.controller.ts index f6da37aa..094d323a 100644 --- a/src/account/controllers/accounts.controller.ts +++ b/src/account/controllers/accounts.controller.ts @@ -198,6 +198,40 @@ export class AccountsController { }); } + @ApiOperation({ operationId: 'getTokensPnlHistory' }) + @ApiParam({ name: 'address', type: 'string', description: 'Account address' }) + @ApiOkResponse({ type: [PortfolioHistorySnapshotDto] }) + @CacheTTL(60 * 10) + @Get(':address/portfolio/tokens/history') + async getTokensPnlHistory( + @Param('address') address: string, + @Query() query: GetPortfolioHistoryQueryDto, + ) { + const start = query.startDate ? moment(query.startDate) : undefined; + const end = query.endDate ? moment(query.endDate) : undefined; + const includeFields = query.include + ? query.include.split(',').map((f) => f.trim()) + : []; + + const minimumInterval = this.getMinimumInterval(start, end); + const requestedInterval = query.interval || 86400; + const finalInterval = Math.max(requestedInterval, minimumInterval); + + const includePnl = + includeFields.includes('pnl') || includeFields.includes('pnl-range'); + const useRangeBasedPnl = includeFields.includes('pnl-range'); + + return await this.portfolioService.getPortfolioHistory(address, { + startDate: start, + endDate: end, + interval: finalInterval, + convertTo: query.convertTo || 'ae', + includePnl, + useRangeBasedPnl, + includeTokensPnl: true, + }); + } + // Portfolio PnL sparkline — MUST come before :address route to avoid route conflict @ApiOperation({ operationId: 'getPortfolioPnlChart', diff --git a/src/account/services/portfolio.service.ts b/src/account/services/portfolio.service.ts index 2dba7b35..48763c38 100644 --- a/src/account/services/portfolio.service.ts +++ b/src/account/services/portfolio.service.ts @@ -89,6 +89,7 @@ export interface GetPortfolioHistoryOptions { | 'xau'; includePnl?: boolean; // Whether to include PNL data useRangeBasedPnl?: boolean; // If true, calculate PNL for range between timestamps; if false, use all previous transactions + includeTokensPnl?: boolean; // Whether to include per-token PNL breakdown (large payload) } export interface PnlDataPoint { @@ -141,6 +142,7 @@ export class PortfolioService { interval = 86400, // Default daily (24 hours) includePnl = false, useRangeBasedPnl = false, + includeTokensPnl = false, } = options; // Calculate date range @@ -379,8 +381,9 @@ export class PortfolioService { }; } - // Include individual token PNL data (use range-based if available, otherwise cumulative) - result.tokens_pnl = pnlData.pnls; + if (includeTokensPnl) { + result.tokens_pnl = pnlData.pnls; + } } return result; From c09710f3ffe39d6bc7bb1461646c2a95787586dc Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Thu, 2 Apr 2026 09:41:21 +0200 Subject: [PATCH 2/2] feat(accounts): enhance portfolio history endpoints with detailed summaries and descriptions; update query DTO for clarity on included fields --- .../controllers/accounts.controller.ts | 21 ++++++++++++++----- .../dto/get-portfolio-history-query.dto.ts | 5 ++++- .../dto/portfolio-history-response.dto.ts | 3 ++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/account/controllers/accounts.controller.ts b/src/account/controllers/accounts.controller.ts index 094d323a..9b76eaa5 100644 --- a/src/account/controllers/accounts.controller.ts +++ b/src/account/controllers/accounts.controller.ts @@ -161,7 +161,14 @@ export class AccountsController { } // Portfolio history endpoint - MUST come before :address route to avoid route conflict - @ApiOperation({ operationId: 'getPortfolioHistory' }) + @ApiOperation({ + operationId: 'getPortfolioHistory', + summary: 'Portfolio value history snapshots', + description: + 'Returns portfolio value over time (AE balance, token values, total value). ' + + 'Pass include=pnl or include=pnl-range to add aggregate total_pnl data. ' + + 'Per-token PnL breakdown (tokens_pnl) is available via the dedicated :address/portfolio/tokens/history endpoint.', + }) @ApiParam({ name: 'address', type: 'string', description: 'Account address' }) @ApiOkResponse({ type: [PortfolioHistorySnapshotDto] }) @CacheTTL(60 * 10) // 10 minutes @@ -198,7 +205,13 @@ export class AccountsController { }); } - @ApiOperation({ operationId: 'getTokensPnlHistory' }) + @ApiOperation({ + operationId: 'getTokensPnlHistory', + summary: 'Per-token PnL history snapshots', + description: + 'Returns portfolio history snapshots that include the per-token PnL breakdown (tokens_pnl). ' + + 'PnL data is always included; pass include=pnl-range to use range-based (daily window) PnL instead of cumulative.', + }) @ApiParam({ name: 'address', type: 'string', description: 'Account address' }) @ApiOkResponse({ type: [PortfolioHistorySnapshotDto] }) @CacheTTL(60 * 10) @@ -217,8 +230,6 @@ export class AccountsController { const requestedInterval = query.interval || 86400; const finalInterval = Math.max(requestedInterval, minimumInterval); - const includePnl = - includeFields.includes('pnl') || includeFields.includes('pnl-range'); const useRangeBasedPnl = includeFields.includes('pnl-range'); return await this.portfolioService.getPortfolioHistory(address, { @@ -226,7 +237,7 @@ export class AccountsController { endDate: end, interval: finalInterval, convertTo: query.convertTo || 'ae', - includePnl, + includePnl: true, useRangeBasedPnl, includeTokensPnl: true, }); diff --git a/src/account/dto/get-portfolio-history-query.dto.ts b/src/account/dto/get-portfolio-history-query.dto.ts index 008ba6a8..db0bdc57 100644 --- a/src/account/dto/get-portfolio-history-query.dto.ts +++ b/src/account/dto/get-portfolio-history-query.dto.ts @@ -62,7 +62,10 @@ export class GetPortfolioHistoryQueryDto { name: 'include', type: 'string', required: false, - description: 'Comma-separated list of fields to include (e.g., "pnl")', + description: + 'Comma-separated list of fields to include. ' + + '"pnl" adds aggregate total_pnl, "pnl-range" uses daily-window PnL instead of cumulative. ' + + 'Per-token breakdown (tokens_pnl) is only available via the /portfolio/tokens/history endpoint.', example: 'pnl', }) @IsOptional() diff --git a/src/account/dto/portfolio-history-response.dto.ts b/src/account/dto/portfolio-history-response.dto.ts index 287578de..fffde21e 100644 --- a/src/account/dto/portfolio-history-response.dto.ts +++ b/src/account/dto/portfolio-history-response.dto.ts @@ -73,7 +73,8 @@ export class PortfolioHistorySnapshotDto { @ApiProperty({ description: - 'PNL breakdown per token (included if requested), keyed by token sale_address', + 'PNL breakdown per token, keyed by token sale_address. ' + + 'Only returned by the dedicated /portfolio/tokens/history endpoint.', type: Object, required: false, })