diff --git a/src/investment/portfolio/dto/rebalancing.dto.ts b/src/investment/portfolio/dto/rebalancing.dto.ts index 8584915..84ce78b 100644 --- a/src/investment/portfolio/dto/rebalancing.dto.ts +++ b/src/investment/portfolio/dto/rebalancing.dto.ts @@ -57,6 +57,15 @@ export class ExecuteRebalancingDto { dryRun?: boolean; } +export class ScheduleRebalancingDto { + @IsEnum(["daily", "weekly", "monthly", "custom"]) + frequency: "daily" | "weekly" | "monthly" | "custom"; + + @IsOptional() + @IsString() + cron?: string; +} + export class CancelRebalancingDto { @IsString() rebalancingEventId: string; @@ -82,6 +91,3 @@ export class RebalancingEventResponseDto { executedAt?: Date; completedAt?: Date; } - - - diff --git a/src/investment/portfolio/portfolio.module.ts b/src/investment/portfolio/portfolio.module.ts index d76a0f1..3a50228 100644 --- a/src/investment/portfolio/portfolio.module.ts +++ b/src/investment/portfolio/portfolio.module.ts @@ -2,6 +2,10 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { BullModule } from "@nestjs/bull"; +// Modules +import { AlertsModule } from "src/growth/alerts/alerts.module"; +import { DeFiModule } from "src/defi/defi.module"; + // Entities import { Portfolio } from "./entities/portfolio.entity"; import { PortfolioAsset } from "./entities/portfolio-asset.entity"; @@ -22,6 +26,9 @@ import { PortfolioConstraintService } from "./services/portfolio-constraint.serv import { AuditLogService } from "src/infrastructure/audit/audit-log.service"; import { TradingTransactionService } from "./services/trading-transaction.service"; +// Processors +import { RebalancingProcessor } from "./processors/rebalancing.processor"; + // Controllers import { PortfolioController } from "./portfolio.controller"; import { PortfolioManagementController } from "./portfolio-management.controller"; @@ -56,6 +63,8 @@ import { PortfolioOwnerGuard } from "./guards/portfolio-owner.guard"; name: "ml-predictions", }, ), + AlertsModule, + DeFiModule, ], providers: [ PortfolioService, @@ -67,6 +76,7 @@ import { PortfolioOwnerGuard } from "./guards/portfolio-owner.guard"; AuditLogService, TradingTransactionService, PortfolioOwnerGuard, + RebalancingProcessor, ], controllers: [PortfolioController, PortfolioManagementController], @@ -81,6 +91,3 @@ import { PortfolioOwnerGuard } from "./guards/portfolio-owner.guard"; ], }) export class PortfolioModule {} - - - diff --git a/src/investment/portfolio/processors/rebalancing.processor.ts b/src/investment/portfolio/processors/rebalancing.processor.ts new file mode 100644 index 0000000..f165258 --- /dev/null +++ b/src/investment/portfolio/processors/rebalancing.processor.ts @@ -0,0 +1,70 @@ +import { Process, Processor } from "@nestjs/bull"; +import { Logger } from "@nestjs/common"; +import { Job } from "bull"; +import { RebalancingService } from "../services/rebalancing.service"; +import { RebalanceTrigger } from "../entities/rebalancing-event.entity"; + +@Processor("rebalancing") +export class RebalancingProcessor { + private readonly logger = new Logger(RebalancingProcessor.name); + + constructor(private readonly rebalancingService: RebalancingService) {} + + @Process("rebalance-task") + async handleRebalanceTask(job: Job<{ portfolioId: string }>) { + const { portfolioId } = job.data; + this.logger.log( + `Processing scheduled rebalance for portfolio: ${portfolioId}`, + ); + + try { + const { shouldRebalance } = + await this.rebalancingService.shouldRebalance(portfolioId); + + if (shouldRebalance) { + this.logger.log( + `Portfolio ${portfolioId} needs rebalancing. Triggering...`, + ); + const event = await this.rebalancingService.triggerRebalancing( + portfolioId, + RebalanceTrigger.TIME_BASED, + "Scheduled rebalancing triggered by system", + ); + + // For automated rebalancing, we might want to execute immediately if the portfolio is set to auto-rebalance + // In this implementation, we simulate and then execute if safe. + const simulation = + await this.rebalancingService.simulateRebalance(portfolioId); + + if (simulation.expectedSlippage <= 0.02) { + await this.rebalancingService.executeRebalancing( + event.id, + undefined, + simulation.expectedSlippage, + ); + this.logger.log( + `Successfully executed scheduled rebalance for portfolio ${portfolioId}`, + ); + } else { + this.logger.warn( + `Slippage too high for portfolio ${portfolioId}, skipping execution.`, + ); + await this.rebalancingService.cancelRebalancing( + event.id, + "High slippage detected during scheduled task", + ); + } + } else { + this.logger.log( + `Portfolio ${portfolioId} does not need rebalancing at this time.`, + ); + } + } catch (error) { + this.logger.error( + `Failed to process rebalance task for portfolio ${portfolioId}`, + error.stack, + ); + throw error; + } + } +} diff --git a/src/investment/portfolio/services/rebalancing.service.spec.ts b/src/investment/portfolio/services/rebalancing.service.spec.ts new file mode 100644 index 0000000..72dbdcb --- /dev/null +++ b/src/investment/portfolio/services/rebalancing.service.spec.ts @@ -0,0 +1,197 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { getRepositoryToken } from "@nestjs/typeorm"; +import { RebalancingService } from "./rebalancing.service"; +import { + RebalancingEvent, + RebalanceStatus, + RebalanceTrigger, +} from "../entities/rebalancing-event.entity"; +import { Portfolio } from "../entities/portfolio.entity"; +import { PortfolioAsset } from "../entities/portfolio-asset.entity"; +import { PortfolioService } from "./portfolio.service"; +import { TradingTransactionService } from "./trading-transaction.service"; +import { AuditLogService } from "src/infrastructure/audit/audit-log.service"; +import { AlertDispatcherService } from "src/growth/alerts/services/alert-dispatcher.service"; +import { TransactionOptimizationService } from "src/defi/services/transaction-optimization.service"; +import { getQueueToken } from "@nestjs/bull"; +import { BadRequestException } from "@nestjs/common"; + +describe("RebalancingService", () => { + let service: RebalancingService; + let portfolioRepository: any; + let portfolioAssetRepository: any; + let rebalancingRepository: any; + let portfolioService: any; + let alertService: any; + let auditLogService: any; + + const mockPortfolio = { + id: "portfolio-1", + userId: "user-1", + name: "Test Portfolio", + totalValue: 10000, + targetAllocation: { BTC: 60, ETH: 40 }, + currentAllocation: { BTC: 50, ETH: 50 }, + rebalanceThreshold: 5, + }; + + const mockAssets = [ + { ticker: "BTC", allocationPercentage: 50, currentPrice: 50000 }, + { ticker: "ETH", allocationPercentage: 50, currentPrice: 2000 }, + ]; + + beforeEach(async () => { + portfolioRepository = { + findOne: jest.fn().mockResolvedValue(mockPortfolio), + save: jest.fn().mockImplementation((p) => Promise.resolve(p)), + }; + portfolioAssetRepository = { + find: jest.fn().mockResolvedValue(mockAssets), + findOne: jest.fn().mockImplementation(({ where: { ticker } }) => { + return Promise.resolve(mockAssets.find((a) => a.ticker === ticker)); + }), + }; + rebalancingRepository = { + create: jest.fn().mockImplementation((d) => ({ ...d, id: "event-1" })), + save: jest.fn().mockImplementation((e) => Promise.resolve(e)), + findOne: jest.fn(), + }; + portfolioService = { + getPortfolio: jest.fn().mockResolvedValue(mockPortfolio), + }; + alertService = { + dispatch: jest.fn().mockResolvedValue(undefined), + }; + auditLogService = { + recordVerification: jest.fn().mockResolvedValue(undefined), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RebalancingService, + { + provide: getRepositoryToken(Portfolio), + useValue: portfolioRepository, + }, + { + provide: getRepositoryToken(PortfolioAsset), + useValue: portfolioAssetRepository, + }, + { + provide: getRepositoryToken(RebalancingEvent), + useValue: rebalancingRepository, + }, + { provide: PortfolioService, useValue: portfolioService }, + { + provide: TradingTransactionService, + useValue: { executeTrade: jest.fn() }, + }, + { provide: AuditLogService, useValue: auditLogService }, + { provide: AlertDispatcherService, useValue: alertService }, + { provide: TransactionOptimizationService, useValue: {} }, + { + provide: getQueueToken("rebalancing"), + useValue: { + add: jest.fn(), + getRepeatableJobs: jest.fn().mockResolvedValue([]), + }, + }, + ], + }).compile(); + + service = module.get(RebalancingService); + }); + + it("should detect when rebalancing is needed", async () => { + const result = await service.shouldRebalance("portfolio-1"); + expect(result.shouldRebalance).toBe(true); + expect(result.maxDrift).toBe(10); // BTC drift is 10% + }); + + it("should not rebalance if drift is below threshold", async () => { + const smallDriftPortfolio = { + ...mockPortfolio, + currentAllocation: { BTC: 58, ETH: 42 }, + }; + const smallDriftAssets = [ + { ticker: "BTC", allocationPercentage: 58 }, + { ticker: "ETH", allocationPercentage: 42 }, + ]; + portfolioService.getPortfolio.mockResolvedValue(smallDriftPortfolio); + portfolioAssetRepository.findOne.mockImplementation( + ({ where: { ticker } }) => { + return Promise.resolve( + smallDriftAssets.find((a) => a.ticker === ticker), + ); + }, + ); + + const result = await service.shouldRebalance("portfolio-1"); + expect(result.shouldRebalance).toBe(false); + expect(result.maxDrift).toBe(2); + }); + + it("should simulate rebalancing with correct trades", async () => { + const simulation = await service.simulateRebalance("portfolio-1"); + expect(simulation.tradePlan).toHaveLength(2); + expect(simulation.gasEstimate).toBeGreaterThan(0); + expect(auditLogService.recordVerification).toHaveBeenCalledWith( + expect.objectContaining({ + type: "REBALANCE_SIMULATION", + }), + ); + }); + + it("should validate slippage correctly", () => { + expect(service.validateSlippage(0.01).safe).toBe(true); + expect(service.validateSlippage(0.03).safe).toBe(false); + }); + + it("should fail execution if slippage exceeds limit", async () => { + const mockEvent = { + id: "event-1", + portfolioId: "portfolio-1", + portfolio: mockPortfolio, + status: RebalanceStatus.PENDING, + }; + rebalancingRepository.findOne.mockResolvedValue(mockEvent); + + await expect( + service.executeRebalancing("event-1", undefined, 0.05), + ).rejects.toThrow(BadRequestException); + + expect(alertService.dispatch).toHaveBeenCalledWith( + "user-1", + expect.objectContaining({ + type: "REBALANCE_CANCELLED", + }), + ); + }); + + it("should execute rebalancing successfully", async () => { + const mockEvent = { + id: "event-1", + portfolioId: "portfolio-1", + portfolio: mockPortfolio, + trades: [{ ticker: "BTC", action: "buy", quantity: 1, price: 50000 }], + allocationAfter: { BTC: 60, ETH: 40 }, + status: RebalanceStatus.PENDING, + }; + rebalancingRepository.findOne.mockResolvedValue(mockEvent); + + const result = await service.executeRebalancing("event-1", 100, 0.01); + + expect(result.status).toBe(RebalanceStatus.COMPLETED); + expect(alertService.dispatch).toHaveBeenCalledWith( + "user-1", + expect.objectContaining({ + type: "REBALANCE_SUCCESS", + }), + ); + expect(auditLogService.recordVerification).toHaveBeenCalledWith( + expect.objectContaining({ + type: "REBALANCE_SUCCESS", + }), + ); + }); +}); diff --git a/src/investment/portfolio/services/rebalancing.service.ts b/src/investment/portfolio/services/rebalancing.service.ts index 843f7ea..156efe3 100644 --- a/src/investment/portfolio/services/rebalancing.service.ts +++ b/src/investment/portfolio/services/rebalancing.service.ts @@ -1,6 +1,8 @@ import { Injectable, Logger, BadRequestException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; +import { InjectQueue } from "@nestjs/bull"; +import { Queue } from "bull"; import { RebalancingEvent, RebalanceTrigger, @@ -10,10 +12,15 @@ import { Portfolio } from "../entities/portfolio.entity"; import { PortfolioAsset } from "../entities/portfolio-asset.entity"; import { PortfolioService } from "./portfolio.service"; import { TradingTransactionService } from "./trading-transaction.service"; +import { AuditLogService } from "src/infrastructure/audit/audit-log.service"; +import { AlertDispatcherService } from "src/growth/alerts/services/alert-dispatcher.service"; +import { TransactionOptimizationService } from "src/defi/services/transaction-optimization.service"; @Injectable() export class RebalancingService { private readonly logger = new Logger(RebalancingService.name); + private readonly DEFAULT_THRESHOLD = 5; + private readonly SLIPPAGE_LIMIT = 0.02; constructor( @InjectRepository(RebalancingEvent) @@ -24,20 +31,27 @@ export class RebalancingService { private portfolioAssetRepository: Repository, private portfolioService: PortfolioService, private tradingService: TradingTransactionService, + private auditLogService: AuditLogService, + private alertService: AlertDispatcherService, + private transactionOptimizationService: TransactionOptimizationService, + @InjectQueue("rebalancing") private rebalancingQueue: Queue, ) {} /** - * Check if portfolio needs rebalancing + * Determine whether deviation exceeds threshold. */ - async checkRebalancingNeeded(portfolioId: string): Promise { + async shouldRebalance( + portfolioId: string, + customThreshold?: number, + ): Promise<{ shouldRebalance: boolean; maxDrift: number }> { const portfolio = await this.portfolioService.getPortfolio(portfolioId); - if (!portfolio.targetAllocation) { - return false; + return { shouldRebalance: false, maxDrift: 0 }; } - // Check drift from target allocation - const maxDrift = portfolio.rebalanceThreshold || 5; + const threshold = + customThreshold ?? portfolio.rebalanceThreshold ?? this.DEFAULT_THRESHOLD; + let maxDrift = 0; for (const [ticker, targetPercentage] of Object.entries( portfolio.targetAllocation, @@ -50,15 +64,240 @@ export class RebalancingService { const drift = Math.abs( asset.allocationPercentage - (targetPercentage as number), ); + if (drift > maxDrift) maxDrift = drift; + } + } - if (drift > maxDrift) { - this.logger.log(`Drift detected for ${ticker}: ${drift}%`); - return true; - } + return { + shouldRebalance: maxDrift > threshold, + maxDrift, + }; + } + + /** + * Simulate rebalancing without executing trades. + */ + async simulateRebalance(portfolioId: string): Promise<{ + expectedAllocations: Record; + tradePlan: any[]; + gasEstimate: number; + expectedSlippage: number; + warnings: string[]; + }> { + const portfolio = await this.portfolioService.getPortfolio(portfolioId); + const trades = await this.calculateRebalancingTrades(portfolioId); + + // Mock gas and slippage estimation based on trade count and volume + const gasEstimate = trades.length * 0.005; // Dummy ETH gas estimate + const expectedSlippage = trades.length > 5 ? 0.015 : 0.005; // Higher slippage for more trades + const warnings = []; + + if (expectedSlippage > this.SLIPPAGE_LIMIT) { + warnings.push("High expected slippage detected"); + } + + await this.auditLogService.recordVerification({ + type: "REBALANCE_SIMULATION", + portfolioId, + tradeCount: trades.length, + gasEstimate, + expectedSlippage, + }); + + return { + expectedAllocations: portfolio.targetAllocation || {}, + tradePlan: trades, + gasEstimate, + expectedSlippage, + warnings, + }; + } + + /** + * Cancel execution if slippage > 2% + */ + validateSlippage(slippage: number): { safe: boolean; error?: string } { + if (slippage > this.SLIPPAGE_LIMIT) { + return { + safe: false, + error: `Slippage ${slippage * 100}% exceeds limit of ${ + this.SLIPPAGE_LIMIT * 100 + }%`, + }; + } + return { safe: true }; + } + + /** + * Execute rebalancing with batching and optimizations. + */ + async executeRebalancing( + rebalancingEventId: string, + actualCost?: number, + slippage: number = 0, + dryRun: boolean = false, + ): Promise { + const event = await this.rebalancingRepository.findOne({ + where: { id: rebalancingEventId }, + relations: ["portfolio"], + }); + + if (!event) { + throw new BadRequestException("Rebalancing event not found"); + } + + // Notify user before execution + await this.alertService.dispatch(event.portfolio.userId, { + type: "REBALANCE_STARTED", + portfolioId: event.portfolioId, + message: `Rebalancing execution started for portfolio ${event.portfolio.name}`, + }); + + const slippageCheck = this.validateSlippage(slippage); + if (!slippageCheck.safe) { + event.status = RebalanceStatus.FAILED; + event.failureReason = slippageCheck.error; + await this.rebalancingRepository.save(event); + + await this.alertService.dispatch(event.portfolio.userId, { + type: "REBALANCE_CANCELLED", + portfolioId: event.portfolioId, + reason: slippageCheck.error, + }); + + await this.auditLogService.recordVerification({ + type: "REBALANCE_SLIPPAGE_FAILURE", + portfolioId: event.portfolioId, + slippage, + }); + + throw new BadRequestException(slippageCheck.error); + } + + if (dryRun) { + this.logger.log(`Dry run for rebalancing event ${rebalancingEventId}`); + return event; + } + + event.status = RebalanceStatus.IN_PROGRESS; + await this.rebalancingRepository.save(event); + + try { + // Execute trades in batches to minimize gas/impact + const batchSize = 3; + for (let i = 0; i < event.trades.length; i += batchSize) { + const batch = event.trades.slice(i, i + batchSize); + await Promise.all( + batch.map((trade) => + this.tradingService.executeTrade( + event.portfolioId, + trade.ticker, + trade.action, + trade.quantity, + trade.price, + ), + ), + ); } + + // Update portfolio + const portfolio = event.portfolio; + portfolio.currentAllocation = event.allocationAfter; + portfolio.lastRebalanceDate = new Date(); + await this.portfolioRepository.save(portfolio); + + // Finalize event + event.status = RebalanceStatus.COMPLETED; + event.actualCost = actualCost || event.estimatedCost; + event.executionSlippage = slippage; + event.executedAt = new Date(); + event.completedAt = new Date(); + await this.rebalancingRepository.save(event); + + // Audit & Notify + await this.auditLogService.recordVerification({ + type: "REBALANCE_SUCCESS", + portfolioId: event.portfolioId, + eventId: event.id, + }); + + await this.alertService.dispatch(portfolio.userId, { + type: "REBALANCE_SUCCESS", + portfolioId: portfolio.id, + message: `Successfully rebalanced portfolio ${portfolio.name}`, + }); + + return event; + } catch (error) { + event.status = RebalanceStatus.FAILED; + event.failureReason = error.message; + await this.rebalancingRepository.save(event); + + await this.alertService.dispatch(event.portfolio.userId, { + type: "REBALANCE_FAILED", + portfolioId: event.portfolioId, + error: error.message, + }); + + throw error; } + } - return false; + /** + * Schedule rebalancing using Bull queue. + */ + async scheduleRebalance( + portfolioId: string, + frequency: "daily" | "weekly" | "monthly" | "custom", + cron?: string, + ): Promise { + const portfolio = await this.portfolioService.getPortfolio(portfolioId); + + // Prevent duplicate scheduling by checking existing repeatable jobs + const jobs = await this.rebalancingQueue.getRepeatableJobs(); + const existingJob = jobs.find((j) => j.id === `rebalance-${portfolioId}`); + if (existingJob) { + await this.rebalancingQueue.removeRepeatableByKey(existingJob.key); + } + + let cronExpression = cron; + if (!cronExpression) { + switch (frequency) { + case "daily": + cronExpression = "0 0 * * *"; + break; + case "weekly": + cronExpression = "0 0 * * 0"; + break; + case "monthly": + cronExpression = "0 0 1 * *"; + break; + default: + cronExpression = "0 0 * * *"; + } + } + + await this.rebalancingQueue.add( + "rebalance-task", + { portfolioId }, + { + repeat: { cron: cronExpression }, + jobId: `rebalance-${portfolioId}`, + removeOnComplete: true, + }, + ); + + this.logger.log( + `Scheduled ${frequency} rebalancing for portfolio ${portfolioId}`, + ); + } + + /** + * Check if portfolio needs rebalancing (Old method, kept for compatibility if needed, but updated to use shouldRebalance) + */ + async checkRebalancingNeeded(portfolioId: string): Promise { + const result = await this.shouldRebalance(portfolioId); + return result.shouldRebalance; } /** @@ -79,14 +318,7 @@ export class RebalancingService { where: { portfolioId }, }); - const trades: Array<{ - ticker: string; - action: "buy" | "sell"; - quantity: number; - price: number; - value: number; - estimatedTaxImpact: number; - }> = []; + const trades: any[] = []; for (const asset of assets) { const targetPercentage = portfolio.targetAllocation?.[asset.ticker] || 0; @@ -94,7 +326,6 @@ export class RebalancingService { const difference = targetPercentage - currentPercentage; if (Math.abs(difference) > 0.5) { - // Significant difference const targetValue = (targetPercentage / 100) * portfolio.totalValue; const currentValue = (currentPercentage / 100) * portfolio.totalValue; const valueDifference = targetValue - currentValue; @@ -109,7 +340,7 @@ export class RebalancingService { (asset.currentPrice - (asset.costBasisPerShare || 0)) * Math.abs(quantity), ); - estimatedTaxImpact = capitalGains * 0.15; // Assume 15% capital gains tax + estimatedTaxImpact = capitalGains * 0.15; } trades.push({ @@ -135,14 +366,9 @@ export class RebalancingService { reason?: string, ): Promise { const portfolio = await this.portfolioService.getPortfolio(portfolioId); - const assets = await this.portfolioAssetRepository.find({ - where: { portfolioId }, - }); - const allocationBefore = { ...portfolio.currentAllocation }; const trades = await this.calculateRebalancingTrades(portfolioId); - // Create rebalancing event const event = this.rebalancingRepository.create({ portfolioId, trigger, @@ -159,10 +385,6 @@ export class RebalancingService { ), }); - this.logger.log( - `Rebalancing triggered for ${portfolioId}: ${trades.length} trades`, - ); - return this.rebalancingRepository.save(event); } @@ -181,60 +403,6 @@ export class RebalancingService { } event.status = RebalanceStatus.IN_PROGRESS; - - return this.rebalancingRepository.save(event); - } - - /** - * Execute rebalancing - */ - async executeRebalancing( - rebalancingEventId: string, - actualCost?: number, - slippage?: number, - dryRun: boolean = false, - ): Promise { - const event = await this.rebalancingRepository.findOne({ - where: { id: rebalancingEventId }, - relations: ["portfolio"], - }); - - if (!event) { - throw new BadRequestException("Rebalancing event not found"); - } - - if (dryRun) { - this.logger.log(`Dry run for rebalancing event ${rebalancingEventId}`); - return event; - } - - // Execute trades - for (const trade of event.trades) { - await this.tradingService.executeTrade( - event.portfolioId, - trade.ticker, - trade.action, - trade.quantity, - trade.price, - ); - } - - // Update portfolio allocation - const portfolio = event.portfolio; - portfolio.currentAllocation = event.allocationAfter; - portfolio.lastRebalanceDate = new Date(); - - await this.portfolioRepository.save(portfolio); - - // Update rebalancing event - event.status = RebalanceStatus.COMPLETED; - event.actualCost = actualCost || event.estimatedCost; - event.executionSlippage = slippage || 0; - event.executedAt = new Date(); - event.completedAt = new Date(); - - this.logger.log(`Rebalancing executed for portfolio ${event.portfolioId}`); - return this.rebalancingRepository.save(event); } @@ -305,7 +473,6 @@ export class RebalancingService { return false; } - // Check time-based trigger if (portfolio.rebalanceFrequency) { const lastRebalance = portfolio.lastRebalanceDate || portfolio.createdAt; const now = new Date(); @@ -320,19 +487,14 @@ export class RebalancingService { } } - // Check drift-based trigger - const needsRebalancing = await this.checkRebalancingNeeded(portfolioId); - - if (needsRebalancing) { + const { shouldRebalance } = await this.shouldRebalance(portfolioId); + if (shouldRebalance) { this.logger.log(`Drift-based rebalancing triggered for ${portfolioId}`); } - return needsRebalancing; + return shouldRebalance; } - /** - * Convert rebalance frequency to days - */ private frequencyToDays( frequency: "daily" | "weekly" | "monthly" | "quarterly", ): number { @@ -350,6 +512,3 @@ export class RebalancingService { } } } - - -