Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/account/controllers/accounts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -198,6 +205,44 @@ export class AccountsController {
});
}

@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)
@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 useRangeBasedPnl = includeFields.includes('pnl-range');

return await this.portfolioService.getPortfolioHistory(address, {
startDate: start,
endDate: end,
interval: finalInterval,
convertTo: query.convertTo || 'ae',
includePnl: true,
useRangeBasedPnl,
includeTokensPnl: true,
});
}

// Portfolio PnL sparkline — MUST come before :address route to avoid route conflict
@ApiOperation({
operationId: 'getPortfolioPnlChart',
Expand Down
5 changes: 4 additions & 1 deletion src/account/dto/get-portfolio-history-query.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion src/account/dto/portfolio-history-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
7 changes: 5 additions & 2 deletions src/account/services/portfolio.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -141,6 +142,7 @@ export class PortfolioService {
interval = 86400, // Default daily (24 hours)
includePnl = false,
useRangeBasedPnl = false,
includeTokensPnl = false,
} = options;

// Calculate date range
Expand Down Expand Up @@ -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;
Expand Down