1use std::fmt;
12
13use rust_decimal::Decimal;
14
15use crate::MetricsError;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum KellyMode {
20 Full,
22 Half,
24 Quarter,
26}
27
28impl KellyMode {
29 pub fn scale(&self) -> Decimal {
31 match self {
32 Self::Full => Decimal::ONE,
33 Self::Half => Decimal::new(5, 1), Self::Quarter => Decimal::new(25, 2), }
36 }
37}
38
39impl fmt::Display for KellyMode {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::Full => write!(f, "Full"),
43 Self::Half => write!(f, "Half"),
44 Self::Quarter => write!(f, "Quarter"),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
53pub struct KellyFraction(Decimal);
54
55impl KellyFraction {
56 pub fn zero() -> Self {
58 Self(Decimal::ZERO)
59 }
60
61 pub fn as_decimal(&self) -> Decimal {
63 self.0
64 }
65
66 pub fn is_zero(&self) -> bool {
68 self.0 == Decimal::ZERO
69 }
70}
71
72impl fmt::Display for KellyFraction {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{:.4}", self.0)
75 }
76}
77
78pub fn compute_kelly_fraction(
91 win_rate: Decimal,
92 avg_win_loss_ratio: Decimal,
93 mode: KellyMode,
94) -> KellyFraction {
95 if avg_win_loss_ratio <= Decimal::ZERO {
97 return KellyFraction::zero();
98 }
99
100 let loss_rate = Decimal::ONE - win_rate;
102 let raw_fraction = win_rate - loss_rate / avg_win_loss_ratio;
103
104 if raw_fraction <= Decimal::ZERO {
106 return KellyFraction::zero();
107 }
108
109 let scaled = raw_fraction * mode.scale();
111
112 let clamped = if scaled > Decimal::ONE {
114 Decimal::ONE
115 } else {
116 scaled
117 };
118
119 KellyFraction(clamped)
120}
121
122pub fn compute_kelly_inputs(
128 trade_pnls: &[Decimal],
129) -> Result<(Decimal, Decimal, usize), MetricsError> {
130 if trade_pnls.is_empty() {
131 return Err(MetricsError::InsufficientData {
132 required: 1,
133 actual: 0,
134 });
135 }
136
137 let wr_pct = crate::win_rate(trade_pnls)?;
139 let wr_fraction = wr_pct / Decimal::from(100);
140
141 let avg_w = crate::avg_win(trade_pnls)?;
142
143 let avg_l = match crate::avg_loss(trade_pnls) {
144 Ok(v) => v.abs(),
145 Err(MetricsError::InsufficientData { .. }) => {
146 return Err(MetricsError::DivisionByZero {
148 context: "no losing trades — cannot compute win/loss ratio",
149 });
150 }
151 Err(e) => return Err(e),
152 };
153
154 let ratio = avg_w / avg_l;
155
156 Ok((wr_fraction, ratio, trade_pnls.len()))
157}
158
159#[cfg(test)]
160#[path = "kelly_tests.rs"]
161mod tests;