use chrono::{NaiveDate, Utc, Datelike}; use std::collections::HashMap; use crate::investment::Portfolio; #[derive(Debug, Clone)] pub struct PerformanceMetrics { pub total_return: f64, pub total_return_percent: f64, pub annualized_return: f64, #[allow(dead_code)] pub daily_return: f64, #[allow(dead_code)] pub monthly_returns: Vec, pub ytd_return: f64, pub ytd_return_percent: f64, #[allow(dead_code)] pub one_year_return: Option, #[allow(dead_code)] pub three_year_return: Option, #[allow(dead_code)] pub inception_date: NaiveDate, } #[derive(Debug, Clone)] pub struct MonthlyReturn { #[allow(dead_code)] pub year: i32, #[allow(dead_code)] pub month: u32, #[allow(dead_code)] pub return_amount: f64, #[allow(dead_code)] pub return_percent: f64, #[allow(dead_code)] pub ending_value: f64, } #[derive(Debug, Clone)] pub struct BenchmarkComparison { pub benchmark_name: String, pub benchmark_return: f64, pub excess_return: f64, #[allow(dead_code)] pub tracking_error: Option, #[allow(dead_code)] pub sharpe_ratio: Option, } #[derive(Debug, Clone)] pub struct PositionPerformance { pub symbol: String, #[allow(dead_code)] pub name: String, pub total_return: f64, pub total_return_percent: f64, pub contribution_to_return: f64, pub weight_in_portfolio: f64, } // Calculate performance metrics for a portfolio pub fn calculate_performance(portfolios: &[Portfolio]) -> PerformanceMetrics { let total_market_value: f64 = portfolios.iter().map(|p| p.total_market_value).sum(); let total_book_value: f64 = portfolios.iter().map(|p| p.total_book_value).sum(); let total_return = total_market_value - total_book_value; let total_return_percent = if total_book_value > 0.0 { (total_return / total_book_value) * 100.0 } else { 0.0 }; // For now, use simple calculations - would need transaction history for accurate time-weighted returns let days_held = 365.0; // Placeholder - would calculate from actual purchase dates let annualized_return = if days_held > 0.0 { ((total_market_value / total_book_value).powf(365.0 / days_held) - 1.0) * 100.0 } else { 0.0 }; let daily_return = total_return / days_held; // YTD calculation let _current_year = Utc::now().year(); let ytd_return = total_return; // Simplified - would need beginning of year values let ytd_return_percent = total_return_percent; PerformanceMetrics { total_return, total_return_percent, annualized_return, daily_return, monthly_returns: Vec::new(), // Would populate from transaction history ytd_return, ytd_return_percent, one_year_return: Some(total_return_percent), // Simplified three_year_return: None, // Need historical data inception_date: NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(), // Placeholder } } // Calculate performance by position pub fn calculate_position_performance(portfolios: &[Portfolio]) -> Vec { let total_market_value: f64 = portfolios.iter().map(|p| p.total_market_value).sum(); let mut position_perfs = Vec::new(); for portfolio in portfolios { for position in &portfolio.positions { let weight = position.market_value / total_market_value; let contribution = (position.unrealized_pnl / total_market_value) * 100.0; position_perfs.push(PositionPerformance { symbol: position.symbol.clone(), name: position.name.clone(), total_return: position.unrealized_pnl, total_return_percent: position.unrealized_pnl_percent, contribution_to_return: contribution, weight_in_portfolio: weight * 100.0, }); } } // Sort by contribution to return position_perfs.sort_by(|a, b| b.contribution_to_return.partial_cmp(&a.contribution_to_return).unwrap()); position_perfs } // Compare to benchmarks pub fn compare_to_benchmarks( portfolio_return: f64, benchmark_returns: &HashMap ) -> Vec { let mut comparisons = Vec::new(); for (name, benchmark_return) in benchmark_returns { comparisons.push(BenchmarkComparison { benchmark_name: name.clone(), benchmark_return: *benchmark_return, excess_return: portfolio_return - benchmark_return, tracking_error: None, // Would need time series data sharpe_ratio: None, // Would need volatility data }); } comparisons } // Get benchmark returns (hardcoded for now - would fetch from API) pub fn get_benchmark_returns() -> HashMap { let mut benchmarks = HashMap::new(); // 2024 YTD approximate returns (as of June) benchmarks.insert("S&P 500".to_string(), 15.3); benchmarks.insert("TSX Composite".to_string(), 6.1); benchmarks.insert("MSCI EAFE".to_string(), 5.8); benchmarks.insert("Canadian Bonds".to_string(), -1.2); benchmarks.insert("60/40 Portfolio".to_string(), 8.5); benchmarks } // Calculate risk metrics pub fn calculate_risk_metrics(portfolios: &[Portfolio]) -> HashMap { let mut metrics = HashMap::new(); // Calculate concentration risk let total_value: f64 = portfolios.iter().map(|p| p.total_market_value).sum(); let mut position_values: Vec = Vec::new(); for portfolio in portfolios { for position in &portfolio.positions { position_values.push(position.market_value); } } position_values.sort_by(|a, b| b.partial_cmp(a).unwrap()); // Top 5 concentration let top5_value: f64 = position_values.iter().take(5).sum(); let top5_concentration = (top5_value / total_value) * 100.0; metrics.insert("top_5_concentration".to_string(), top5_concentration); // Largest position if let Some(largest) = position_values.first() { let largest_position_pct = (largest / total_value) * 100.0; metrics.insert("largest_position_pct".to_string(), largest_position_pct); } // Asset class concentration let allocation = crate::investment::calculate_asset_allocation(portfolios); for (asset_type, value) in allocation { let pct = (value / total_value) * 100.0; metrics.insert(format!("{:?}_allocation", asset_type), pct); } metrics } // Generate performance summary text pub fn generate_performance_summary( metrics: &PerformanceMetrics, benchmarks: &[BenchmarkComparison], risk_metrics: &HashMap ) -> String { let mut summary = String::new(); summary.push_str(&format!("📊 Portfolio Performance Summary\n")); summary.push_str(&format!("================================\n\n")); summary.push_str(&format!("Overall Returns:\n")); summary.push_str(&format!(" Total Return: ${:.2} ({:+.2}%)\n", metrics.total_return, metrics.total_return_percent)); summary.push_str(&format!(" Annualized: {:+.2}%\n", metrics.annualized_return)); summary.push_str(&format!(" YTD: ${:.2} ({:+.2}%)\n", metrics.ytd_return, metrics.ytd_return_percent)); if !benchmarks.is_empty() { summary.push_str(&format!("\nBenchmark Comparison:\n")); for benchmark in benchmarks { let indicator = if benchmark.excess_return > 0.0 { "📈" } else { "📉" }; summary.push_str(&format!(" {} vs {}: {:+.2}% (benchmark: {:+.2}%)\n", indicator, benchmark.benchmark_name, benchmark.excess_return, benchmark.benchmark_return)); } } if let Some(concentration) = risk_metrics.get("top_5_concentration") { summary.push_str(&format!("\nRisk Metrics:\n")); summary.push_str(&format!(" Top 5 Concentration: {:.1}%\n", concentration)); if let Some(largest) = risk_metrics.get("largest_position_pct") { summary.push_str(&format!(" Largest Position: {:.1}%\n", largest)); } } summary } // Create simple performance chart (ASCII) #[allow(dead_code)] pub fn create_performance_chart(monthly_returns: &[MonthlyReturn]) -> Vec { let mut lines = Vec::new(); if monthly_returns.is_empty() { lines.push("No monthly data available".to_string()); return lines; } // Find min and max for scaling let max_return = monthly_returns.iter() .map(|r| r.return_percent) .fold(f64::NEG_INFINITY, f64::max); let min_return = monthly_returns.iter() .map(|r| r.return_percent) .fold(f64::INFINITY, f64::min); let range = max_return - min_return; let scale = if range > 0.0 { 20.0 / range } else { 1.0 }; lines.push(format!("Monthly Returns ({}% to {}%)", min_return as i32, max_return as i32)); lines.push("─".repeat(50)); for month in monthly_returns.iter().rev().take(12) { let bar_length = ((month.return_percent - min_return) * scale) as usize; let bar = "█".repeat(bar_length.max(1)); let indicator = if month.return_percent >= 0.0 { "+" } else { "" }; lines.push(format!("{:02}/{}: {:>6}{}% {}", month.month, month.year % 100, indicator, format!("{:.1}", month.return_percent), bar)); } lines } #[cfg(test)] mod tests { use super::*; use crate::investment::{Portfolio, Position, AccountType, AssetType}; use chrono::Utc; fn create_test_portfolio() -> Portfolio { Portfolio { broker: "Test".to_string(), account_id: "TEST123".to_string(), account_type: AccountType::TFSA, positions: vec![ Position { symbol: "VTI".to_string(), name: "Vanguard Total Market".to_string(), asset_type: AssetType::ETF, quantity: 100.0, market_price: 220.0, market_value: 22000.0, book_value: 20000.0, average_cost: 200.0, unrealized_pnl: 2000.0, unrealized_pnl_percent: 10.0, currency: "USD".to_string(), }, ], cash_balance: 1000.0, total_market_value: 23000.0, total_book_value: 21000.0, total_unrealized_pnl: 2000.0, last_updated: Utc::now().naive_utc(), } } #[test] fn test_calculate_performance() { let portfolios = vec![create_test_portfolio()]; let metrics = calculate_performance(&portfolios); assert_eq!(metrics.total_return, 2000.0); assert!((metrics.total_return_percent - 9.52).abs() < 0.01); assert!(metrics.annualized_return > 0.0); } #[test] fn test_position_performance() { let portfolios = vec![create_test_portfolio()]; let position_perfs = calculate_position_performance(&portfolios); assert_eq!(position_perfs.len(), 1); assert_eq!(position_perfs[0].symbol, "VTI"); assert_eq!(position_perfs[0].total_return, 2000.0); } #[test] fn test_benchmark_comparison() { let portfolio_return = 12.5; let mut benchmarks = HashMap::new(); benchmarks.insert("S&P 500".to_string(), 15.0); benchmarks.insert("TSX".to_string(), 8.0); let comparisons = compare_to_benchmarks(portfolio_return, &benchmarks); assert_eq!(comparisons.len(), 2); assert!(comparisons.iter().any(|c| c.benchmark_name == "S&P 500" && c.excess_return == -2.5)); assert!(comparisons.iter().any(|c| c.benchmark_name == "TSX" && c.excess_return == 4.5)); } }