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