From 4a618351898edb41654ce9959e8018d996ae4452 Mon Sep 17 00:00:00 2001 From: sadeeq6400 Date: Sun, 19 Jul 2026 16:05:10 +0100 Subject: [PATCH] Standardize DTO validation: enable global ValidationPipe and annotate DTOs with class-validator --- src/core/auth/dto/kyc.dto.ts | 22 +-- src/core/auth/dto/recover-wallet.dto.ts | 14 +- src/core/auth/dto/request-recovery.dto.ts | 10 +- src/investment/portfolio/dto/backtest.dto.ts | 39 ++++- .../portfolio/dto/optimization.dto.ts | 34 ++++- .../portfolio/dto/portfolio-asset.dto.ts | 47 +++++- .../portfolio/dto/portfolio-management.dto.ts | 142 ------------------ src/investment/portfolio/dto/portfolio.dto.ts | 122 ++++++++------- .../portfolio/dto/rebalancing.dto.ts | 35 ++++- .../portfolio/dto/risk-profile.dto.ts | 50 +++++- .../portfolio-management.controller.ts | 21 ++- .../portfolio/services/portfolio.service.ts | 21 +-- .../risk-management/dto/risk.dto.ts | 35 ++++- 13 files changed, 311 insertions(+), 281 deletions(-) delete mode 100644 src/investment/portfolio/dto/portfolio-management.dto.ts diff --git a/src/core/auth/dto/kyc.dto.ts b/src/core/auth/dto/kyc.dto.ts index 2adae00..af09295 100644 --- a/src/core/auth/dto/kyc.dto.ts +++ b/src/core/auth/dto/kyc.dto.ts @@ -1,4 +1,5 @@ -import { IsString, IsNotEmpty, IsOptional } from "class-validator"; +import { IsString, IsNotEmpty } from "class-validator"; +import { RefreshTokenDto, TwoFactorVerifyDto } from "./auth.dto"; export class TwoFactorSetupDto { @IsString() @@ -6,21 +7,4 @@ export class TwoFactorSetupDto { type: "totp" | "sms" | "email"; } -export class TwoFactorVerifyDto { - @IsString() - @IsNotEmpty() - code: string; - - @IsOptional() - @IsString() - backupCode?: string; -} - -export class RefreshTokenDto { - @IsString() - @IsNotEmpty() - refreshToken: string; -} - - - +export { TwoFactorVerifyDto, RefreshTokenDto }; \ No newline at end of file diff --git a/src/core/auth/dto/recover-wallet.dto.ts b/src/core/auth/dto/recover-wallet.dto.ts index e3a1a17..8bff263 100644 --- a/src/core/auth/dto/recover-wallet.dto.ts +++ b/src/core/auth/dto/recover-wallet.dto.ts @@ -1,13 +1,19 @@ import { IsEmail, IsString, Length } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; export class RecoverWalletDto { + @ApiProperty({ + description: "The user's email address.", + example: "user@example.com", + }) @IsEmail() email: string; + @ApiProperty({ + description: "The recovery token from the email.", + example: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", + }) @IsString() @Length(64, 64) recoveryToken: string; -} - - - +} \ No newline at end of file diff --git a/src/core/auth/dto/request-recovery.dto.ts b/src/core/auth/dto/request-recovery.dto.ts index 29689d7..f6fe78c 100644 --- a/src/core/auth/dto/request-recovery.dto.ts +++ b/src/core/auth/dto/request-recovery.dto.ts @@ -1,9 +1,11 @@ import { IsEmail } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; export class RequestRecoveryDto { + @ApiProperty({ + description: "The email address to send the recovery link to.", + example: "user@example.com", + }) @IsEmail() email: string; -} - - - +} \ No newline at end of file diff --git a/src/investment/portfolio/dto/backtest.dto.ts b/src/investment/portfolio/dto/backtest.dto.ts index 286dc22..fb975b7 100644 --- a/src/investment/portfolio/dto/backtest.dto.ts +++ b/src/investment/portfolio/dto/backtest.dto.ts @@ -3,68 +3,97 @@ import { IsOptional, IsNumber, IsDateString, - IsEnum, IsArray, } from "class-validator"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { BacktestStatus } from "../entities/backtest-result.entity"; export class CreateBacktestDto { + @ApiProperty({ description: "Name of the backtest", example: "My Strategy Backtest" }) @IsString() name: string; + @ApiPropertyOptional({ description: "Optional description of the backtest" }) @IsOptional() @IsString() description?: string; + @ApiProperty({ description: "Start date of the backtest", example: "2022-01-01" }) @IsDateString() startDate: string; + @ApiProperty({ description: "End date of the backtest", example: "2023-01-01" }) @IsDateString() endDate: string; + @ApiProperty({ description: "Initial capital for the backtest", example: 10000 }) @IsNumber() initialCapital: number; + @ApiProperty({ description: "The strategy to backtest", example: "Momentum" }) @IsString() strategy: string; + @ApiProperty({ description: "Assets and their weights for the backtest", example: [{ ticker: "BTC", weight: 1 }] }) @IsArray() assets: Array<{ ticker: string; weight: number }>; + @ApiPropertyOptional({ description: "Benchmark ticker for comparison", example: "SPY" }) @IsOptional() @IsString() benchmarkTicker?: string; + @ApiPropertyOptional({ description: "Rebalancing frequency in months", example: 1 }) @IsOptional() @IsNumber() rebalanceFrequency?: number; // months } export class BacktestResultResponseDto { + @ApiProperty({ description: "Unique identifier of the backtest result" }) id: string; + @ApiProperty({ description: "Name of the backtest" }) name: string; + @ApiPropertyOptional({ description: "Optional description of the backtest" }) description?: string; + @ApiProperty({ description: "Status of the backtest", enum: BacktestStatus }) status: BacktestStatus; + @ApiProperty({ description: "Start date of the backtest" }) startDate: Date; + @ApiProperty({ description: "End date of the backtest" }) endDate: Date; + @ApiProperty({ description: "Initial capital for the backtest" }) initialCapital: number; + @ApiPropertyOptional({ description: "Final value of the portfolio" }) finalValue?: number; + @ApiPropertyOptional({ description: "Total return of the backtest" }) totalReturn?: number; + @ApiPropertyOptional({ description: "Annualized return of the backtest" }) annualizedReturn?: number; + @ApiPropertyOptional({ description: "Volatility of the backtest" }) volatility?: number; + @ApiPropertyOptional({ description: "Sharpe ratio of the backtest" }) sharpeRatio?: number; + @ApiPropertyOptional({ description: "Sortino ratio of the backtest" }) sortinoRatio?: number; + @ApiPropertyOptional({ description: "Maximum drawdown of the backtest" }) maxDrawdown?: number; + @ApiPropertyOptional({ description: "Return of the benchmark" }) benchmarkReturn?: number; + @ApiPropertyOptional({ description: "Alpha of the backtest" }) alpha?: number; + @ApiPropertyOptional({ description: "Beta of the backtest" }) beta?: number; + @ApiPropertyOptional({ description: "Correlation of the backtest with the benchmark" }) Correlation?: number; + @ApiPropertyOptional({ description: "Total number of trades in the backtest" }) totalTrades?: number; + @ApiPropertyOptional({ description: "Win rate of the trades in the backtest" }) winRate?: number; + @ApiPropertyOptional({ description: "Profit factor of the backtest" }) profitFactor?: number; + @ApiProperty({ description: "Date of backtest creation" }) createdAt: Date; + @ApiPropertyOptional({ description: "Date of backtest completion" }) completedAt?: Date; -} - - - +} \ No newline at end of file diff --git a/src/investment/portfolio/dto/optimization.dto.ts b/src/investment/portfolio/dto/optimization.dto.ts index 5a7dd25..b3bffd6 100644 --- a/src/investment/portfolio/dto/optimization.dto.ts +++ b/src/investment/portfolio/dto/optimization.dto.ts @@ -5,83 +5,107 @@ import { IsNumber, IsArray, IsJSON, - IsDateString, } from "class-validator"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { OptimizationMethod, OptimizationStatus, } from "../entities/optimization-history.entity"; export class CreateOptimizationDto { + @ApiProperty({ description: "The optimization method to use", enum: OptimizationMethod }) @IsEnum(OptimizationMethod) method: OptimizationMethod; + @ApiProperty({ description: "ID of the portfolio to optimize" }) @IsString() portfolioId: string; + @ApiPropertyOptional({ description: "Parameters for the optimization method" }) @IsOptional() @IsJSON() parameters?: Record; + @ApiPropertyOptional({ description: "ID of the risk profile to use for optimization" }) @IsOptional() @IsString() riskProfileId?: string; + @ApiPropertyOptional({ description: "Target return for the optimization" }) @IsOptional() @IsNumber() targetReturn?: number; + @ApiPropertyOptional({ description: "Maximum volatility for the optimization" }) @IsOptional() @IsNumber() maxVolatility?: number; + @ApiPropertyOptional({ description: "Constraints for the optimization" }) @IsOptional() @IsArray() constraints?: Array<{ asset: string; min: number; max: number }>; } export class ApproveOptimizationDto { + @ApiProperty({ description: "ID of the optimization to approve" }) @IsString() optimizationId: string; + @ApiPropertyOptional({ description: "Optional notes for the approval" }) @IsOptional() @IsString() notes?: string; } export class RejectOptimizationDto { + @ApiProperty({ description: "ID of the optimization to reject" }) @IsString() optimizationId: string; + @ApiProperty({ description: "Reason for rejecting the optimization" }) @IsString() rejectionReason: string; } export class ImplementOptimizationDto { + @ApiProperty({ description: "ID of the optimization to implement" }) @IsString() optimizationId: string; + @ApiPropertyOptional({ description: "Optional notes for the implementation" }) @IsOptional() @IsString() executionNotes?: string; } export class OptimizationHistoryResponseDto { + @ApiProperty({ description: "Unique identifier of the optimization history record" }) id: string; + @ApiProperty({ description: "The optimization method used", enum: OptimizationMethod }) method: OptimizationMethod; + @ApiProperty({ description: "Status of the optimization", enum: OptimizationStatus }) status: OptimizationStatus; + @ApiProperty({ description: "Suggested asset allocation from the optimization" }) suggestedAllocation: Record; + @ApiPropertyOptional({ description: "Expected return of the optimized portfolio" }) expectedReturn?: number; + @ApiPropertyOptional({ description: "Expected volatility of the optimized portfolio" }) expectedVolatility?: number; + @ApiPropertyOptional({ description: "Expected Sharpe ratio of the optimized portfolio" }) expectedSharpeRatio?: number; + @ApiPropertyOptional({ description: "Value at Risk (VaR) of the optimized portfolio" }) valueAtRisk?: number; + @ApiPropertyOptional({ description: "Maximum drawdown of the optimized portfolio" }) maxDrawdown?: number; + @ApiPropertyOptional({ description: "Improvement score of the optimization" }) improvementScore?: number; + @ApiPropertyOptional({ description: "Backtested metrics of the optimized portfolio" }) backtestedMetrics?: Record; + @ApiProperty({ description: "Date of optimization creation" }) createdAt: Date; + @ApiPropertyOptional({ description: "Date of optimization completion" }) completedAt?: Date; + @ApiPropertyOptional({ description: "Date of optimization implementation" }) implementedAt?: Date; -} - - - +} \ No newline at end of file diff --git a/src/investment/portfolio/dto/portfolio-asset.dto.ts b/src/investment/portfolio/dto/portfolio-asset.dto.ts index 0fb2892..646360a 100644 --- a/src/investment/portfolio/dto/portfolio-asset.dto.ts +++ b/src/investment/portfolio/dto/portfolio-asset.dto.ts @@ -6,127 +6,166 @@ import { Length, IsBoolean, } from "class-validator"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Chain, AssetType } from "../entities/portfolio-asset.entity"; export class PortfolioAssetDto { + @ApiProperty({ description: "Ticker symbol of the asset", example: "BTC" }) @IsString() @Length(3, 10) ticker: string; + @ApiProperty({ description: "Name of the asset", example: "Bitcoin" }) @IsString() name: string; + @ApiProperty({ description: "Blockchain network of the asset", enum: Chain }) @IsEnum(Chain) chain: Chain; + @ApiPropertyOptional({ description: "Type of the asset", enum: AssetType }) @IsOptional() @IsEnum(AssetType) type?: AssetType; + @ApiPropertyOptional({ description: "Quantity of the asset held", example: 1.5 }) @IsOptional() @IsNumber() quantity?: number; + @ApiPropertyOptional({ description: "Current price of the asset", example: 60000 }) @IsOptional() @IsNumber() currentPrice?: number; + @ApiPropertyOptional({ description: "Cost basis of the asset", example: 50000 }) @IsOptional() @IsNumber() costBasis?: number; } export class AddAssetToPortfolioDto { + @ApiProperty({ description: "Ticker symbol of the asset", example: "BTC" }) @IsString() ticker: string; + @ApiProperty({ description: "Name of the asset", example: "Bitcoin" }) @IsString() name: string; + @ApiProperty({ description: "Quantity of the asset to add", example: 1.5 }) @IsNumber() quantity: number; + @ApiPropertyOptional({ description: "Current price of the asset", example: 60000 }) @IsOptional() @IsNumber() currentPrice?: number; + @ApiPropertyOptional({ description: "Cost basis of the asset", example: 50000 }) @IsOptional() @IsNumber() costBasis?: number; } export class ConstraintOverrideDto { + @ApiPropertyOptional({ description: "Override constraints for this operation" }) @IsOptional() @IsBoolean() overrideConstraints?: boolean; + @ApiPropertyOptional({ description: "Reason for overriding constraints" }) @IsOptional() @IsString() overrideReason?: string; + @ApiPropertyOptional({ description: "User who acknowledged the override" }) @IsOptional() @IsString() acknowledgedBy?: string; } export class AddHoldingDto extends ConstraintOverrideDto { + @ApiProperty({ description: "Ticker symbol of the asset", example: "BTC" }) @IsString() @Length(3, 10) ticker: string; + @ApiProperty({ description: "Name of the asset", example: "Bitcoin" }) @IsString() name: string; + @ApiProperty({ description: "Blockchain network of the asset", enum: Chain }) @IsEnum(Chain) chain: Chain; + @ApiPropertyOptional({ description: "Type of the asset", enum: AssetType }) @IsOptional() @IsEnum(AssetType) type?: AssetType; + @ApiProperty({ description: "Quantity of the asset to add", example: 1.5 }) @IsNumber() quantity: number; + @ApiPropertyOptional({ description: "Current price of the asset", example: 60000 }) @IsOptional() @IsNumber() currentPrice?: number; + @ApiProperty({ description: "Cost basis of the asset", example: 50000 }) @IsNumber() costBasis: number; } export class UpdateHoldingDto extends ConstraintOverrideDto { + @ApiPropertyOptional({ description: "New quantity of the asset" }) @IsOptional() @IsNumber() quantity?: number; + @ApiPropertyOptional({ description: "New current price of the asset" }) @IsOptional() @IsNumber() currentPrice?: number; + @ApiPropertyOptional({ description: "New cost basis of the asset" }) @IsOptional() @IsNumber() costBasis?: number; } export class PortfolioAssetResponseDto { + @ApiProperty({ description: "Unique identifier of the portfolio asset" }) id: string; + @ApiProperty({ description: "Ticker symbol of the asset" }) ticker: string; + @ApiProperty({ description: "Name of the asset" }) name: string; + @ApiProperty({ description: "Blockchain network of the asset", enum: Chain }) chain: Chain; + @ApiProperty({ description: "Type of the asset", enum: AssetType }) type: AssetType; + @ApiProperty({ description: "Quantity of the asset held" }) quantity: number; + @ApiPropertyOptional({ description: "Current price of the asset" }) currentPrice?: number; + @ApiProperty({ description: "Total value of the asset holding" }) value: number; + @ApiProperty({ description: "Allocation percentage of the asset in the portfolio" }) allocationPercentage: number; + @ApiPropertyOptional({ description: "Suggested allocation percentage for the asset" }) suggestedAllocation?: number; + @ApiPropertyOptional({ description: "Expected return of the asset" }) expectedReturn?: number; + @ApiPropertyOptional({ description: "Volatility of the asset" }) volatility?: number; + @ApiPropertyOptional({ description: "Beta of the asset" }) beta?: number; + @ApiPropertyOptional({ description: "Cost basis of the asset" }) costBasis?: number; + @ApiPropertyOptional({ description: "Unrealized gain of the asset" }) unrealizedGain?: number; + @ApiProperty({ description: "Date of last update" }) updatedAt: Date; -} - - - +} \ No newline at end of file diff --git a/src/investment/portfolio/dto/portfolio-management.dto.ts b/src/investment/portfolio/dto/portfolio-management.dto.ts deleted file mode 100644 index 9ce629e..0000000 --- a/src/investment/portfolio/dto/portfolio-management.dto.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { - IsBoolean, - IsEnum, - IsNumber, - IsOptional, - IsString, - IsJSON, - IsPositive, -} from "class-validator"; -import { PortfolioStatus } from "../entities/portfolio.entity"; - -export class CreatePortfolioRequestDto { - @ApiProperty({ example: "Retirement Growth" }) - @IsString() - name: string; - - @ApiPropertyOptional({ example: "Diversified long-term portfolio" }) - @IsOptional() - @IsString() - description?: string; - - @ApiPropertyOptional({ example: 10000 }) - @IsOptional() - @IsNumber() - totalValue?: number; - - @ApiPropertyOptional({ example: { strategy: "balanced" } }) - @IsOptional() - @IsJSON() - metadata?: Record; - - @ApiPropertyOptional({ example: true }) - @IsOptional() - @IsBoolean() - autoRebalanceEnabled?: boolean; - - @ApiPropertyOptional({ - example: "monthly", - enum: ["daily", "weekly", "monthly", "quarterly"], - }) - @IsOptional() - @IsString() - rebalanceFrequency?: "daily" | "weekly" | "monthly" | "quarterly"; - - @ApiPropertyOptional({ example: 5, minimum: 0 }) - @IsOptional() - @IsNumber() - rebalanceThreshold?: number; -} - -export class UpdatePortfolioRequestDto { - @ApiPropertyOptional({ example: "Retirement Growth" }) - @IsOptional() - @IsString() - name?: string; - - @ApiPropertyOptional({ example: "Diversified long-term portfolio" }) - @IsOptional() - @IsString() - description?: string; - - @ApiPropertyOptional({ - example: "active", - enum: ["active", "inactive", "archived"], - }) - @IsOptional() - @IsEnum(PortfolioStatus) - status?: PortfolioStatus; - - @ApiPropertyOptional({ example: true }) - @IsOptional() - @IsBoolean() - autoRebalanceEnabled?: boolean; - - @ApiPropertyOptional({ - example: "monthly", - enum: ["daily", "weekly", "monthly", "quarterly"], - }) - @IsOptional() - @IsString() - rebalanceFrequency?: "daily" | "weekly" | "monthly" | "quarterly"; - - @ApiPropertyOptional({ example: 5, minimum: 0 }) - @IsOptional() - @IsNumber() - rebalanceThreshold?: number; - - @ApiPropertyOptional({ example: { strategy: "balanced" } }) - @IsOptional() - @IsJSON() - metadata?: Record; -} - -export class PortfolioResponseDto { - @ApiProperty({ example: "d9e6c8d0-5f9c-4bb1-8db2-7d3b0d0a1d1f" }) - id: string; - - @ApiProperty({ example: "Retirement Growth" }) - name: string; - - @ApiPropertyOptional({ example: "Diversified long-term portfolio" }) - description?: string; - - @ApiProperty({ example: "active", enum: ["active", "inactive", "archived"] }) - status: PortfolioStatus; - - @ApiProperty({ example: 10000 }) - totalValue: number; - - @ApiProperty({ example: { AAPL: 50, MSFT: 50 } }) - currentAllocation: Record; - - @ApiPropertyOptional({ example: { AAPL: 60, MSFT: 40 } }) - targetAllocation?: Record; - - @ApiProperty({ example: true }) - autoRebalanceEnabled: boolean; - - @ApiPropertyOptional({ example: "monthly" }) - rebalanceFrequency?: string; - - @ApiProperty({ example: 5 }) - rebalanceThreshold: number; - - @ApiPropertyOptional({ example: "2026-06-20T00:00:00.000Z" }) - lastRebalanceDate?: Date; - - @ApiProperty({ example: "2026-06-20T00:00:00.000Z" }) - createdAt: Date; - - @ApiProperty({ example: "2026-06-20T00:00:00.000Z" }) - updatedAt: Date; -} - -export class PortfolioListResponseDto { - @ApiProperty({ type: [PortfolioResponseDto] }) - portfolios: PortfolioResponseDto[]; -} - - - diff --git a/src/investment/portfolio/dto/portfolio.dto.ts b/src/investment/portfolio/dto/portfolio.dto.ts index e96ef09..0522e87 100644 --- a/src/investment/portfolio/dto/portfolio.dto.ts +++ b/src/investment/portfolio/dto/portfolio.dto.ts @@ -1,133 +1,143 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { - IsString, - IsOptional, + IsBoolean, IsEnum, IsNumber, - IsBoolean, + IsOptional, + IsString, IsJSON, - IsObject, - Min, - Max, } from "class-validator"; -import { PortfolioType, PortfolioStatus, AllocationStrategy } from "../entities/portfolio.entity"; +import { PortfolioStatus } from "../entities/portfolio.entity"; + export class CreatePortfolioDto { + @ApiProperty({ example: "Retirement Growth" }) @IsString() name: string; - @IsOptional() - @IsEnum(PortfolioType) - type?: PortfolioType; - + @ApiPropertyOptional({ example: "Diversified long-term portfolio" }) @IsOptional() @IsString() description?: string; - @IsOptional() - @IsObject() - initialAllocation?: Record; - - @IsOptional() - @IsObject() - targetAllocation?: Record; - - @IsOptional() - @IsEnum(AllocationStrategy) - allocationStrategy?: AllocationStrategy; - + @ApiPropertyOptional({ example: 10000 }) @IsOptional() @IsNumber() - @Min(0) totalValue?: number; + @ApiPropertyOptional({ example: { strategy: "balanced" } }) @IsOptional() - @IsObject() + @IsJSON() metadata?: Record; + @ApiPropertyOptional({ example: true }) @IsOptional() @IsBoolean() autoRebalanceEnabled?: boolean; + @ApiPropertyOptional({ + example: "monthly", + enum: ["daily", "weekly", "monthly", "quarterly"], + }) @IsOptional() @IsString() rebalanceFrequency?: "daily" | "weekly" | "monthly" | "quarterly"; + @ApiPropertyOptional({ example: 5, minimum: 0 }) @IsOptional() @IsNumber() - @Min(0) - @Max(100) rebalanceThreshold?: number; + + @ApiPropertyOptional({ example: { "BTC": 50, "ETH": 50 } }) + @IsOptional() + @IsJSON() + initialAllocation?: Record; } export class UpdatePortfolioDto { + @ApiPropertyOptional({ example: "Retirement Growth" }) @IsOptional() @IsString() name?: string; - @IsOptional() - @IsEnum(PortfolioType) - type?: PortfolioType; - + @ApiPropertyOptional({ example: "Diversified long-term portfolio" }) @IsOptional() @IsString() description?: string; + @ApiPropertyOptional({ + example: "active", + enum: ["active", "inactive", "archived"], + }) @IsOptional() @IsEnum(PortfolioStatus) status?: PortfolioStatus; - @IsOptional() - @IsObject() - initialAllocation?: Record; - - @IsOptional() - @IsObject() - currentAllocation?: Record; - - @IsOptional() - @IsObject() - targetAllocation?: Record; - - @IsOptional() - @IsEnum(AllocationStrategy) - allocationStrategy?: AllocationStrategy; - + @ApiPropertyOptional({ example: true }) @IsOptional() @IsBoolean() autoRebalanceEnabled?: boolean; + @ApiPropertyOptional({ + example: "monthly", + enum: ["daily", "weekly", "monthly", "quarterly"], + }) @IsOptional() @IsString() rebalanceFrequency?: "daily" | "weekly" | "monthly" | "quarterly"; + @ApiPropertyOptional({ example: 5, minimum: 0 }) @IsOptional() @IsNumber() - @Min(0) - @Max(100) rebalanceThreshold?: number; + @ApiPropertyOptional({ example: { strategy: "balanced" } }) @IsOptional() - @IsObject() + @IsJSON() metadata?: Record; } export class PortfolioResponseDto { + @ApiProperty({ example: "d9e6c8d0-5f9c-4bb1-8db2-7d3b0d0a1d1f" }) id: string; + + @ApiProperty({ example: "Retirement Growth" }) name: string; - type: PortfolioType; + + @ApiPropertyOptional({ example: "Diversified long-term portfolio" }) description?: string; + + @ApiProperty({ example: "active", enum: ["active", "inactive", "archived"] }) status: PortfolioStatus; - initialAllocation: Record; + + @ApiProperty({ example: 10000 }) + totalValue: number; + + @ApiProperty({ example: { AAPL: 50, MSFT: 50 } }) currentAllocation: Record; + + @ApiPropertyOptional({ example: { AAPL: 60, MSFT: 40 } }) targetAllocation?: Record; - allocationStrategy?: AllocationStrategy; - totalValue: number; + + @ApiProperty({ example: true }) autoRebalanceEnabled: boolean; + + @ApiPropertyOptional({ example: "monthly" }) rebalanceFrequency?: string; + + @ApiProperty({ example: 5 }) rebalanceThreshold: number; + + @ApiPropertyOptional({ example: "2026-06-20T00:00:00.000Z" }) lastRebalanceDate?: Date; + + @ApiProperty({ example: "2026-06-20T00:00:00.000Z" }) createdAt: Date; + + @ApiProperty({ example: "2026-06-20T00:00:00.000Z" }) updatedAt: Date; } - - +export class PortfolioListResponseDto { + @ApiProperty({ type: [PortfolioResponseDto] }) + portfolios: PortfolioResponseDto[]; +} \ No newline at end of file diff --git a/src/investment/portfolio/dto/rebalancing.dto.ts b/src/investment/portfolio/dto/rebalancing.dto.ts index 84ce78b..f97b0c6 100644 --- a/src/investment/portfolio/dto/rebalancing.dto.ts +++ b/src/investment/portfolio/dto/rebalancing.dto.ts @@ -6,88 +6,121 @@ import { IsArray, IsJSON, IsDateString, + IsBoolean, } from "class-validator"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { RebalanceTrigger, RebalanceStatus, } from "../entities/rebalancing-event.entity"; export class TriggerRebalancingDto { + @ApiProperty({ description: "ID of the portfolio to rebalance" }) @IsString() portfolioId: string; + @ApiProperty({ description: "The trigger for the rebalancing event", enum: RebalanceTrigger }) @IsEnum(RebalanceTrigger) trigger: RebalanceTrigger; + @ApiPropertyOptional({ description: "Reason for the rebalancing trigger" }) @IsOptional() @IsString() triggerReason?: string; + @ApiPropertyOptional({ description: "Custom allocation for the rebalancing" }) @IsOptional() @IsJSON() customAllocation?: Record; } export class ApproveRebalancingDto { + @ApiProperty({ description: "ID of the rebalancing event to approve" }) @IsString() rebalancingEventId: string; + @ApiPropertyOptional({ description: "Optional notes for the approval" }) @IsOptional() @IsString() notes?: string; } export class ExecuteRebalancingDto { + @ApiProperty({ description: "ID of the rebalancing event to execute" }) @IsString() rebalancingEventId: string; + @ApiPropertyOptional({ description: "Optional notes for the execution" }) @IsOptional() @IsString() executionNotes?: string; + @ApiPropertyOptional({ description: "Actual cost of the rebalancing execution" }) @IsOptional() @IsNumber() actualCost?: number; + @ApiPropertyOptional({ description: "Slippage during the rebalancing execution" }) @IsOptional() @IsNumber() executionSlippage?: number; + @ApiPropertyOptional({ description: "Perform a dry run without executing trades" }) @IsOptional() + @IsBoolean() dryRun?: boolean; } export class ScheduleRebalancingDto { + @ApiProperty({ description: "Frequency of the rebalancing schedule", enum: ["daily", "weekly", "monthly", "custom"] }) @IsEnum(["daily", "weekly", "monthly", "custom"]) frequency: "daily" | "weekly" | "monthly" | "custom"; + @ApiPropertyOptional({ description: "Cron expression for custom schedules" }) @IsOptional() @IsString() cron?: string; } export class CancelRebalancingDto { + @ApiProperty({ description: "ID of the rebalancing event to cancel" }) @IsString() rebalancingEventId: string; + @ApiProperty({ description: "Reason for cancelling the rebalancing event" }) @IsString() reason: string; } export class RebalancingEventResponseDto { + @ApiProperty({ description: "Unique identifier of the rebalancing event" }) id: string; + @ApiProperty({ description: "The trigger for the rebalancing event", enum: RebalanceTrigger }) trigger: RebalanceTrigger; + @ApiProperty({ description: "Status of the rebalancing event", enum: RebalanceStatus }) status: RebalanceStatus; + @ApiPropertyOptional({ description: "Reason for the rebalancing trigger" }) triggerReason?: string; + @ApiProperty({ description: "Asset allocation before rebalancing" }) allocationBefore: Record; + @ApiProperty({ description: "Asset allocation after rebalancing" }) allocationAfter: Record; + @ApiProperty({ description: "List of trades executed during rebalancing" }) trades: Array; + @ApiPropertyOptional({ description: "Estimated cost of rebalancing" }) estimatedCost?: number; + @ApiPropertyOptional({ description: "Actual cost of rebalancing" }) actualCost?: number; + @ApiPropertyOptional({ description: "Maximum allocation drift before rebalancing" }) maxAllocationDrift?: number; + @ApiPropertyOptional({ description: "Expected return improvement from rebalancing" }) expectedReturnImprovement?: number; + @ApiPropertyOptional({ description: "Change in portfolio volatility after rebalancing" }) volatilityChange?: number; + @ApiProperty({ description: "Date of rebalancing event creation" }) createdAt: Date; + @ApiPropertyOptional({ description: "Date of rebalancing execution" }) executedAt?: Date; + @ApiPropertyOptional({ description: "Date of rebalancing completion" }) completedAt?: Date; -} +} \ No newline at end of file diff --git a/src/investment/portfolio/dto/risk-profile.dto.ts b/src/investment/portfolio/dto/risk-profile.dto.ts index e307c81..2e513c4 100644 --- a/src/investment/portfolio/dto/risk-profile.dto.ts +++ b/src/investment/portfolio/dto/risk-profile.dto.ts @@ -4,136 +4,176 @@ import { IsEnum, IsNumber, IsArray, - IsJSON, } from "class-validator"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { RiskTolerance, InvestmentGoal } from "../entities/risk-profile.entity"; export class CreateRiskProfileDto { + @ApiProperty({ description: "Name of the risk profile", example: "Aggressive Growth" }) @IsString() name: string; + @ApiPropertyOptional({ description: "Optional description of the risk profile" }) @IsOptional() @IsString() description?: string; + @ApiPropertyOptional({ description: "Tolerance for risk", enum: RiskTolerance }) @IsOptional() @IsEnum(RiskTolerance) riskTolerance?: RiskTolerance; + @ApiPropertyOptional({ description: "Primary investment goal", enum: InvestmentGoal }) @IsOptional() @IsEnum(InvestmentGoal) investmentGoal?: InvestmentGoal; + @ApiPropertyOptional({ description: "Target annual return", example: 0.1 }) @IsOptional() @IsNumber() targetReturn?: number; + @ApiPropertyOptional({ description: "Maximum acceptable volatility", example: 0.2 }) @IsOptional() @IsNumber() maxVolatility?: number; + @ApiPropertyOptional({ description: "Maximum acceptable drawdown", example: 0.15 }) @IsOptional() @IsNumber() maxDrawdown?: number; + @ApiPropertyOptional({ description: "Investment horizon in years", example: 10 }) @IsOptional() @IsNumber() investmentHorizonYears?: number; + @ApiPropertyOptional({ description: "Minimum equity allocation", example: 0.6 }) @IsOptional() @IsNumber() equityAllocationMin?: number; + @ApiPropertyOptional({ description: "Maximum equity allocation", example: 0.9 }) @IsOptional() @IsNumber() equityAllocationMax?: number; + @ApiPropertyOptional({ description: "Minimum bond allocation", example: 0.1 }) @IsOptional() @IsNumber() bondAllocationMin?: number; + @ApiPropertyOptional({ description: "Maximum bond allocation", example: 0.4 }) @IsOptional() @IsNumber() bondAllocationMax?: number; + @ApiPropertyOptional({ description: "List of excluded assets", example: ["BND"] }) @IsOptional() @IsArray() excludedAssets?: string[]; + @ApiPropertyOptional({ description: "List of required assets", example: ["VOO"] }) @IsOptional() @IsArray() requiredAssets?: string[]; + @ApiPropertyOptional({ description: "Minimum ESG score for assets", example: 70 }) @IsOptional() @IsNumber() minESGScore?: number; } export class UpdateRiskProfileDto { + @ApiPropertyOptional({ description: "Name of the risk profile" }) @IsOptional() @IsString() name?: string; + @ApiPropertyOptional({ description: "Optional description of the risk profile" }) @IsOptional() @IsString() description?: string; + @ApiPropertyOptional({ description: "Tolerance for risk", enum: RiskTolerance }) @IsOptional() @IsEnum(RiskTolerance) riskTolerance?: RiskTolerance; + @ApiPropertyOptional({ description: "Primary investment goal", enum: InvestmentGoal }) @IsOptional() @IsEnum(InvestmentGoal) investmentGoal?: InvestmentGoal; + @ApiPropertyOptional({ description: "Target annual return" }) @IsOptional() @IsNumber() targetReturn?: number; + @ApiPropertyOptional({ description: "Maximum acceptable volatility" }) @IsOptional() @IsNumber() maxVolatility?: number; + @ApiPropertyOptional({ description: "Maximum acceptable drawdown" }) @IsOptional() @IsNumber() maxDrawdown?: number; + @ApiPropertyOptional({ description: "Investment horizon in years" }) @IsOptional() @IsNumber() investmentHorizonYears?: number; + @ApiPropertyOptional({ description: "List of excluded assets" }) @IsOptional() @IsArray() excludedAssets?: string[]; + @ApiPropertyOptional({ description: "List of required assets" }) @IsOptional() @IsArray() requiredAssets?: string[]; + @ApiPropertyOptional({ description: "Minimum ESG score for assets" }) @IsOptional() @IsNumber() minESGScore?: number; } export class RiskProfileResponseDto { + @ApiProperty({ description: "Unique identifier of the risk profile" }) id: string; + @ApiProperty({ description: "Name of the risk profile" }) name: string; + @ApiPropertyOptional({ description: "Optional description of the risk profile" }) description?: string; + @ApiProperty({ description: "Tolerance for risk", enum: RiskTolerance }) riskTolerance: RiskTolerance; + @ApiProperty({ description: "Primary investment goal", enum: InvestmentGoal }) investmentGoal: InvestmentGoal; + @ApiProperty({ description: "Target annual return" }) targetReturn: number; + @ApiProperty({ description: "Maximum acceptable volatility" }) maxVolatility: number; + @ApiProperty({ description: "Maximum acceptable drawdown" }) maxDrawdown: number; + @ApiProperty({ description: "Target Sharpe ratio" }) sharpeRatioTarget: number; + @ApiProperty({ description: "Minimum equity allocation" }) equityAllocationMin: number; + @ApiProperty({ description: "Maximum equity allocation" }) equityAllocationMax: number; + @ApiProperty({ description: "Minimum bond allocation" }) bondAllocationMin: number; + @ApiProperty({ description: "Maximum bond allocation" }) bondAllocationMax: number; + @ApiProperty({ description: "Investment horizon in years" }) investmentHorizonYears: number; + @ApiProperty({ description: "Whether to use machine learning for optimization" }) useMachineLearning: boolean; + @ApiProperty({ description: "Date of risk profile creation" }) createdAt: Date; + @ApiProperty({ description: "Date of last risk profile update" }) updatedAt: Date; -} - - - +} \ No newline at end of file diff --git a/src/investment/portfolio/portfolio-management.controller.ts b/src/investment/portfolio/portfolio-management.controller.ts index 028856e..6b15f16 100644 --- a/src/investment/portfolio/portfolio-management.controller.ts +++ b/src/investment/portfolio/portfolio-management.controller.ts @@ -21,11 +21,11 @@ import { JwtAuthGuard } from "src/core/auth/jwt.guard"; import { PortfolioOwnerGuard } from "./guards/portfolio-owner.guard"; import { PortfolioService } from "./services/portfolio.service"; import { - CreatePortfolioRequestDto, - PortfolioListResponseDto, + CreatePortfolioDto, + UpdatePortfolioDto, PortfolioResponseDto, - UpdatePortfolioRequestDto, -} from "./dto/portfolio-management.dto"; + PortfolioListResponseDto, +} from "./dto/portfolio.dto"; import { ApiErrorDto } from "./dto/api-error.dto"; import { PortfolioStatus } from "./entities/portfolio.entity"; @@ -56,9 +56,9 @@ export class PortfolioManagementController { @HttpCode(HttpStatus.CREATED) async createPortfolio( @Request() req: any, - @Body() dto: CreatePortfolioRequestDto, + @Body() createPortfolioDto: CreatePortfolioDto, ): Promise { - return this.portfolioService.createPortfolio(req.user.id, dto as any); + return this.portfolioService.createPortfolio(req.user.id, createPortfolioDto); } @Get(":id") @@ -128,9 +128,9 @@ export class PortfolioManagementController { }) async updatePortfolio( @Param("id") id: string, - @Body() dto: UpdatePortfolioRequestDto, + @Body() updatePortfolioDto: UpdatePortfolioDto, ): Promise { - return this.portfolioService.updatePortfolio(id, dto as any) as any; + return this.portfolioService.updatePortfolio(id, updatePortfolioDto); } @Delete(":id") @@ -156,7 +156,4 @@ export class PortfolioManagementController { ): Promise { return this.portfolioService.archivePortfolio(id, PortfolioStatus.ARCHIVED); } -} - - - +} \ No newline at end of file diff --git a/src/investment/portfolio/services/portfolio.service.ts b/src/investment/portfolio/services/portfolio.service.ts index f08273b..739ad59 100644 --- a/src/investment/portfolio/services/portfolio.service.ts +++ b/src/investment/portfolio/services/portfolio.service.ts @@ -91,7 +91,6 @@ export class PortfolioService { async createPortfolio(userId: string, dto: CreatePortfolioDto): Promise { this.validatePortfolioName(dto.name); - this.validateAllocation(dto.initialAllocation); const existingPortfolio = await this.portfolioRepository.findOne({ where: { name: dto.name, userId }, @@ -148,21 +147,6 @@ export class PortfolioService { this.validatePortfolioName(dto.name); } - if (dto.initialAllocation) { - this.validateAllocation(dto.initialAllocation); - portfolio.initialAllocation = dto.initialAllocation; - } - - if (dto.currentAllocation) { - this.validateAllocation(dto.currentAllocation); - portfolio.currentAllocation = dto.currentAllocation; - } - - if (dto.targetAllocation) { - this.validateAllocation(dto.targetAllocation); - portfolio.targetAllocation = dto.targetAllocation; - } - if (dto.status === PortfolioStatus.ARCHIVED) { portfolio.status = PortfolioStatus.ARCHIVED; portfolio.deletedAt = new Date(); @@ -649,7 +633,4 @@ export class PortfolioService { take: limit, }); } -} - - - +} \ No newline at end of file diff --git a/src/investment/risk-management/dto/risk.dto.ts b/src/investment/risk-management/dto/risk.dto.ts index 901c7e3..fbbffd7 100644 --- a/src/investment/risk-management/dto/risk.dto.ts +++ b/src/investment/risk-management/dto/risk.dto.ts @@ -6,6 +6,7 @@ import { Min, Max, } from "class-validator"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; export enum RiskModel { VAR = "VaR", @@ -15,76 +16,102 @@ export enum RiskModel { } export class RiskConfigDto { + @ApiProperty({ description: "User ID for the risk configuration" }) @IsString() userId: string; + @ApiProperty({ description: "Risk tolerance of the user", minimum: 0, maximum: 1 }) @IsNumber() @Min(0) @Max(1) riskTolerance: number; + @ApiProperty({ description: "Maximum size of a single position", minimum: 0 }) @IsNumber() @Min(0) maxPositionSize: number; + @ApiProperty({ description: "Stop loss percentage", minimum: 0 }) @IsNumber() @Min(0) stopLossPercentage: number; + @ApiProperty({ description: "Take profit percentage", minimum: 0 }) @IsNumber() @Min(0) takeProfitPercentage: number; + @ApiPropertyOptional({ description: "Maximum drawdown allowed" }) @IsOptional() @IsNumber() maxDrawdown?: number; } export class PortfolioRiskDto { + @ApiProperty({ description: "User ID for the portfolio risk" }) userId: string; + @ApiProperty({ description: "Total value of the portfolio" }) totalValue: number; + @ApiProperty({ description: "Value at Risk (VaR) at 95% confidence" }) var95: number; + @ApiProperty({ description: "Value at Risk (VaR) at 99% confidence" }) var99: number; + @ApiProperty({ description: "Conditional Value at Risk (CVaR) at 95% confidence" }) cvar95: number; + @ApiProperty({ description: "Sharpe ratio of the portfolio" }) sharpeRatio: number; + @ApiProperty({ description: "Maximum drawdown of the portfolio" }) maxDrawdown: number; + @ApiProperty({ description: "Current drawdown of the portfolio" }) currentDrawdown: number; + @ApiProperty({ description: "Diversification score of the portfolio" }) diversificationScore: number; + @ApiProperty({ description: "Overall risk score of the portfolio" }) riskScore: number; + @ApiProperty({ description: "List of risk alerts for the portfolio" }) alerts: RiskAlertDto[]; + @ApiProperty({ description: "Date of the last risk calculation" }) calculatedAt: Date; } export class RiskAlertDto { + @ApiProperty({ description: "Type of the risk alert" }) type: | "stop_loss" | "take_profit" | "drawdown" | "concentration" | "volatility"; + @ApiProperty({ description: "Severity of the risk alert" }) severity: "low" | "medium" | "high" | "critical"; + @ApiProperty({ description: "Message of the risk alert" }) message: string; + @ApiPropertyOptional({ description: "Asset related to the risk alert" }) asset?: string; + @ApiProperty({ description: "Threshold for the risk alert" }) threshold: number; + @ApiProperty({ description: "Current value related to the risk alert" }) currentValue: number; + @ApiProperty({ description: "Date the risk alert was triggered" }) triggeredAt: Date; } export class PositionSizeDto { + @ApiProperty({ description: "User ID for the position size calculation" }) @IsString() userId: string; + @ApiProperty({ description: "Asset for the position size calculation" }) @IsString() asset: string; + @ApiProperty({ description: "Value of the portfolio", minimum: 0 }) @IsNumber() @Min(0) portfolioValue: number; + @ApiProperty({ description: "Volatility of the asset", minimum: 0 }) @IsNumber() @Min(0) volatility: number; -} - - - +} \ No newline at end of file