129 lines
5.2 KiB
C#
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;
|
|
}
|
|
}
|
|
} |