1#[derive(Clone, Debug, Eq, PartialEq)]
2pub struct Example {
3 pub stuff: String,
4}
5
6impl Example {
7 pub fn new(value: String) -> Self {
8 Example { stuff: value }
9 }
10}
11
12pub type Matrix = Vec<Vec<f64>>;
13pub type RiskContributionParts = (Vec<f64>, Vec<f64>, Vec<f64>);
14
15fn validate_matrix(matrix: &Matrix) -> Result<(usize, usize), String> {
16 if matrix.is_empty() {
17 return Err("matrix must not be empty".to_string());
18 }
19 let cols = matrix[0].len();
20 if cols == 0 {
21 return Err("matrix rows must not be empty".to_string());
22 }
23 if matrix.iter().any(|row| row.len() != cols) {
24 return Err("matrix rows must have equal length".to_string());
25 }
26 Ok((matrix.len(), cols))
27}
28
29fn validate_square(matrix: &Matrix) -> Result<usize, String> {
30 let (rows, cols) = validate_matrix(matrix)?;
31 if rows != cols {
32 return Err("matrix must be square".to_string());
33 }
34 Ok(rows)
35}
36
37fn normalize(mut weights: Vec<f64>, gross: bool) -> Result<Vec<f64>, String> {
38 let denominator = if gross {
39 weights.iter().map(|value| value.abs()).sum::<f64>()
40 } else {
41 weights.iter().sum::<f64>()
42 };
43 if denominator == 0.0 {
44 return Err("weights cannot be normalized when the denominator is zero".to_string());
45 }
46 for weight in &mut weights {
47 *weight /= denominator;
48 }
49 Ok(weights)
50}
51
52pub fn equal_weights(count: usize) -> Result<Vec<f64>, String> {
53 if count == 0 {
54 return Err("assets must not be empty".to_string());
55 }
56 Ok(vec![1.0 / count as f64; count])
57}
58
59pub fn rank_weights(values: Vec<f64>, ascending: bool) -> Result<Vec<f64>, String> {
60 if values.is_empty() {
61 return Err("signals must not be empty".to_string());
62 }
63 let mut ranked: Vec<(usize, f64)> = values.into_iter().enumerate().collect();
64 ranked.sort_by(|left, right| {
65 let ordering = left
66 .1
67 .partial_cmp(&right.1)
68 .unwrap_or(std::cmp::Ordering::Equal);
69 if ascending {
70 ordering
71 } else {
72 ordering.reverse()
73 }
74 });
75 let mut raw = vec![0.0; ranked.len()];
76 for (rank, (idx, _value)) in ranked.into_iter().enumerate() {
77 raw[idx] = rank as f64 + 1.0;
78 }
79 normalize(raw, false)
80}
81
82pub fn signal_proportional_weights(values: Vec<f64>) -> Result<Vec<f64>, String> {
83 if values.is_empty() {
84 return Err("signals must not be empty".to_string());
85 }
86 normalize(values, true)
87}
88
89pub fn target_volatility_weights(volatility: Vec<f64>) -> Result<Vec<f64>, String> {
90 if volatility.is_empty() {
91 return Err("volatility must not be empty".to_string());
92 }
93 if volatility.iter().any(|value| *value <= 0.0) {
94 return Err("all volatility values must be positive".to_string());
95 }
96 normalize(
97 volatility.into_iter().map(|value| 1.0 / value).collect(),
98 false,
99 )
100}
101
102fn mat_vec(matrix: &Matrix, vector: &[f64]) -> Vec<f64> {
103 matrix
104 .iter()
105 .map(|row| {
106 row.iter()
107 .zip(vector)
108 .map(|(left, right)| left * right)
109 .sum()
110 })
111 .collect()
112}
113
114fn dot(left: &[f64], right: &[f64]) -> f64 {
115 left.iter().zip(right).map(|(l, r)| l * r).sum()
116}
117
118fn invert_matrix(matrix: &Matrix) -> Option<Matrix> {
119 let n = matrix.len();
120 let mut augmented = vec![vec![0.0; n * 2]; n];
121 for row in 0..n {
122 for col in 0..n {
123 augmented[row][col] = matrix[row][col];
124 }
125 augmented[row][n + row] = 1.0;
126 }
127 for col in 0..n {
128 let mut pivot = col;
129 for row in (col + 1)..n {
130 if augmented[row][col].abs() > augmented[pivot][col].abs() {
131 pivot = row;
132 }
133 }
134 if augmented[pivot][col].abs() < 1e-14 {
135 return None;
136 }
137 augmented.swap(col, pivot);
138 let divisor = augmented[col][col];
139 for value in &mut augmented[col] {
140 *value /= divisor;
141 }
142 for row in 0..n {
143 if row == col {
144 continue;
145 }
146 let factor = augmented[row][col];
147 let pivot_row = augmented[col].clone();
148 for (target, source) in augmented[row].iter_mut().zip(pivot_row) {
149 *target -= factor * source;
150 }
151 }
152 }
153 Some(augmented.into_iter().map(|row| row[n..].to_vec()).collect())
154}
155
156fn invert_with_ridge(covariance: &Matrix) -> Result<Matrix, String> {
157 if let Some(inverse) = invert_matrix(covariance) {
158 return Ok(inverse);
159 }
160 let mut regularized = covariance.clone();
161 for (idx, row) in regularized.iter_mut().enumerate() {
162 row[idx] += 1e-12;
163 }
164 invert_matrix(®ularized).ok_or_else(|| "covariance matrix is singular".to_string())
165}
166
167pub fn minimum_variance_weights(covariance: Matrix) -> Result<Vec<f64>, String> {
168 let size = validate_square(&covariance)?;
169 let inverse = invert_with_ridge(&covariance)?;
170 let ones = vec![1.0; size];
171 let inv_ones = mat_vec(&inverse, &ones);
172 normalize(inv_ones, false)
173}
174
175pub fn mean_variance_weights(
176 expected_returns: Vec<f64>,
177 covariance: Matrix,
178 risk_aversion: f64,
179) -> Result<Vec<f64>, String> {
180 let size = validate_square(&covariance)?;
181 if expected_returns.len() != size {
182 return Err("expected_returns length must match covariance dimensions".to_string());
183 }
184 let inverse = invert_with_ridge(&covariance)?;
185 let scale = risk_aversion.max(1e-12);
186 let raw: Vec<f64> = mat_vec(&inverse, &expected_returns)
187 .into_iter()
188 .map(|value| value / scale)
189 .collect();
190 if raw.iter().sum::<f64>() == 0.0 {
191 return equal_weights(size);
192 }
193 normalize(raw, false)
194}
195
196pub fn risk_parity_weights(
197 covariance: Matrix,
198 max_iter: usize,
199 tolerance: f64,
200) -> Result<Vec<f64>, String> {
201 let size = validate_square(&covariance)?;
202 let mut weights: Vec<f64> = (0..size)
203 .map(|idx| 1.0 / covariance[idx][idx].max(1e-18).sqrt())
204 .collect();
205 weights = normalize(weights, false)?;
206 let target = 1.0 / size as f64;
207 for _ in 0..max_iter {
208 let cov_weights = mat_vec(&covariance, &weights);
209 let variance = dot(&weights, &cov_weights);
210 let vol = variance.max(0.0).sqrt();
211 if vol == 0.0 {
212 break;
213 }
214 let mut max_error: f64 = 0.0;
215 for idx in 0..size {
216 let pct = weights[idx] * cov_weights[idx] / variance;
217 max_error = max_error.max((pct - target).abs());
218 if pct > 0.0 {
219 weights[idx] *= (target / pct).sqrt();
220 }
221 weights[idx] = weights[idx].max(1e-12);
222 }
223 weights = normalize(weights, false)?;
224 if max_error < tolerance {
225 break;
226 }
227 }
228 Ok(weights)
229}
230
231pub fn hierarchical_risk_parity_weights(covariance: Matrix) -> Result<Vec<f64>, String> {
232 let size = validate_square(&covariance)?;
233 let raw: Vec<f64> = (0..size)
234 .map(|idx| 1.0 / covariance[idx][idx].max(1e-18))
235 .collect();
236 normalize(raw, false)
237}
238
239pub fn portfolio_variance(weights: Vec<f64>, covariance: Matrix) -> Result<f64, String> {
240 let size = validate_square(&covariance)?;
241 if weights.len() != size {
242 return Err("covariance dimensions must match weights".to_string());
243 }
244 Ok(dot(&weights, &mat_vec(&covariance, &weights)))
245}
246
247pub fn portfolio_volatility(weights: Vec<f64>, covariance: Matrix) -> Result<f64, String> {
248 Ok(portfolio_variance(weights, covariance)?.max(0.0).sqrt())
249}
250
251pub fn risk_contribution(
252 weights: Vec<f64>,
253 covariance: Matrix,
254) -> Result<RiskContributionParts, String> {
255 let size = validate_square(&covariance)?;
256 if weights.len() != size {
257 return Err("covariance dimensions must match weights".to_string());
258 }
259 let cov_weights = mat_vec(&covariance, &weights);
260 let vol = dot(&weights, &cov_weights).max(0.0).sqrt();
261 let marginal: Vec<f64> = if vol == 0.0 {
262 vec![0.0; size]
263 } else {
264 cov_weights.into_iter().map(|value| value / vol).collect()
265 };
266 let component: Vec<f64> = weights
267 .iter()
268 .zip(&marginal)
269 .map(|(weight, marg)| weight * marg)
270 .collect();
271 let percentage: Vec<f64> = if vol == 0.0 {
272 vec![0.0; size]
273 } else {
274 component.iter().map(|value| value / vol).collect()
275 };
276 Ok((marginal, component, percentage))
277}
278
279pub fn tracking_error(active_weights: Vec<f64>, covariance: Matrix) -> Result<f64, String> {
280 portfolio_volatility(active_weights, covariance)
281}
282
283pub fn active_share(active_weights: Vec<f64>) -> f64 {
284 0.5 * active_weights.iter().map(|value| value.abs()).sum::<f64>()
285}
286
287pub fn brinson_attribution(
288 portfolio_weights: Vec<f64>,
289 benchmark_weights: Vec<f64>,
290 portfolio_returns: Vec<f64>,
291 benchmark_returns: Vec<f64>,
292) -> Result<(f64, f64, f64, f64), String> {
293 let size = portfolio_weights.len();
294 if benchmark_weights.len() != size
295 || portfolio_returns.len() != size
296 || benchmark_returns.len() != size
297 {
298 return Err("all input vectors must have the same length".to_string());
299 }
300 let benchmark_total = benchmark_weights
301 .iter()
302 .zip(&benchmark_returns)
303 .map(|(weight, ret)| weight * ret)
304 .sum::<f64>();
305 let mut allocation = 0.0;
306 let mut selection = 0.0;
307 let mut interaction = 0.0;
308 for idx in 0..size {
309 allocation += (portfolio_weights[idx] - benchmark_weights[idx])
310 * (benchmark_returns[idx] - benchmark_total);
311 selection += benchmark_weights[idx] * (portfolio_returns[idx] - benchmark_returns[idx]);
312 interaction += (portfolio_weights[idx] - benchmark_weights[idx])
313 * (portfolio_returns[idx] - benchmark_returns[idx]);
314 }
315 Ok((
316 allocation,
317 selection,
318 interaction,
319 allocation + selection + interaction,
320 ))
321}
322
323pub fn factor_return_decomposition(
324 exposures: Vec<f64>,
325 factor_returns: Vec<f64>,
326) -> Result<(Vec<f64>, f64), String> {
327 if exposures.len() != factor_returns.len() {
328 return Err("exposures and factor_returns must have the same length".to_string());
329 }
330 let components: Vec<f64> = exposures
331 .iter()
332 .zip(&factor_returns)
333 .map(|(exposure, ret)| exposure * ret)
334 .collect();
335 let total = components.iter().sum();
336 Ok((components, total))
337}
338
339#[cfg(test)]
341mod example_tests {
342 use super::*;
343
344 #[test]
345 fn test_new() {
346 let e = Example::new(String::from("test"));
347 assert_eq!(e.stuff, String::from("test"));
348 }
349
350 #[test]
351 fn test_clone_and_eq() {
352 let e = Example::new(String::from("test"));
353 assert_eq!(e, e.clone());
354 }
355
356 #[test]
357 fn test_debug() {
358 let e = Example::new(String::from("test"));
359 assert_eq!(format!("{e:?}"), "Example { stuff: \"test\" }");
360 }
361}