Skip to main content

corp_fin/ratios/
risk_metrics.rs

1use crate::{covariance, mean, sd, variance};
2
3pub struct Beta(f64);
4
5// # Beta
6//
7// ref: https://corporatefinanceinstitute.com/resources/valuation/what-is-beta-guide/
8//
9// Symbol: β
10//
11// The beta coefficient can be interpreted as follows:
12//
13//     β = 1: exactly as volatile as the market
14//     β > 1: more volatile than the market (higher risk and potential return)
15//     β < 1 (but > 0): less volatile than the market
16//     β = 0: uncorrelated to the market
17//     β < 0: negatively correlated to the market
18//
19// ## Formula
20//
21// Beta = Covariance (Re, Rm) / Variance (Rm)
22//
23impl Beta {
24    pub fn new(series: &[f64], market: &[f64]) -> Self {
25        // If they don't match, the math is technically invalid for a specific timeframe
26        assert_eq!(
27            series.len(),
28            market.len(),
29            "series[{}] and market[{}] must have the same mumber of data points",
30            series.len(),
31            market.len()
32        );
33
34        if series.is_empty() {
35            return Self(0.0);
36        }
37
38        let beta = covariance!(series, market) / variance!(market);
39        Self(beta)
40    }
41
42    // if Beta > 1.0 => The fund is more volatile than the market
43    /// If true, it means the fund moves exactly with the market.
44    pub const fn is_one(&self) -> bool {
45        // self.0 == 1.00 - this is risky due to precision errors
46        // check if the difference is within a tiny margin (epsilon)
47        (self.0 - 1.0).abs() < 1e-6 // or f64::EPSILON? isn't EPSILON way too small for
48        // financial calculations
49    }
50
51    /// The fund is "defensive" and moves less than the market
52    pub const fn is_negative(&self) -> bool {
53        self.0.is_sign_negative()
54    }
55
56    pub const fn is_positive(&self) -> bool {
57        self.0.is_sign_positive()
58    }
59
60    pub const fn value(&self) -> f64 {
61        self.0
62    }
63}
64
65impl From<f64> for Beta {
66    fn from(value: f64) -> Self {
67        Self(value)
68    }
69}
70
71impl From<Beta> for f64 {
72    fn from(value: Beta) -> Self {
73        value.0
74    }
75}
76
77/// # Sharpe ratio
78///
79/// ## Formula:
80///
81/// sharpe = (mean risk - risk free return) / std deviation of mean risk
82///
83/// ## Usage
84///
85/// sharpe(series, rf);
86///
87/// where:
88///     `series: &[f64]` - portfolio return as slice Item = % return (not absolute return)
89///     `rf: f64` - risk free return
90///
91/// ## Grading Thresholds
92///
93/// ```text
94///     Less than 1: Bad
95///     1 – 1.99: Adequate/good
96///     2 – 2.99: Very good
97///     Greater than 3: Excellent
98/// ```
99///
100/// ref: [Sharpe Ratio](https://corporatefinanceinstitute.com/resources/career-map/sell-side/risk-management/sharpe-ratio-definition-formula/)
101///
102/// # See also
103///
104/// [`sharpe!`] macro (more feature rich)
105pub fn sharpe(series: &[f64], rf: f64) -> f64 {
106    internal_sharpe(Some(series), rf, None, None)
107}
108
109fn internal_sharpe(series: Option<&[f64]>, rf: f64, rp: Option<f64>, sd: Option<f64>) -> f64 {
110    let portfolio_ret: f64;
111    let std_div: f64;
112    if let Some(series) = series {
113        portfolio_ret = mean!(series);
114        std_div = sd!(variance!(series, portfolio_ret));
115    } else {
116        assert!(rp.is_some(), "");
117        assert!(sd.is_some(), "");
118        portfolio_ret = rp.unwrap();
119        std_div = sd.unwrap();
120    }
121
122    if std_div < f64::EPSILON {
123        return 0.0;
124    }
125
126    (portfolio_ret - rf) / std_div
127}
128
129/// # Sharpe ratio
130///
131/// ## Formula:
132///
133/// sharpe = (mean risk - risk free return) / std deviation of mean risk
134///
135/// ## Usage
136///
137/// 1. sharpe!(series, rf);
138/// 2. sharpe!(rp, rf, sd);
139///
140/// where:
141///     `series: &[f64]` - portfolio return as slice Item = % return (not absolute return)
142///     `rf: f64` - risk free return
143///     `rp: f64` - portfolio return
144///     `sd: f64` - standard deviation
145///
146/// ## Grading Thresholds
147/// ```text
148///     Less than 1: Bad
149///     1 – 1.99: Adequate/good
150///     2 – 2.99: Very good
151///     Greater than 3: Excellent
152/// ```
153/// ref: [Sharpe Ratio](https://corporatefinanceinstitute.com/resources/career-map/sell-side/risk-management/sharpe-ratio-definition-formula/)
154#[macro_export]
155macro_rules! sharpe {
156    ($series: expr, $rf: expr) => {
157        $crate::ratios::risk_metrics::internal_sharpe(Some($series), $rf, None, None)
158    };
159    ($rp: expr, $rf: expr, $sd: expr) => {
160        $crate::ratios::risk_metrics::internal_sharpe(None, $rf, Some($rp), Some($sd))
161    };
162}
163
164// number of observation / number of year are same as the x.len()
165pub fn downside_deviation(x: &[f64], mar: f64) -> f64 {
166    let no_of_year = x.len() as f64;
167    let result = x
168        .iter()
169        .map(|xi| {
170            let diff = xi - mar;
171            if diff.is_sign_negative() {
172                diff * diff
173            } else {
174                0.0
175            }
176        })
177        .sum::<f64>();
178    let re = result / no_of_year;
179    re.sqrt()
180}
181
182#[cfg(test)]
183mod test {
184    use crate::F64Extras;
185    use crate::ratios::risk_metrics::sharpe;
186
187    use super::{Beta, downside_deviation};
188
189    // in the month of Jan 2026
190    const NIFTY_50: [f64; 19] = [
191        0.006960765170238605,
192        -0.002972058760474052,
193        -0.002727647317136396,
194        -0.0014496220164682432,
195        -0.01009536415844993,
196        -0.007479613285493556,
197        0.004164153963733427,
198        -0.0022469428853927357,
199        -0.0025921184600641006,
200        0.0011201764399651254,
201        -0.0042363247573810724,
202        -0.013796877137441129,
203        -0.002972357079163777,
204        0.0052628596094604,
205        -0.009539381186706124,
206        0.005060152863462813,
207        0.006647346488174181,
208        0.003004819548983437,
209        -0.003865234077404724,
210    ];
211    // in the month of Jan 2026
212    const ITC: [f64; 19] = [
213        -0.03792780822846855,
214        -0.0009997348459663343,
215        -0.02073202469727801,
216        -0.0035042128799998833,
217        -0.001025698054907989,
218        -0.011000239733699091,
219        0.003707530300019869,
220        -0.0109337303722129,
221        0.000149316018136497,
222        -0.016579515057863883,
223        0.012150627231730476,
224        -0.02070828665292772,
225        -0.00475024862225316,
226        0.00030797372763436743,
227        -0.004463668706180607,
228        -0.014687699127850123,
229        0.007845658409425468,
230        -0.007940199219514013,
231        0.011142447553835816,
232    ];
233    #[test]
234    fn beta_t() {
235        let beta: f64 = Beta::new(&ITC, &NIFTY_50).into();
236        assert_eq!(beta, -0.13098715705340794);
237    }
238
239    #[test]
240    fn sharpe_t() {
241        let protfolio_return = 0.18; // one year return = 18%
242
243        let rf = 0.03; // risk free return = 3%
244        let annaulized_sd = 0.12;
245
246        let s1 = sharpe!(&ITC, rf);
247        let s3 = sharpe(&ITC, rf);
248        let s2 = sharpe!(protfolio_return, rf, annaulized_sd);
249        assert_eq!(s2, 1.25);
250        assert_eq!(s1, -3.024907069875915);
251        assert_eq!(s3, -3.024907069875915);
252    }
253
254    // ref: https://www.investopedia.com/terms/d/downside-deviation.asp
255    #[test]
256    fn downside_t() {
257        // downside deviation input data
258        let _years = [2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019];
259
260        let returns = [-0.02, 0.16, 0.31, 0.17, -0.11, 0.21, 0.26, -0.03, 0.38];
261        let mar = 0.01;
262
263        let dd = downside_deviation(&returns, mar);
264        assert_eq!(dd.round_4(), 0.0433)
265    }
266}