1use nautilus_model::{
18 instruments::InstrumentAny,
19 types::{Money, Price, Quantity},
20};
21use rust_decimal::{
22 Decimal,
23 prelude::{FromPrimitive, ToPrimitive},
24};
25
26#[must_use]
27#[allow(clippy::too_many_arguments)]
28pub fn calculate_fixed_risk_position_size(
29 instrument: InstrumentAny,
30 entry: Price,
31 stop_loss: Price,
32 equity: Money,
33 risk: Decimal,
34 commission_rate: Decimal,
35 exchange_rate: Decimal,
36 hard_limit: Option<Decimal>,
37 unit_batch_size: Decimal,
38 units: usize,
39) -> Quantity {
40 if exchange_rate.is_zero() {
41 return instrument.make_qty(0.0);
42 }
43
44 let risk_points = calculate_risk_ticks(entry, stop_loss, &instrument);
45 let risk_money = calculate_riskable_money(equity.as_decimal(), risk, commission_rate);
46
47 if risk_points <= Decimal::ZERO {
48 return instrument.make_qty(0.0);
49 }
50
51 let mut position_size =
52 ((risk_money / exchange_rate) / risk_points) / instrument.price_increment().as_decimal();
53
54 if let Some(hard_limit) = hard_limit {
55 position_size = position_size.min(hard_limit);
56 }
57
58 let mut position_size_batched = (position_size
59 / Decimal::from_usize(units).expect("Error: Failed to convert units to decimal"))
60 .max(Decimal::ZERO);
61
62 if unit_batch_size > Decimal::ZERO {
63 position_size_batched = (position_size_batched / unit_batch_size).floor() * unit_batch_size;
64 }
65
66 let final_size: Decimal = position_size_batched.min(
67 instrument
68 .max_quantity()
69 .unwrap_or_else(|| instrument.make_qty(0.0))
70 .as_decimal(),
71 );
72
73 Quantity::new(
74 final_size
75 .to_f64()
76 .expect("Error: Decimal to f64 conversion failed"),
77 instrument.size_precision(),
78 )
79}
80
81fn calculate_risk_ticks(entry: Price, stop_loss: Price, instrument: &InstrumentAny) -> Decimal {
83 (entry - stop_loss).as_decimal().abs() / instrument.price_increment().as_decimal()
84}
85
86fn calculate_riskable_money(equity: Decimal, risk: Decimal, commission_rate: Decimal) -> Decimal {
87 if equity <= Decimal::ZERO {
88 return Decimal::ZERO;
89 }
90
91 let risk_money = equity * risk;
92 let commission = risk_money * commission_rate * Decimal::TWO; risk_money - commission
95}
96
97#[cfg(test)]
98mod tests {
99 use nautilus_model::{
100 identifiers::Symbol, instruments::stubs::default_fx_ccy, types::Currency,
101 };
102 use rstest::*;
103
104 use super::*;
105
106 const EXCHANGE_RATE: Decimal = Decimal::ONE;
107
108 #[fixture]
109 fn instrument_gbpusd() -> InstrumentAny {
110 InstrumentAny::CurrencyPair(default_fx_ccy(Symbol::from_str_unchecked("GBP/USD"), None))
111 }
112
113 #[rstest]
114 fn test_calculate_with_zero_equity_returns_quantity_zero(instrument_gbpusd: InstrumentAny) {
115 let equity = Money::new(0.0, instrument_gbpusd.quote_currency());
116 let entry = Price::new(1.00100, instrument_gbpusd.price_precision());
117 let stop_loss = Price::new(1.00000, instrument_gbpusd.price_precision());
118
119 let result = calculate_fixed_risk_position_size(
120 instrument_gbpusd,
121 entry,
122 stop_loss,
123 equity,
124 Decimal::new(1, 3), Decimal::ZERO,
126 EXCHANGE_RATE,
127 None,
128 Decimal::from(1000),
129 1,
130 );
131
132 assert_eq!(result.as_f64(), 0.0);
133 }
134
135 #[rstest]
136 fn test_calculate_with_zero_exchange_rate(instrument_gbpusd: InstrumentAny) {
137 let equity = Money::new(100000.0, instrument_gbpusd.quote_currency());
138 let entry = Price::new(1.00100, instrument_gbpusd.price_precision());
139 let stop_loss = Price::new(1.00000, instrument_gbpusd.price_precision());
140
141 let result = calculate_fixed_risk_position_size(
142 instrument_gbpusd,
143 entry,
144 stop_loss,
145 equity,
146 Decimal::new(1, 3), Decimal::ZERO,
148 Decimal::ZERO, None,
150 Decimal::from(1000),
151 1,
152 );
153
154 assert_eq!(result.as_f64(), 0.0);
155 }
156
157 #[rstest]
158 fn test_calculate_with_zero_risk(instrument_gbpusd: InstrumentAny) {
159 let equity = Money::new(100000.0, instrument_gbpusd.quote_currency());
160 let price = Price::new(1.00100, instrument_gbpusd.price_precision());
161
162 let result = calculate_fixed_risk_position_size(
163 instrument_gbpusd,
164 price,
165 price, equity,
167 Decimal::new(1, 3), Decimal::ZERO,
169 EXCHANGE_RATE,
170 None,
171 Decimal::from(1000),
172 1,
173 );
174
175 assert_eq!(result.as_f64(), 0.0);
176 }
177
178 #[rstest]
179 fn test_calculate_single_unit_size(instrument_gbpusd: InstrumentAny) {
180 let equity = Money::new(1_000_000.0, instrument_gbpusd.quote_currency());
181 let entry = Price::new(1.00100, instrument_gbpusd.price_precision());
182 let stop_loss = Price::new(1.00000, instrument_gbpusd.price_precision());
183
184 let result = calculate_fixed_risk_position_size(
185 instrument_gbpusd,
186 entry,
187 stop_loss,
188 equity,
189 Decimal::new(1, 3), Decimal::ZERO,
191 EXCHANGE_RATE,
192 None,
193 Decimal::from(1000),
194 1,
195 );
196
197 assert_eq!(result.as_f64(), 1_000_000.0);
198 }
199
200 #[rstest]
201 fn test_calculate_single_unit_with_exchange_rate(instrument_gbpusd: InstrumentAny) {
202 let equity = Money::new(1_000_000.0, Currency::USD());
203 let entry = Price::new(110.010, instrument_gbpusd.price_precision());
204 let stop_loss = Price::new(110.000, instrument_gbpusd.price_precision());
205
206 let result = calculate_fixed_risk_position_size(
207 instrument_gbpusd,
208 entry,
209 stop_loss,
210 equity,
211 Decimal::new(1, 3), Decimal::ZERO,
213 Decimal::from_f64(0.00909).unwrap(), None,
215 Decimal::from(1),
216 1,
217 );
218
219 assert_eq!(result.as_f64(), 1_000_000.0);
220 }
221
222 #[rstest]
223 fn test_calculate_single_unit_size_when_risk_too_high(instrument_gbpusd: InstrumentAny) {
224 let equity = Money::new(100000.0, Currency::USD());
225 let entry = Price::new(3.00000, instrument_gbpusd.price_precision());
226 let stop_loss = Price::new(1.00000, instrument_gbpusd.price_precision());
227
228 let result = calculate_fixed_risk_position_size(
229 instrument_gbpusd,
230 entry,
231 stop_loss,
232 equity,
233 Decimal::new(1, 2), Decimal::ZERO,
235 EXCHANGE_RATE,
236 None,
237 Decimal::from(1000),
238 1,
239 );
240
241 assert_eq!(result.as_f64(), 0.0);
242 }
243
244 #[rstest]
245 fn test_impose_hard_limit(instrument_gbpusd: InstrumentAny) {
246 let equity = Money::new(1_000_000.0, instrument_gbpusd.quote_currency());
247 let entry = Price::new(1.00010, instrument_gbpusd.price_precision());
248 let stop_loss = Price::new(1.00000, instrument_gbpusd.price_precision());
249
250 let result = calculate_fixed_risk_position_size(
251 instrument_gbpusd,
252 entry,
253 stop_loss,
254 equity,
255 Decimal::new(1, 2), Decimal::ZERO,
257 EXCHANGE_RATE,
258 Some(Decimal::from(500000)),
259 Decimal::from(1000),
260 1,
261 );
262
263 assert_eq!(result.as_f64(), 500_000.0);
264 }
265
266 #[rstest]
267 fn test_calculate_multiple_unit_size(instrument_gbpusd: InstrumentAny) {
268 let equity = Money::new(1_000_000.0, instrument_gbpusd.quote_currency());
269 let entry = Price::new(1.00010, instrument_gbpusd.price_precision());
270 let stop_loss = Price::new(1.00000, instrument_gbpusd.price_precision());
271
272 let result = calculate_fixed_risk_position_size(
273 instrument_gbpusd,
274 entry,
275 stop_loss,
276 equity,
277 Decimal::new(1, 3), Decimal::ZERO,
279 EXCHANGE_RATE,
280 None,
281 Decimal::from(1000),
282 3, );
284
285 assert_eq!(result.as_f64(), 1000000.0);
286 }
287
288 #[rstest]
289 fn test_calculate_multiple_unit_size_larger_batches(instrument_gbpusd: InstrumentAny) {
290 let equity = Money::new(1_000_000.0, instrument_gbpusd.quote_currency());
291 let entry = Price::new(1.00087, instrument_gbpusd.price_precision());
292 let stop_loss = Price::new(1.00000, instrument_gbpusd.price_precision());
293
294 let result = calculate_fixed_risk_position_size(
295 instrument_gbpusd,
296 entry,
297 stop_loss,
298 equity,
299 Decimal::new(1, 3), Decimal::ZERO,
301 EXCHANGE_RATE,
302 None,
303 Decimal::from(25000),
304 4, );
306
307 assert_eq!(result.as_f64(), 275000.0);
308 }
309
310 #[rstest]
311 fn test_calculate_for_gbpusd_with_commission(instrument_gbpusd: InstrumentAny) {
312 let equity = Money::new(1_000_000.0, instrument_gbpusd.quote_currency());
313 let entry = Price::new(107.703, instrument_gbpusd.price_precision());
314 let stop_loss = Price::new(107.403, instrument_gbpusd.price_precision());
315
316 let result = calculate_fixed_risk_position_size(
317 instrument_gbpusd,
318 entry,
319 stop_loss,
320 equity,
321 Decimal::new(1, 2), Decimal::new(2, 4), Decimal::from_f64(0.009931).unwrap(), None,
325 Decimal::from(1000),
326 1,
327 );
328
329 assert_eq!(result.as_f64(), 1000000.0);
330 }
331}