using System.Collections.Generic; using System.Linq; namespace MarketData.ValueAtRisk { public class HistoricalVaR { private int returnDays = 1; private HistoricalVaR() { } public static VaRResult GetVaR(PortfolioHoldings portfolioHoldings, double percentile, int returnDays = 1) { VaRResult varResult = new VaRResult(); // Validate portfolio if (portfolioHoldings == null || portfolioHoldings.Count == 0) { varResult.Success = false; varResult.Message = "Portfolio is null or empty."; return varResult; } // Determine the minimum common price history across holdings int minPriceCount = int.MaxValue; for (int i = 0; i < portfolioHoldings.Count; i++) { int count = portfolioHoldings[i].Prices.Count; if (count < minPriceCount) minPriceCount = count; } // Enforce minimum observation requirement if (minPriceCount < 30) // or whatever threshold you prefer { varResult.Success = false; varResult.Message = $"Insufficient common price history ({minPriceCount} observations)."; return varResult; } // Truncate all holdings to the common window for (int i = 0; i < portfolioHoldings.Count; i++) { if (portfolioHoldings[i].Prices.Count > minPriceCount) { portfolioHoldings[i].Prices = new MarketDataModel.Prices( portfolioHoldings[i].Prices .Skip(portfolioHoldings[i].Prices.Count - minPriceCount) .ToList() ); } } // Calculate total market value and holdings weightings double marketValue = portfolioHoldings.GetMarketValue(); if (marketValue == 0) { varResult.Success = false; varResult.Message = "Portfolio market value is zero."; return varResult; } for (int index = 0; index < portfolioHoldings.Count; index++) { PortfolioHolding portfolioHolding = portfolioHoldings[index]; portfolioHolding.Weight = portfolioHolding.MarketValue / marketValue; } // Calculate weighted returns for the observation period portfolioHoldings.SetReturnDays(returnDays); int numReturns = portfolioHoldings.Min(p => p.Returns.Length); WeightedReturnsWithContribution weightedReturnsWithContribution = new WeightedReturnsWithContribution(); for (int index = 0; index < numReturns; index++) { for (int portfolioIndex = 0; portfolioIndex < portfolioHoldings.Count; portfolioIndex++) { PortfolioHolding portfolioHolding = portfolioHoldings[portfolioIndex]; WeightedReturn weightedReturn = new WeightedReturn( portfolioHolding.Symbol, portfolioHolding.Prices[index].Date, portfolioHolding.Returns[index] * portfolioHolding.Weight ); weightedReturnsWithContribution.Add(index, weightedReturn); } } double[] weightedReturns = weightedReturnsWithContribution.GetWeightedReturns(); List contributionsList = weightedReturnsWithContribution.GetContributions(); // Organize the weighted returns into bins for percentile access BinManager binManager = new BinManager(); BinResult binResult = binManager.GetVaRReturn(weightedReturns, contributionsList, percentile); Contributions contributions = binResult.Item; if (contributions == null) { varResult.Success = false; return varResult; } // Map contributions back to portfolio holdings Dictionary portfolioHoldingsBySymbol = new Dictionary(); foreach (PortfolioHolding portfolioHolding in portfolioHoldings) { if (!portfolioHoldingsBySymbol.ContainsKey(portfolioHolding.Symbol)) portfolioHoldingsBySymbol.Add(portfolioHolding.Symbol, portfolioHolding); } foreach (Contribution contribution in contributions) { if (!portfolioHoldingsBySymbol.ContainsKey(contribution.Symbol)) continue; portfolioHoldingsBySymbol[contribution.Symbol].Contribution = contribution.ContributionValue; portfolioHoldingsBySymbol[contribution.Symbol].ContributionDate = contribution.AnalysisDate; } return new VaRResult(binResult.Value, marketValue * binResult.Value); } public int ReturnDays { get => returnDays; set => returnDays = value; } } }