barter/statistic/metric/
sharpe.rs

1use crate::statistic::time::TimeInterval;
2use rust_decimal::{Decimal, MathematicalOps};
3use serde::{Deserialize, Serialize};
4
5/// Represents a Sharpe Ratio value over a specific [`TimeInterval`].
6///
7/// Sharpe Ratio measures the risk-adjusted return of an investment by comparing
8/// its excess returns (over risk-free rate) to its standard deviation.
9///
10/// See docs: <https://www.investopedia.com/articles/07/sharpe_ratio.asp>
11#[derive(Debug, Clone, PartialEq, PartialOrd, Default, Deserialize, Serialize)]
12pub struct SharpeRatio<Interval> {
13    pub value: Decimal,
14    pub interval: Interval,
15}
16
17impl<Interval> SharpeRatio<Interval>
18where
19    Interval: TimeInterval,
20{
21    /// Calculate the [`SharpeRatio`] over the provided [`TimeInterval`].
22    pub fn calculate(
23        risk_free_return: Decimal,
24        mean_return: Decimal,
25        std_dev_returns: Decimal,
26        returns_period: Interval,
27    ) -> Self {
28        if std_dev_returns.is_zero() {
29            Self {
30                value: Decimal::MAX,
31                interval: returns_period,
32            }
33        } else {
34            let excess_returns = mean_return - risk_free_return;
35            let ratio = excess_returns.checked_div(std_dev_returns).unwrap();
36            Self {
37                value: ratio,
38                interval: returns_period,
39            }
40        }
41    }
42
43    /// Scale the [`SharpeRatio`] from the current [`TimeInterval`] to the provided [`TimeInterval`].
44    ///
45    /// This scaling assumed the returns are independently and identically distributed (IID).
46    pub fn scale<TargetInterval>(self, target: TargetInterval) -> SharpeRatio<TargetInterval>
47    where
48        TargetInterval: TimeInterval,
49    {
50        // Determine scale factor: square root of number of Self Intervals in TargetIntervals
51        let target_secs = Decimal::from(target.interval().num_seconds());
52        let current_secs = Decimal::from(self.interval.interval().num_seconds());
53
54        let scale = target_secs
55            .abs()
56            .checked_div(current_secs.abs())
57            .unwrap_or(Decimal::MAX)
58            .sqrt()
59            .expect("ensured seconds are Positive");
60
61        SharpeRatio {
62            value: self.value.checked_mul(scale).unwrap_or(Decimal::MAX),
63            interval: target,
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::statistic::time::{Annual252, Daily};
72    use chrono::TimeDelta;
73    use rust_decimal_macros::dec;
74
75    #[test]
76    fn test_sharpe_ratio_with_zero_std_dev() {
77        let risk_free_return = dec!(0.001);
78        let mean_return = dec!(0.002);
79        let std_dev_returns = dec!(0.0);
80        let time_period = TimeDelta::hours(2);
81
82        let result =
83            SharpeRatio::calculate(risk_free_return, mean_return, std_dev_returns, time_period);
84        assert_eq!(result.value, Decimal::MAX);
85    }
86
87    #[test]
88    fn test_sharpe_ratio_calculate_with_custom_interval() {
89        // Define custom interval returns statistics
90        let risk_free_return = dec!(0.0015); // 0.15%
91        let mean_return = dec!(0.0025); // 0.25%
92        let std_dev_returns = dec!(0.02); // 2%
93        let time_period = TimeDelta::hours(2);
94
95        let actual =
96            SharpeRatio::calculate(risk_free_return, mean_return, std_dev_returns, time_period);
97
98        let expected = SharpeRatio {
99            value: dec!(0.05),
100            interval: time_period,
101        };
102
103        assert_eq!(actual.value, expected.value);
104        assert_eq!(actual.interval, expected.interval);
105    }
106
107    #[test]
108    fn test_sharpe_ratio_calculate_with_daily_interval() {
109        // Define daily returns statistics
110        let risk_free_return = dec!(0.0015); // 0.15%
111        let mean_return = dec!(0.0025); // 0.25%
112        let std_dev_returns = dec!(0.02); // 2%
113        let time_period = Daily;
114
115        let actual =
116            SharpeRatio::calculate(risk_free_return, mean_return, std_dev_returns, time_period);
117
118        let expected = SharpeRatio {
119            value: dec!(0.05),
120            interval: time_period,
121        };
122
123        assert_eq!(actual.value, expected.value);
124        assert_eq!(actual.interval, expected.interval);
125    }
126
127    #[test]
128    fn test_sharpe_ratio_scale_from_daily_to_annual_252() {
129        let input = SharpeRatio {
130            value: dec!(0.05),
131            interval: Daily,
132        };
133
134        let actual = input.scale(Annual252);
135
136        let expected = SharpeRatio {
137            value: dec!(0.7937253933193771771504847261),
138            interval: Annual252,
139        };
140
141        assert_eq!(actual.value, expected.value);
142        assert_eq!(actual.interval, expected.interval);
143    }
144}