Skip to main content

portfolio_analyzer/
analysis.rs

1/// Portfolio analysis: overlaps, diversity, optimal weights
2use std::collections::{HashMap, HashSet};
3/// Overlap %: fraction of portfolio_weights that are in target_tickers
4pub fn overlap_pct(
5    portfolio_weights: &HashMap<String, f64>,
6    target_tickers: &HashSet<String>,
7) -> f64 {
8    portfolio_weights
9        .iter()
10        .filter(|(sym, _)| target_tickers.contains(*sym))
11        .map(|(_, &w)| w)
12        .sum::<f64>()
13        * 100.0
14}
15/// HHI and effective N
16pub fn diversity(weights: &HashMap<String, f64>) -> (f64, f64) {
17    let total: f64 = weights.values().sum();
18    if total == 0.0 {
19        return (1.0, 1.0);
20    }
21    let hhi: f64 = weights.values().map(|&w| (w / total).powi(2)).sum();
22    let eff_n = if hhi > 0.0 { 1.0 / hhi } else { 0.0 };
23    (hhi, eff_n)
24}
25
26/// Weighted mean SD score for a portfolio
27pub fn portfolio_sd(weights: &HashMap<String, f64>, scores: &HashMap<String, f64>) -> f64 {
28    let mut numerator = 0.0f64;
29    let mut denom = 0.0f64;
30    for (sym, &w) in weights {
31        if let Some(&s) = scores.get(sym) {
32            numerator += w * s;
33            denom += w;
34        }
35    }
36    if denom > 0.0 {
37        numerator / denom
38    } else {
39        0.5
40    }
41}
42
43/// Build blended target weights: p_m * momentum + p_i * international
44pub fn blend_target(
45    mom_weights: &HashMap<String, f64>,
46    intl_weights: &HashMap<String, f64>,
47    p_m: f64,
48    p_i: f64,
49) -> HashMap<String, f64> {
50    let mut target: HashMap<String, f64> = HashMap::new();
51    for (sym, &w) in mom_weights {
52        *target.entry(sym.clone()).or_insert(0.0) += p_m * w;
53    }
54    for (sym, &w) in intl_weights {
55        *target.entry(sym.clone()).or_insert(0.0) += p_i * w;
56    }
57    target
58}
59
60pub struct OptimalWeights {
61    pub p_m: f64,
62    pub p_i: f64,
63    pub a1: f64,
64    pub a2: f64,
65    pub a3: f64,
66    pub a4: f64,
67    pub p_ratio: f64,
68}
69
70pub fn compute_optimal_weights(
71    t_val: f64,
72    n_val: f64,
73    sd_i: f64,
74    sd_m: f64,
75    sd_t: f64,
76    sd_n: f64,
77    o_m: f64,
78    o_i: f64,
79    o_j: f64,
80    o_mn: f64,
81) -> OptimalWeights {
82    let p = t_val / (t_val + n_val);
83
84    let o_i_frac = o_i / 100.0;
85    let o_j_frac = o_j / 100.0;
86    let o_m_frac = o_m / 100.0;
87    let o_mn_frac = o_mn / 100.0;
88
89    // Each entry: fund_SD × (1 − overlap) × account_importance_weight × account_SD
90    // Weighting against overlap so underserved channels receive more allocation.
91    let a1_raw = sd_i * (1.0 - o_i_frac) * p * sd_t; // Intl  ∩ Taxable
92    let a2_raw = sd_i * (1.0 - o_j_frac) * (1.0 - p) * sd_n; // Intl  ∩ Non-Taxable
93    let a3_raw = sd_m * (1.0 - o_m_frac) * p * sd_t; // Mom   ∩ Taxable
94    let a4_raw = sd_m * (1.0 - o_mn_frac) * (1.0 - p) * sd_n; // Mom   ∩ Non-Taxable
95
96    let z = a1_raw + a2_raw + a3_raw + a4_raw;
97    let (a1, a2, a3, a4) = if z > 1e-10 {
98        (a1_raw / z, a2_raw / z, a3_raw / z, a4_raw / z)
99    } else {
100        (0.25, 0.25, 0.25, 0.25)
101    };
102
103    let p_m = a3 + a4;
104    let p_i = a1 + a2;
105
106    OptimalWeights {
107        p_m,
108        p_i,
109        a1,
110        a2,
111        a3,
112        a4,
113        p_ratio: p,
114    }
115}