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
43 changes: 40 additions & 3 deletions src/investment/portfolio/dto/portfolio-asset.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { IsString, IsOptional, IsNumber, IsDateString } from "class-validator";
import { IsString, IsOptional, IsNumber, IsEnum, Length } from "class-validator";
import { Chain, AssetType } from "../entities/portfolio-asset.entity";

export class PortfolioAssetDto {
@IsString()
@Length(3, 10)
ticker: string;

@IsString()
name: string;

@IsEnum(Chain)
chain: Chain;

@IsOptional()
@IsEnum(AssetType)
type?: AssetType;

@IsOptional()
@IsNumber()
quantity?: number;
Expand Down Expand Up @@ -39,7 +48,33 @@ export class AddAssetToPortfolioDto {
costBasis?: number;
}

export class UpdatePortfolioAssetDto {
export class AddHoldingDto {
@IsString()
@Length(3, 10)
ticker: string;

@IsString()
name: string;

@IsEnum(Chain)
chain: Chain;

@IsOptional()
@IsEnum(AssetType)
type?: AssetType;

@IsNumber()
quantity: number;

@IsOptional()
@IsNumber()
currentPrice?: number;

@IsNumber()
costBasis: number;
}

export class UpdateHoldingDto {
@IsOptional()
@IsNumber()
quantity?: number;
Expand All @@ -57,7 +92,8 @@ export class PortfolioAssetResponseDto {
id: string;
ticker: string;
name: string;
type: string;
chain: Chain;
type: AssetType;
quantity: number;
currentPrice?: number;
value: number;
Expand All @@ -66,6 +102,7 @@ export class PortfolioAssetResponseDto {
expectedReturn?: number;
volatility?: number;
beta?: number;
costBasis?: number;
unrealizedGain?: number;
updatedAt: Date;
}
17 changes: 16 additions & 1 deletion src/investment/portfolio/entities/portfolio-asset.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ export enum AssetType {
OTHER = "other",
}

export enum Chain {
ETHEREUM = "ethereum",
POLYGON = "polygon",
BINANCE = "binance",
SOLANA = "solana",
OTHER = "other",
}

@Entity("portfolio_assets")
@Index(["portfolioId", "ticker"])
@Index(["portfolioId", "ticker", "chain"], { unique: true })
export class PortfolioAsset {
@PrimaryGeneratedColumn("uuid")
id: string;
Expand All @@ -33,6 +41,13 @@ export class PortfolioAsset {
@Column()
name: string;

@Column({
type: "enum",
enum: Chain,
default: Chain.OTHER,
})
chain: Chain;

@Column({
type: "enum",
enum: AssetType,
Expand Down
40 changes: 37 additions & 3 deletions src/investment/portfolio/portfolio.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { BacktestingService } from "./services/backtesting.service";
import { MLPredictionService } from "./services/ml-prediction.service";
import { PortfolioOwnerGuard } from "./guards/portfolio-owner.guard";
import { CreatePortfolioDto, UpdatePortfolioDto } from "./dto/portfolio.dto";
import { AddAssetToPortfolioDto } from "./dto/portfolio-asset.dto";
import { AddAssetToPortfolioDto, AddHoldingDto, UpdateHoldingDto } from "./dto/portfolio-asset.dto";
import { ApproveOptimizationDto, CreateOptimizationDto } from "./dto/optimization.dto";
import { ExecuteRebalancingDto, TriggerRebalancingDto } from "./dto/rebalancing.dto";
import { GetPerformanceMetricsDto } from "./dto/performance.dto";
Expand Down Expand Up @@ -82,10 +82,44 @@ export class PortfolioController {
return this.portfolioService.deletePortfolio(portfolioId);
}

// Asset Management Endpoints
// Holding Management Endpoints

@Post("portfolios/:portfolioId/holdings")
@ApiOperation({ summary: "Add holding to portfolio" })
@UseGuards(PortfolioOwnerGuard)
async addHolding(
@Param("portfolioId") portfolioId: string,
@Body() dto: AddHoldingDto,
) {
return this.portfolioService.addHolding(portfolioId, dto);
}

@Put("portfolios/:portfolioId/holdings/:holdingId")
@ApiOperation({ summary: "Update holding in portfolio" })
@UseGuards(PortfolioOwnerGuard)
async updateHolding(
@Param("portfolioId") portfolioId: string,
@Param("holdingId") holdingId: string,
@Body() dto: UpdateHoldingDto,
) {
return this.portfolioService.updateHolding(portfolioId, holdingId, dto);
}

@Delete("portfolios/:portfolioId/holdings/:holdingId")
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: "Remove holding from portfolio" })
@UseGuards(PortfolioOwnerGuard)
async removeHolding(
@Param("portfolioId") portfolioId: string,
@Param("holdingId") holdingId: string,
) {
return this.portfolioService.removeHolding(portfolioId, holdingId);
}

// Asset Management Endpoints (Backward Compatibility)

@Post("portfolios/:portfolioId/assets")
@ApiOperation({ summary: "Add asset to portfolio" })
@ApiOperation({ summary: "Add asset to portfolio (legacy)" })
@UseGuards(PortfolioOwnerGuard)
async addAsset(
@Param("portfolioId") portfolioId: string,
Expand Down
171 changes: 135 additions & 36 deletions src/investment/portfolio/services/portfolio.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable, Logger, BadRequestException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Portfolio } from "../entities/portfolio.entity";
import { PortfolioAsset } from "../entities/portfolio-asset.entity";
import { PortfolioAsset, Chain, AssetType } from "../entities/portfolio-asset.entity";
import {
OptimizationHistory,
OptimizationMethod,
Expand All @@ -11,6 +11,7 @@ import {
import { RiskProfile } from "../entities/risk-profile.entity";
import { CreatePortfolioDto, UpdatePortfolioDto } from "../dto/portfolio.dto";
import { CreateOptimizationDto } from "../dto/optimization.dto";
import { AddHoldingDto, UpdateHoldingDto } from "../dto/portfolio-asset.dto";
import { PortfolioStatus } from "../entities/portfolio.entity";
import { ModernPortfolioTheory } from "../algorithms/modern-portfolio-theory";
import { BlackLittermanModel } from "../algorithms/black-litterman";
Expand Down Expand Up @@ -91,47 +92,133 @@ export class PortfolioService {
}

/**
* Add asset to portfolio
* Add holding to portfolio
*/
async addAsset(
async addHolding(
portfolioId: string,
ticker: string,
name: string,
quantity: number,
currentPrice: number = 0,
costBasis: number = 0,
dto: AddHoldingDto,
): Promise<PortfolioAsset> {
const portfolio = await this.getPortfolio(portfolioId);
await this.getPortfolio(portfolioId);

// Check if asset already exists
let asset = await this.portfolioAssetRepository.findOne({
where: { portfolioId, ticker },
// Check if holding already exists
const existing = await this.portfolioAssetRepository.findOne({
where: { portfolioId, ticker: dto.ticker, chain: dto.chain },
});

if (!asset) {
asset = this.portfolioAssetRepository.create({
portfolioId,
ticker,
name,
quantity: 0,
value: 0,
allocationPercentage: 0,
costBasis,
costBasisPerShare: currentPrice,
});
if (existing) {
throw new BadRequestException("Holding with same ticker and chain already exists");
}

// Update asset
asset.quantity = quantity;
asset.currentPrice = currentPrice;
asset.value = quantity * currentPrice;
const holding = this.portfolioAssetRepository.create({
portfolioId,
ticker: dto.ticker,
name: dto.name,
chain: dto.chain,
type: dto.type || AssetType.CRYPTOCURRENCY,
quantity: dto.quantity,
currentPrice: dto.currentPrice || 0,
value: dto.quantity * (dto.currentPrice || 0),
costBasis: dto.costBasis,
costBasisPerShare: dto.quantity > 0 ? dto.costBasis / dto.quantity : 0,
});

// Calculate unrealized gain
if (holding.currentPrice && holding.costBasisPerShare) {
holding.unrealizedGain = (holding.currentPrice - holding.costBasisPerShare) * holding.quantity;
}

const saved = await this.portfolioAssetRepository.save(holding);

// Update portfolio metrics
await this.updatePortfolioMetrics(portfolioId);

return saved;
}

/**
* Update holding
*/
async updateHolding(
portfolioId: string,
holdingId: string,
dto: UpdateHoldingDto,
): Promise<PortfolioAsset> {
const holding = await this.portfolioAssetRepository.findOne({
where: { id: holdingId, portfolioId },
});

if (!holding) {
throw new BadRequestException("Holding not found");
}

// Update fields
if (dto.quantity !== undefined) {
holding.quantity = dto.quantity;
}
if (dto.currentPrice !== undefined) {
holding.currentPrice = dto.currentPrice;
}
if (dto.costBasis !== undefined) {
holding.costBasis = dto.costBasis;
holding.costBasisPerShare = holding.quantity > 0 ? dto.costBasis / holding.quantity : 0;
}

// Recalculate value
holding.value = holding.quantity * (holding.currentPrice || 0);

// Recalculate unrealized gain
if (holding.currentPrice && holding.costBasisPerShare) {
holding.unrealizedGain = (holding.currentPrice - holding.costBasisPerShare) * holding.quantity;
}

asset = await this.portfolioAssetRepository.save(asset);
const updated = await this.portfolioAssetRepository.save(holding);

// Update portfolio allocation
await this.updatePortfolioAllocation(portfolioId);
// Update portfolio metrics
await this.updatePortfolioMetrics(portfolioId);

return asset;
return updated;
}

/**
* Remove holding from portfolio
*/
async removeHolding(
portfolioId: string,
holdingId: string,
): Promise<void> {
const holding = await this.portfolioAssetRepository.findOne({
where: { id: holdingId, portfolioId },
});

if (!holding) {
throw new BadRequestException("Holding not found");
}

await this.portfolioAssetRepository.remove(holding);

// Update portfolio metrics
await this.updatePortfolioMetrics(portfolioId);
}

/**
* Add asset to portfolio (keeping for backward compatibility)
*/
async addAsset(
portfolioId: string,
ticker: string,
name: string,
quantity: number,
currentPrice: number = 0,
costBasis: number = 0,
): Promise<PortfolioAsset> {
return this.addHolding(portfolioId, {
ticker,
name,
chain: Chain.OTHER,
quantity,
currentPrice,
costBasis,
});
}

/**
Expand All @@ -153,18 +240,23 @@ export class PortfolioService {
asset.value = asset.quantity * currentPrice;
asset.lastPriceUpdate = new Date();

// Recalculate unrealized gain
if (asset.costBasisPerShare) {
asset.unrealizedGain = (asset.currentPrice - asset.costBasisPerShare) * asset.quantity;
}

const updated = await this.portfolioAssetRepository.save(asset);

// Recalculate allocation
await this.updatePortfolioAllocation(asset.portfolioId);
// Update portfolio metrics
await this.updatePortfolioMetrics(asset.portfolioId);

return updated;
}

/**
* Update portfolio allocation percentages
* Update portfolio metrics
*/
async updatePortfolioAllocation(portfolioId: string): Promise<void> {
async updatePortfolioMetrics(portfolioId: string): Promise<void> {
const portfolio = await this.getPortfolio(portfolioId);
const assets = await this.portfolioAssetRepository.find({
where: { portfolioId },
Expand All @@ -182,7 +274,7 @@ export class PortfolioService {
for (const asset of assets) {
const percentage = totalValue > 0 ? (asset.value / totalValue) * 100 : 0;
asset.allocationPercentage = percentage;
allocation[asset.ticker] = percentage;
allocation[`${asset.ticker}-${asset.chain}`] = percentage;
}

portfolio.currentAllocation = allocation;
Expand All @@ -191,6 +283,13 @@ export class PortfolioService {
await this.portfolioAssetRepository.save(assets);
}

/**
* Update portfolio allocation percentages (keeping for backward compatibility)
*/
async updatePortfolioAllocation(portfolioId: string): Promise<void> {
await this.updatePortfolioMetrics(portfolioId);
}

/**
* Run portfolio optimization
*/
Expand Down
Loading