1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
use rust_decimal::Decimal;
use std::str::FromStr;
/// Risk management configuration
#[derive(Debug, Clone)]
pub struct RiskConfig {
/// Maximum risk per trade as percentage of account (e.g., 0.02 = 2%)
pub max_risk_per_trade: Decimal,
/// Maximum position size in base currency
pub max_position_size: Decimal,
/// Maximum leverage allowed
pub max_leverage: u32,
/// Maximum number of concurrent positions
pub max_positions: usize,
}
impl Default for RiskConfig {
fn default() -> Self {
RiskConfig {
max_risk_per_trade: Decimal::from_str("0.02").unwrap(), // 2%
max_position_size: Decimal::from_str("1.0").unwrap(), // 1 BTC
max_leverage: 10,
max_positions: 3,
}
}
}
/// Position sizing calculator
pub struct PositionSizer {
config: RiskConfig,
}
impl PositionSizer {
pub fn new(config: RiskConfig) -> Self {
PositionSizer { config }
}
/// Calculate position size based on fixed percentage risk
///
/// Formula: size = (account_balance * risk_percent) / (entry_price * stop_loss_percent)
pub fn fixed_percentage(
&self,
account_balance: Decimal,
entry_price: Decimal,
stop_loss_percent: Decimal,
) -> Decimal {
let risk_amount = account_balance * self.config.max_risk_per_trade;
let stop_loss_amount = entry_price * stop_loss_percent;
if stop_loss_amount.is_zero() {
return Decimal::ZERO;
}
let size = risk_amount / stop_loss_amount;
// Apply maximum position size limit
if size > self.config.max_position_size {
self.config.max_position_size
} else {
size
}
}
/// Kelly Criterion position sizing
///
/// Formula: f* = (bp - q) / b
/// where: b = win/loss ratio, p = win probability, q = loss probability
pub fn kelly_criterion(
&self,
account_balance: Decimal,
win_rate: Decimal, // e.g., 0.55 = 55%
avg_win: Decimal, // average winning trade size
avg_loss: Decimal, // average losing trade size
) -> Decimal {
if avg_loss.is_zero() {
return Decimal::ZERO;
}
let b = avg_win / avg_loss; // Win/loss ratio
let p = win_rate;
let q = Decimal::ONE - win_rate;
// Kelly fraction: f* = (bp - q) / b
let kelly_fraction = (b * p - q) / b;
// Use half-Kelly for safety
let safe_fraction = kelly_fraction / Decimal::from(2);
if safe_fraction <= Decimal::ZERO {
return Decimal::ZERO;
}
let size = account_balance * safe_fraction;
// Apply maximum limit
if size > self.config.max_position_size {
self.config.max_position_size
} else {
size
}
}
/// Check if we can open a new position given current positions count
pub fn can_open_position(&self, current_positions: usize) -> bool {
current_positions < self.config.max_positions
}
/// Validate leverage is within limits
pub fn validate_leverage(&self, leverage: u32) -> u32 {
if leverage > self.config.max_leverage {
self.config.max_leverage
} else {
leverage
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fixed_percentage() {
let sizer = PositionSizer::new(RiskConfig::default());
let size = sizer.fixed_percentage(
Decimal::from(10000), // $10k account
Decimal::from(50000), // $50k BTC
Decimal::from_str("0.02").unwrap(), // 2% stop loss
);
// Risk = $10k * 2% = $200
// Stop loss = $50k * 2% = $1000
// Size = $200 / $1000 = 0.2 BTC
assert_eq!(size, Decimal::from_str("0.2").unwrap());
}
}