Files

129 lines
5.2 KiB
C#

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<Contributions> contributionsList = weightedReturnsWithContribution.GetContributions();
// Organize the weighted returns into bins for percentile access
BinManager<Contributions> binManager = new BinManager<Contributions>();
BinResult<Contributions> 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<string, PortfolioHolding> portfolioHoldingsBySymbol = new Dictionary<string, PortfolioHolding>();
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;
}
}
}