Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
+254
View File
@@ -0,0 +1,254 @@
//! Portfolio constraint validation and enforcement
use crate::{Asset, PortfolioConstraints, PortfolioError, Result};
/// Validate portfolio weights against constraints
pub fn validate_weights(
weights: &[f64],
assets: &[Asset],
constraints: &PortfolioConstraints,
) -> Result<()> {
if weights.len() != assets.len() {
return Err(PortfolioError::InvalidConfig(
"Number of weights must match number of assets".to_string(),
));
}
validate_sum(weights)?;
validate_bounds(weights, constraints)?;
validate_long_only(weights, constraints)?;
validate_sector_limits(weights, assets, constraints)?;
Ok(())
}
/// Validate that weights sum to 1.0
fn validate_sum(weights: &[f64]) -> Result<()> {
let sum: f64 = weights.iter().sum();
if (sum - 1.0).abs() > 1e-6 {
return Err(PortfolioError::ConstraintViolation(format!(
"Weights must sum to 1.0, got {sum}"
)));
}
Ok(())
}
/// Validate weight bounds
fn validate_bounds(weights: &[f64], constraints: &PortfolioConstraints) -> Result<()> {
for (i, &weight) in weights.iter().enumerate() {
if weight < constraints.min_weight - 1e-6 {
return Err(PortfolioError::ConstraintViolation(format!(
"Weight {i} ({weight}) below minimum ({})",
constraints.min_weight
)));
}
if weight > constraints.max_weight + 1e-6 {
return Err(PortfolioError::ConstraintViolation(format!(
"Weight {i} ({weight}) above maximum ({})",
constraints.max_weight
)));
}
}
Ok(())
}
/// Validate long-only constraint
fn validate_long_only(weights: &[f64], constraints: &PortfolioConstraints) -> Result<()> {
if constraints.long_only {
for (i, &weight) in weights.iter().enumerate() {
if weight < -1e-6 {
return Err(PortfolioError::ConstraintViolation(format!(
"Negative weight {i} ({weight}) violates long-only constraint"
)));
}
}
}
Ok(())
}
/// Validate sector exposure limits
fn validate_sector_limits(
weights: &[f64],
assets: &[Asset],
constraints: &PortfolioConstraints,
) -> Result<()> {
for sector_limit in &constraints.sector_limits {
let mut sector_exposure = 0.0;
for (i, asset) in assets.iter().enumerate() {
if let Some(sector) = &asset.sector
&& sector == &sector_limit.sector
{
sector_exposure += weights[i];
}
}
if sector_exposure > sector_limit.max_weight + 1e-6 {
return Err(PortfolioError::ConstraintViolation(format!(
"Sector '{}' exposure ({sector_exposure}) exceeds limit ({})",
sector_limit.sector, sector_limit.max_weight
)));
}
}
Ok(())
}
/// Project weights to satisfy constraints (simple heuristic)
pub fn project_to_constraints(
weights: &mut [f64],
_assets: &[Asset],
constraints: &PortfolioConstraints,
) -> Result<()> {
if constraints.long_only {
for weight in weights.iter_mut() {
*weight = weight.max(constraints.min_weight);
}
}
for weight in weights.iter_mut() {
*weight = weight.clamp(constraints.min_weight, constraints.max_weight);
}
let sum: f64 = weights.iter().sum();
if sum > 0.0 {
for weight in weights.iter_mut() {
*weight /= sum;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SectorLimit;
fn create_test_assets() -> Vec<Asset> {
vec![
Asset {
ticker: "TECH1".to_string(),
name: "Tech Stock 1".to_string(),
asset_class: "equity".to_string(),
sector: Some("Technology".to_string()),
expected_return: 0.12,
volatility: 0.20,
},
Asset {
ticker: "TECH2".to_string(),
name: "Tech Stock 2".to_string(),
asset_class: "equity".to_string(),
sector: Some("Technology".to_string()),
expected_return: 0.14,
volatility: 0.25,
},
Asset {
ticker: "HEALTHCARE".to_string(),
name: "Healthcare Stock".to_string(),
asset_class: "equity".to_string(),
sector: Some("Healthcare".to_string()),
expected_return: 0.10,
volatility: 0.18,
},
]
}
#[test]
fn test_validate_sum() {
let valid_weights = vec![0.5, 0.3, 0.2];
assert!(validate_sum(&valid_weights).is_ok());
let invalid_weights = vec![0.5, 0.3, 0.1];
assert!(validate_sum(&invalid_weights).is_err());
}
#[test]
fn test_validate_bounds() {
let constraints = PortfolioConstraints {
min_weight: 0.1,
max_weight: 0.6,
..Default::default()
};
let valid_weights = vec![0.4, 0.3, 0.3];
assert!(validate_bounds(&valid_weights, &constraints).is_ok());
let too_low = vec![0.05, 0.45, 0.5];
assert!(validate_bounds(&too_low, &constraints).is_err());
let too_high = vec![0.7, 0.2, 0.1];
assert!(validate_bounds(&too_high, &constraints).is_err());
}
#[test]
fn test_validate_long_only() {
let long_only_constraints = PortfolioConstraints {
long_only: true,
..Default::default()
};
let valid_weights = vec![0.5, 0.3, 0.2];
assert!(validate_long_only(&valid_weights, &long_only_constraints).is_ok());
let negative_weights = vec![0.6, -0.1, 0.5];
assert!(validate_long_only(&negative_weights, &long_only_constraints).is_err());
let short_allowed_constraints = PortfolioConstraints {
long_only: false,
..Default::default()
};
assert!(validate_long_only(&negative_weights, &short_allowed_constraints).is_ok());
}
#[test]
fn test_validate_sector_limits() {
let assets = create_test_assets();
let mut constraints = PortfolioConstraints::default();
constraints.sector_limits.push(SectorLimit {
sector: "Technology".to_string(),
max_weight: 0.5,
});
let valid_weights = vec![0.3, 0.2, 0.5];
assert!(validate_sector_limits(&valid_weights, &assets, &constraints).is_ok());
let exceeds_limit = vec![0.4, 0.3, 0.3];
assert!(validate_sector_limits(&exceeds_limit, &assets, &constraints).is_err());
}
#[test]
fn test_validate_weights_success() {
let assets = create_test_assets();
let constraints = PortfolioConstraints::default();
let weights = vec![0.4, 0.3, 0.3];
assert!(validate_weights(&weights, &assets, &constraints).is_ok());
}
#[test]
fn test_validate_weights_dimension_mismatch() {
let assets = create_test_assets();
let constraints = PortfolioConstraints::default();
let weights = vec![0.5, 0.5];
assert!(validate_weights(&weights, &assets, &constraints).is_err());
}
#[test]
fn test_project_to_constraints() {
let assets = create_test_assets();
let constraints = PortfolioConstraints {
long_only: true,
min_weight: 0.1,
max_weight: 0.5,
..Default::default()
};
let mut weights = vec![0.7, -0.1, 0.4];
project_to_constraints(&mut weights, &assets, &constraints).unwrap();
assert!(weights.iter().all(|&w| w >= 0.0));
assert!(weights.iter().all(|&w| w <= 0.5));
let sum: f64 = weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
}
}