fin_primitives/portfolio/
factor_model.rs1#[derive(Debug, Clone, PartialEq)]
6pub enum FactorType {
7 Market,
9 Size,
11 Value,
13 Momentum,
15 Quality,
17 LowVolatility,
19 Custom(String),
21}
22
23#[derive(Debug, Clone)]
25pub struct Factor {
26 pub name: String,
28 pub returns: Vec<f64>,
30 pub factor_type: FactorType,
32}
33
34#[derive(Debug, Clone)]
36pub struct FactorExposure {
37 pub factor_name: String,
39 pub beta: f64,
41 pub t_stat: f64,
43 pub is_significant: bool,
45}
46
47#[derive(Debug, Clone)]
49pub struct FactorModelResult {
50 pub alpha: f64,
52 pub alpha_t_stat: f64,
54 pub exposures: Vec<FactorExposure>,
56 pub r_squared: f64,
58 pub residuals: Vec<f64>,
60 pub information_ratio: f64,
62 pub tracking_error: f64,
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct FactorModel;
69
70impl FactorModel {
71 pub fn ols(y: &[f64], x: &[Vec<f64>]) -> (Vec<f64>, f64) {
77 let n = y.len();
78 if n == 0 || x.is_empty() {
79 return (vec![], 0.0);
80 }
81 let k = x[0].len(); let mut xtx = vec![vec![0.0_f64; k]; k];
85 let mut xty = vec![0.0_f64; k];
86 for (i, row) in x.iter().enumerate() {
87 let yi = y[i];
88 for j in 0..k {
89 xty[j] += row[j] * yi;
90 for l in 0..k {
91 xtx[j][l] += row[j] * row[l];
92 }
93 }
94 }
95
96 let mut aug: Vec<Vec<f64>> = (0..k)
98 .map(|r| {
99 let mut row = xtx[r].clone();
100 row.push(xty[r]);
101 row
102 })
103 .collect();
104
105 for col in 0..k {
106 let mut max_row = col;
108 let mut max_val = aug[col][col].abs();
109 for row in (col + 1)..k {
110 if aug[row][col].abs() > max_val {
111 max_val = aug[row][col].abs();
112 max_row = row;
113 }
114 }
115 aug.swap(col, max_row);
116
117 let pivot = aug[col][col];
118 if pivot.abs() < 1e-14 {
119 continue; }
121 for row in 0..k {
122 if row == col {
123 continue;
124 }
125 let factor = aug[row][col] / pivot;
126 for c in col..=k {
127 aug[row][c] -= factor * aug[col][c];
128 }
129 }
130 let d = aug[col][col];
131 for c in col..=k {
132 aug[col][c] /= d;
133 }
134 }
135
136 let coefficients: Vec<f64> = (0..k).map(|r| aug[r][k]).collect();
137
138 let y_mean = y.iter().sum::<f64>() / n as f64;
140 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
141 let ss_res: f64 = x
142 .iter()
143 .zip(y.iter())
144 .map(|(row, &yi)| {
145 let y_hat: f64 = row.iter().zip(coefficients.iter()).map(|(&xi, &b)| xi * b).sum();
146 (yi - y_hat).powi(2)
147 })
148 .sum();
149 let r_squared = if ss_tot < 1e-14 { 0.0 } else { 1.0 - ss_res / ss_tot };
150
151 (coefficients, r_squared)
152 }
153
154 pub fn t_statistics(
157 coefficients: &[f64],
158 x: &[Vec<f64>],
159 y: &[f64],
160 betas: &[f64],
161 ) -> Vec<f64> {
162 let n = y.len();
163 let k = coefficients.len();
164 if n <= k {
165 return vec![0.0; k];
166 }
167 let rss: f64 = x
169 .iter()
170 .zip(y.iter())
171 .map(|(row, &yi)| {
172 let y_hat: f64 = row.iter().zip(betas.iter()).map(|(&xi, &b)| xi * b).sum();
173 (yi - y_hat).powi(2)
174 })
175 .sum();
176 let sigma2 = rss / (n - k) as f64;
177
178 let kk = k;
181 let mut xtx = vec![vec![0.0_f64; kk]; kk];
182 for row in x.iter() {
183 for j in 0..kk {
184 for l in 0..kk {
185 xtx[j][l] += row[j] * row[l];
186 }
187 }
188 }
189 let mut aug: Vec<Vec<f64>> = (0..kk)
191 .map(|r| {
192 let mut row = xtx[r].clone();
193 let mut id = vec![0.0_f64; kk];
194 id[r] = 1.0;
195 row.extend(id);
196 row
197 })
198 .collect();
199
200 for col in 0..kk {
201 let mut max_row = col;
202 let mut max_val = aug[col][col].abs();
203 for row in (col + 1)..kk {
204 if aug[row][col].abs() > max_val {
205 max_val = aug[row][col].abs();
206 max_row = row;
207 }
208 }
209 aug.swap(col, max_row);
210 let pivot = aug[col][col];
211 if pivot.abs() < 1e-14 {
212 continue;
213 }
214 for row in 0..kk {
215 if row == col {
216 continue;
217 }
218 let f = aug[row][col] / pivot;
219 for c in 0..(2 * kk) {
220 aug[row][c] -= f * aug[col][c];
221 }
222 }
223 let d = aug[col][col];
224 for c in 0..(2 * kk) {
225 aug[col][c] /= d;
226 }
227 }
228
229 (0..k)
231 .map(|i| {
232 let var_i = sigma2 * aug[i][kk + i];
233 if var_i <= 0.0 { 0.0 } else { coefficients[i] / var_i.sqrt() }
234 })
235 .collect()
236 }
237
238 pub fn fit(&self, asset_returns: &[f64], factors: &[Factor]) -> FactorModelResult {
242 let n = asset_returns.len();
243 if n == 0 || factors.is_empty() {
244 return FactorModelResult {
245 alpha: 0.0,
246 alpha_t_stat: 0.0,
247 exposures: vec![],
248 r_squared: 0.0,
249 residuals: vec![],
250 information_ratio: 0.0,
251 tracking_error: 0.0,
252 };
253 }
254
255 let x: Vec<Vec<f64>> = (0..n)
257 .map(|i| {
258 let mut row = vec![1.0_f64];
259 for fac in factors.iter() {
260 row.push(*fac.returns.get(i).unwrap_or(&0.0));
261 }
262 row
263 })
264 .collect();
265
266 let (coeffs, r_squared) = Self::ols(asset_returns, &x);
267 let t_stats = Self::t_statistics(&coeffs, &x, asset_returns, &coeffs);
268
269 let alpha = *coeffs.first().unwrap_or(&0.0);
270 let alpha_t_stat = *t_stats.first().unwrap_or(&0.0);
271
272 let exposures: Vec<FactorExposure> = factors
273 .iter()
274 .enumerate()
275 .map(|(idx, fac)| {
276 let beta = *coeffs.get(idx + 1).unwrap_or(&0.0);
277 let t_stat = *t_stats.get(idx + 1).unwrap_or(&0.0);
278 FactorExposure {
279 factor_name: fac.name.clone(),
280 beta,
281 t_stat,
282 is_significant: t_stat.abs() > 2.0,
283 }
284 })
285 .collect();
286
287 let residuals: Vec<f64> = x
289 .iter()
290 .zip(asset_returns.iter())
291 .map(|(row, &yi)| {
292 let y_hat: f64 = row.iter().zip(coeffs.iter()).map(|(&xi, &b)| xi * b).sum();
293 yi - y_hat
294 })
295 .collect();
296
297 let tracking_error = Self::std_dev(&residuals);
298 let information_ratio = Self::information_ratio(alpha, &residuals);
299
300 FactorModelResult {
301 alpha,
302 alpha_t_stat,
303 exposures,
304 r_squared,
305 residuals,
306 information_ratio,
307 tracking_error,
308 }
309 }
310
311 pub fn fama_french_3(
315 asset_returns: &[f64],
316 mkt_rf: &[f64],
317 smb: &[f64],
318 hml: &[f64],
319 rf_rate: f64,
320 ) -> FactorModelResult {
321 let n = asset_returns.len();
322 let excess: Vec<f64> = asset_returns.iter().map(|&r| r - rf_rate).collect();
323
324 let mkt_factor = Factor {
325 name: "MKT-RF".to_string(),
326 returns: mkt_rf[..n.min(mkt_rf.len())].to_vec(),
327 factor_type: FactorType::Market,
328 };
329 let smb_factor = Factor {
330 name: "SMB".to_string(),
331 returns: smb[..n.min(smb.len())].to_vec(),
332 factor_type: FactorType::Size,
333 };
334 let hml_factor = Factor {
335 name: "HML".to_string(),
336 returns: hml[..n.min(hml.len())].to_vec(),
337 factor_type: FactorType::Value,
338 };
339
340 let model = FactorModel;
341 model.fit(&excess, &[mkt_factor, smb_factor, hml_factor])
342 }
343
344 pub fn information_ratio(alpha: f64, residuals: &[f64]) -> f64 {
348 let te = Self::std_dev(residuals);
349 if te < 1e-14 { 0.0 } else { alpha / te * 252.0_f64.sqrt() }
350 }
351
352 pub fn factor_contribution(
354 exposures: &[FactorExposure],
355 factor_returns: &[f64],
356 ) -> Vec<f64> {
357 exposures
358 .iter()
359 .zip(factor_returns.iter())
360 .map(|(exp, &fr)| exp.beta * fr)
361 .collect()
362 }
363
364 pub fn systematic_return(
366 exposures: &[FactorExposure],
367 period_factor_returns: &[f64],
368 ) -> f64 {
369 exposures
370 .iter()
371 .zip(period_factor_returns.iter())
372 .map(|(exp, &fr)| exp.beta * fr)
373 .sum()
374 }
375
376 pub fn idiosyncratic_return(asset_return: f64, systematic: f64) -> f64 {
378 asset_return - systematic
379 }
380
381 fn std_dev(v: &[f64]) -> f64 {
386 let n = v.len();
387 if n < 2 {
388 return 0.0;
389 }
390 let mean = v.iter().sum::<f64>() / n as f64;
391 let var = v.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
392 var.sqrt()
393 }
394}
395
396#[derive(Debug, Clone, Default)]
405pub struct AptModel;
406
407impl AptModel {
408 pub fn fit(asset_returns: &[f64], macro_factors: &[Vec<f64>]) -> FactorModelResult {
412 let factors: Vec<Factor> = macro_factors
413 .iter()
414 .enumerate()
415 .map(|(i, series)| Factor {
416 name: format!("MacroFactor{}", i + 1),
417 returns: series.clone(),
418 factor_type: FactorType::Custom(format!("macro_{}", i + 1)),
419 })
420 .collect();
421 let model = FactorModel;
422 model.fit(asset_returns, &factors)
423 }
424
425 pub fn risk_premium(exposures: &[FactorExposure], factor_risk_premia: &[f64]) -> f64 {
427 exposures
428 .iter()
429 .zip(factor_risk_premia.iter())
430 .map(|(exp, &rp)| exp.beta * rp)
431 .sum()
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn test_ols_simple() {
441 let x: Vec<Vec<f64>> = (0..10).map(|i| vec![1.0, i as f64]).collect();
443 let y: Vec<f64> = (0..10).map(|i| 2.0 + 3.0 * i as f64).collect();
444 let (coeffs, r2) = FactorModel::ols(&y, &x);
445 assert!((coeffs[0] - 2.0).abs() < 1e-8, "intercept");
446 assert!((coeffs[1] - 3.0).abs() < 1e-8, "slope");
447 assert!((r2 - 1.0).abs() < 1e-8, "R2");
448 }
449
450 #[test]
451 fn test_fama_french_3() {
452 let n = 50;
453 let mkt: Vec<f64> = (0..n).map(|i| 0.01 * (i as f64).sin()).collect();
454 let smb: Vec<f64> = (0..n).map(|i| 0.005 * (i as f64).cos()).collect();
455 let hml: Vec<f64> = vec![0.002; n];
456 let asset: Vec<f64> = mkt.iter().zip(smb.iter()).map(|(&m, &s)| m + 0.5 * s + 0.001).collect();
457 let result = FactorModel::fama_french_3(&asset, &mkt, &smb, &hml, 0.0);
458 assert!(result.r_squared >= 0.0 && result.r_squared <= 1.0 + 1e-9);
459 assert_eq!(result.exposures.len(), 3);
460 }
461
462 #[test]
463 fn test_information_ratio() {
464 let residuals = vec![0.01, -0.01, 0.02, -0.02, 0.01];
465 let ir = FactorModel::information_ratio(0.001, &residuals);
466 assert!(ir.is_finite());
467 }
468
469 #[test]
470 fn test_apt_risk_premium() {
471 let exposures = vec![
472 FactorExposure { factor_name: "f1".into(), beta: 1.2, t_stat: 3.0, is_significant: true },
473 FactorExposure { factor_name: "f2".into(), beta: 0.5, t_stat: 1.5, is_significant: false },
474 ];
475 let premia = vec![0.04, 0.02];
476 let rp = AptModel::risk_premium(&exposures, &premia);
477 assert!((rp - (1.2 * 0.04 + 0.5 * 0.02)).abs() < 1e-10);
478 }
479}