1use nautilus_core::python::{
19 clone_py_object, to_pynotimplemented_err, to_pyruntime_err, to_pytype_err,
20};
21use nautilus_model::{
22 instruments::InstrumentAny,
23 orders::OrderAny,
24 python::{
25 instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
26 orders::{order_any_to_pyobject, pyobject_to_order_any},
27 },
28 types::{Money, Price, Quantity},
29};
30use pyo3::{
31 IntoPyObject, PyClass,
32 prelude::*,
33 types::{PyDict, PyTuple},
34};
35use rust_decimal::Decimal;
36
37use crate::models::fee::{
38 CappedOptionFeeModel, FeeModel, FeeModelAny, FeeModelHandle, FixedFeeModel, MakerTakerFeeModel,
39 PerContractFeeModel, ProbabilityPriceFeeModel, TieredNotionalOptionFeeModel,
40};
41
42#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")]
43#[pyclass(
44 module = "nautilus_trader.execution",
45 name = "FeeModel",
46 subclass,
47 unsendable
48)]
49#[derive(Debug)]
50pub struct PyFeeModel;
51
52#[pyo3_stub_gen::derive::gen_stub_pymethods]
53#[pymethods]
54impl PyFeeModel {
55 #[new]
56 #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
57 #[pyo3(signature = (*_args, **_kwargs))]
58 fn py_new(_args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>) -> Self {
59 Self
60 }
61
62 fn get_commission(
63 &self,
64 _order: &Bound<'_, PyAny>,
65 _fill_quantity: Quantity,
66 _fill_px: Price,
67 _instrument: &Bound<'_, PyAny>,
68 ) -> PyResult<Money> {
69 Err(to_pynotimplemented_err(
70 "Method 'get_commission' must be implemented in a subclass.",
71 ))
72 }
73
74 #[pyo3(signature = (order, fill_quantity, fill_px, instrument, _underlying_px = None))]
75 fn get_commission_with_context(
76 slf: PyRef<'_, Self>,
77 order: &Bound<'_, PyAny>,
78 fill_quantity: Quantity,
79 fill_px: Price,
80 instrument: &Bound<'_, PyAny>,
81 _underlying_px: Option<Price>,
82 ) -> PyResult<Money> {
83 let py = slf.py();
84 let obj = match slf.into_pyobject(py) {
85 Ok(obj) => obj,
86 Err(e) => match e {},
87 };
88 obj.as_any()
89 .call_method1(
90 "get_commission",
91 (order.clone(), fill_quantity, fill_px, instrument.clone()),
92 )?
93 .extract()
94 .map_err(to_pyruntime_err)
95 }
96}
97
98fn fee_args_to_any(
99 py: Python<'_>,
100 order: &Bound<'_, PyAny>,
101 instrument: &Bound<'_, PyAny>,
102) -> PyResult<(OrderAny, InstrumentAny)> {
103 let instrument_any =
104 pyobject_to_instrument_any(py, instrument.clone().unbind()).map_err(|_| {
105 let type_name = instrument
106 .get_type()
107 .name()
108 .map_or_else(|_| "unknown".to_string(), |name| name.to_string());
109 to_pytype_err(format!(
110 "`instrument` must be an `Instrument`, was `{type_name}`"
111 ))
112 })?;
113 let order_any = pyobject_to_order_any(py, order.clone().unbind()).map_err(|_| {
114 let type_name = order
115 .get_type()
116 .name()
117 .map_or_else(|_| "unknown".to_string(), |name| name.to_string());
118 to_pytype_err(format!("`order` must be an `Order`, was `{type_name}`"))
119 })?;
120 Ok((order_any, instrument_any))
121}
122
123fn call_fee_get_commission<M: FeeModel>(
124 model: &M,
125 py: Python<'_>,
126 order: &Bound<'_, PyAny>,
127 fill_quantity: Quantity,
128 fill_px: Price,
129 instrument: &Bound<'_, PyAny>,
130) -> PyResult<Money> {
131 let (order_any, instrument_any) = fee_args_to_any(py, order, instrument)?;
132 model
133 .get_commission(&order_any, fill_quantity, fill_px, &instrument_any)
134 .map_err(to_pyruntime_err)
135}
136
137fn call_fee_get_commission_with_context<M: FeeModel>(
138 model: &M,
139 py: Python<'_>,
140 order: &Bound<'_, PyAny>,
141 fill_quantity: Quantity,
142 fill_px: Price,
143 instrument: &Bound<'_, PyAny>,
144 underlying_px: Option<Price>,
145) -> PyResult<Money> {
146 let (order_any, instrument_any) = fee_args_to_any(py, order, instrument)?;
147 model
148 .get_commission_with_context(
149 &order_any,
150 fill_quantity,
151 fill_px,
152 &instrument_any,
153 underlying_px,
154 )
155 .map_err(to_pyruntime_err)
156}
157
158#[derive(Debug)]
159pub struct PythonFeeModel {
160 obj: Py<PyAny>,
161}
162
163impl Clone for PythonFeeModel {
164 fn clone(&self) -> Self {
165 Self::new(clone_py_object(&self.obj))
166 }
167}
168
169impl PythonFeeModel {
170 pub fn new(obj: Py<PyAny>) -> Self {
171 Self { obj }
172 }
173}
174
175impl FeeModel for PythonFeeModel {
176 fn get_commission(
177 &self,
178 order: &OrderAny,
179 fill_quantity: Quantity,
180 fill_px: Price,
181 instrument: &InstrumentAny,
182 ) -> anyhow::Result<Money> {
183 Python::attach(|py| -> anyhow::Result<Money> {
184 let order = order_any_to_pyobject(py, order.clone())?;
185 let instrument = instrument_any_to_pyobject(py, instrument.clone())?;
186 self.obj
187 .bind(py)
188 .call_method1(
189 "get_commission",
190 (order, fill_quantity, fill_px, instrument),
191 )?
192 .extract()
193 .map_err(|e| anyhow::anyhow!("{e}"))
194 })
195 .map_err(|e| anyhow::anyhow!("Python FeeModel.get_commission failed: {e}"))
196 }
197
198 fn get_commission_with_context(
199 &self,
200 order: &OrderAny,
201 fill_quantity: Quantity,
202 fill_px: Price,
203 instrument: &InstrumentAny,
204 underlying_px: Option<Price>,
205 ) -> anyhow::Result<Money> {
206 Python::attach(|py| -> anyhow::Result<Money> {
207 let obj = self.obj.bind(py);
208 if !has_method_override_before_base(py, obj, "get_commission_with_context")? {
209 let order = order_any_to_pyobject(py, order.clone())?;
210 let instrument = instrument_any_to_pyobject(py, instrument.clone())?;
211 return obj
212 .call_method1(
213 "get_commission",
214 (order, fill_quantity, fill_px, instrument),
215 )?
216 .extract()
217 .map_err(|e| anyhow::anyhow!("{e}"));
218 }
219
220 let order = order_any_to_pyobject(py, order.clone())?;
221 let instrument = instrument_any_to_pyobject(py, instrument.clone())?;
222 obj.call_method1(
223 "get_commission_with_context",
224 (order, fill_quantity, fill_px, instrument, underlying_px),
225 )?
226 .extract()
227 .map_err(|e| anyhow::anyhow!("{e}"))
228 })
229 .map_err(|e| anyhow::anyhow!("Python FeeModel.get_commission_with_context failed: {e}"))
230 }
231}
232
233fn has_method_override_before_base(
234 py: Python<'_>,
235 obj: &Bound<'_, PyAny>,
236 method_name: &str,
237) -> PyResult<bool> {
238 let base_type = py.get_type::<PyFeeModel>();
239 for cls in obj.get_type().getattr("__mro__")?.try_iter()? {
240 let cls = cls?;
241 if cls.is(base_type.as_any()) {
242 return Ok(false);
243 }
244
245 if cls.getattr("__dict__")?.contains(method_name)? {
246 return Ok(true);
247 }
248 }
249
250 Ok(false)
251}
252
253#[pyo3_stub_gen::derive::gen_stub_pymethods]
254#[pymethods]
255#[expect(
256 clippy::use_self,
257 reason = "`Self` breaks pyo3-stub-gen derive for subclass pyclasses"
258)]
259impl FixedFeeModel {
260 #[new]
266 #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
267 #[pyo3(signature = (commission, charge_commission_once=None, change_commission_once=None))]
268 fn py_new(
269 commission: Money,
270 charge_commission_once: Option<bool>,
271 change_commission_once: Option<bool>,
272 ) -> PyResult<PyClassInitializer<FixedFeeModel>> {
273 let charge_commission_once = resolve_fixed_fee_charge_commission_once(
274 charge_commission_once,
275 change_commission_once,
276 )?;
277 let model = Self::new(commission, charge_commission_once).map_err(to_pyruntime_err)?;
278 Ok(PyClassInitializer::from(PyFeeModel).add_subclass(model))
279 }
280
281 fn __repr__(&self) -> String {
282 format!("{self:?}")
283 }
284
285 fn get_commission(
286 &self,
287 order: &Bound<'_, PyAny>,
288 fill_quantity: Quantity,
289 fill_px: Price,
290 instrument: &Bound<'_, PyAny>,
291 ) -> PyResult<Money> {
292 call_fee_get_commission(self, order.py(), order, fill_quantity, fill_px, instrument)
293 }
294}
295
296fn resolve_fixed_fee_charge_commission_once(
297 charge_commission_once: Option<bool>,
298 change_commission_once: Option<bool>,
299) -> PyResult<Option<bool>> {
300 if charge_commission_once.is_some() && change_commission_once.is_some() {
301 return Err(to_pytype_err(
302 "Provide only one of `charge_commission_once` or `change_commission_once`",
303 ));
304 }
305
306 Ok(charge_commission_once.or(change_commission_once))
307}
308
309#[pyo3_stub_gen::derive::gen_stub_pymethods]
310#[pymethods]
311#[expect(
312 clippy::use_self,
313 reason = "`Self` breaks pyo3-stub-gen derive for subclass pyclasses"
314)]
315impl MakerTakerFeeModel {
316 #[new]
317 #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
318 fn py_new() -> PyClassInitializer<MakerTakerFeeModel> {
319 PyClassInitializer::from(PyFeeModel).add_subclass(MakerTakerFeeModel)
320 }
321
322 fn __repr__(&self) -> String {
323 format!("{self:?}")
324 }
325
326 fn get_commission(
327 &self,
328 order: &Bound<'_, PyAny>,
329 fill_quantity: Quantity,
330 fill_px: Price,
331 instrument: &Bound<'_, PyAny>,
332 ) -> PyResult<Money> {
333 call_fee_get_commission(self, order.py(), order, fill_quantity, fill_px, instrument)
334 }
335}
336
337#[pyo3_stub_gen::derive::gen_stub_pymethods]
338#[pymethods]
339#[expect(
340 clippy::use_self,
341 reason = "`Self` breaks pyo3-stub-gen derive for subclass pyclasses"
342)]
343impl PerContractFeeModel {
344 #[new]
350 #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
351 fn py_new(commission: Money) -> PyResult<PyClassInitializer<PerContractFeeModel>> {
352 let model = Self::new(commission).map_err(to_pyruntime_err)?;
353 Ok(PyClassInitializer::from(PyFeeModel).add_subclass(model))
354 }
355
356 fn __repr__(&self) -> String {
357 format!("{self:?}")
358 }
359
360 fn get_commission(
361 &self,
362 order: &Bound<'_, PyAny>,
363 fill_quantity: Quantity,
364 fill_px: Price,
365 instrument: &Bound<'_, PyAny>,
366 ) -> PyResult<Money> {
367 call_fee_get_commission(self, order.py(), order, fill_quantity, fill_px, instrument)
368 }
369}
370
371#[pyo3_stub_gen::derive::gen_stub_pymethods]
372#[pymethods]
373#[expect(
374 clippy::use_self,
375 reason = "`Self` breaks pyo3-stub-gen derive for subclass pyclasses"
376)]
377impl ProbabilityPriceFeeModel {
378 #[new]
389 #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
390 fn py_new() -> PyClassInitializer<ProbabilityPriceFeeModel> {
391 PyClassInitializer::from(PyFeeModel).add_subclass(ProbabilityPriceFeeModel)
392 }
393
394 fn __repr__(&self) -> String {
395 format!("{self:?}")
396 }
397
398 fn get_commission(
399 &self,
400 order: &Bound<'_, PyAny>,
401 fill_quantity: Quantity,
402 fill_px: Price,
403 instrument: &Bound<'_, PyAny>,
404 ) -> PyResult<Money> {
405 call_fee_get_commission(self, order.py(), order, fill_quantity, fill_px, instrument)
406 }
407}
408
409#[pyo3_stub_gen::derive::gen_stub_pymethods]
410#[pymethods]
411#[expect(
412 clippy::use_self,
413 reason = "`Self` breaks pyo3-stub-gen derive for subclass pyclasses"
414)]
415impl CappedOptionFeeModel {
416 #[new]
422 #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
423 #[pyo3(signature = (maker_rate=None, taker_rate=None, cap_rate=None))]
424 fn py_new(
425 maker_rate: Option<Decimal>,
426 taker_rate: Option<Decimal>,
427 cap_rate: Option<Decimal>,
428 ) -> PyResult<PyClassInitializer<CappedOptionFeeModel>> {
429 let model = Self::new(maker_rate, taker_rate, cap_rate).map_err(to_pyruntime_err)?;
430 Ok(PyClassInitializer::from(PyFeeModel).add_subclass(model))
431 }
432
433 fn __repr__(&self) -> String {
434 format!("{self:?}")
435 }
436
437 fn get_commission(
438 &self,
439 order: &Bound<'_, PyAny>,
440 fill_quantity: Quantity,
441 fill_px: Price,
442 instrument: &Bound<'_, PyAny>,
443 ) -> PyResult<Money> {
444 call_fee_get_commission(self, order.py(), order, fill_quantity, fill_px, instrument)
445 }
446
447 #[pyo3(signature = (order, fill_quantity, fill_px, instrument, underlying_px = None))]
448 fn get_commission_with_context(
449 &self,
450 order: &Bound<'_, PyAny>,
451 fill_quantity: Quantity,
452 fill_px: Price,
453 instrument: &Bound<'_, PyAny>,
454 underlying_px: Option<Price>,
455 ) -> PyResult<Money> {
456 call_fee_get_commission_with_context(
457 self,
458 order.py(),
459 order,
460 fill_quantity,
461 fill_px,
462 instrument,
463 underlying_px,
464 )
465 }
466}
467
468#[pyo3_stub_gen::derive::gen_stub_pymethods]
469#[pymethods]
470#[expect(
471 clippy::use_self,
472 reason = "`Self` breaks pyo3-stub-gen derive for subclass pyclasses"
473)]
474impl TieredNotionalOptionFeeModel {
475 #[new]
481 #[gen_stub(override_return_type(type_repr = "typing.Self", imports = ("typing",)))]
482 #[pyo3(signature = (maker_rate=None, taker_rate=None))]
483 fn py_new(
484 maker_rate: Option<Decimal>,
485 taker_rate: Option<Decimal>,
486 ) -> PyResult<PyClassInitializer<TieredNotionalOptionFeeModel>> {
487 let model = Self::new(maker_rate, taker_rate).map_err(to_pyruntime_err)?;
488 Ok(PyClassInitializer::from(PyFeeModel).add_subclass(model))
489 }
490
491 fn __repr__(&self) -> String {
492 format!("{self:?}")
493 }
494
495 fn get_commission(
496 &self,
497 order: &Bound<'_, PyAny>,
498 fill_quantity: Quantity,
499 fill_px: Price,
500 instrument: &Bound<'_, PyAny>,
501 ) -> PyResult<Money> {
502 call_fee_get_commission(self, order.py(), order, fill_quantity, fill_px, instrument)
503 }
504}
505
506pub fn pyobject_to_fee_model_any(obj: &Bound<'_, PyAny>) -> PyResult<FeeModelAny> {
513 if let Ok(m) = obj.extract::<PyRef<'_, FixedFeeModel>>() {
514 return Ok(FeeModelAny::Fixed((*m).clone()));
515 }
516
517 if let Ok(m) = obj.extract::<PyRef<'_, MakerTakerFeeModel>>() {
518 return Ok(FeeModelAny::MakerTaker((*m).clone()));
519 }
520
521 if let Ok(m) = obj.extract::<PyRef<'_, PerContractFeeModel>>() {
522 return Ok(FeeModelAny::PerContract((*m).clone()));
523 }
524
525 if let Ok(m) = obj.extract::<PyRef<'_, ProbabilityPriceFeeModel>>() {
526 return Ok(FeeModelAny::ProbabilityPrice((*m).clone()));
527 }
528
529 if let Ok(m) = obj.extract::<PyRef<'_, CappedOptionFeeModel>>() {
530 return Ok(FeeModelAny::CappedOption((*m).clone()));
531 }
532
533 if let Ok(m) = obj.extract::<PyRef<'_, TieredNotionalOptionFeeModel>>() {
534 return Ok(FeeModelAny::TieredNotionalOption((*m).clone()));
535 }
536
537 if !obj.hasattr("get_commission")? {
538 let type_name = obj.get_type().name()?;
539 return Err(to_pytype_err(format!(
540 "Cannot convert {type_name} to FeeModel"
541 )));
542 }
543
544 Ok(FeeModelAny::Python(PythonFeeModel::new(
545 obj.clone().unbind(),
546 )))
547}
548
549pub fn pyobject_to_fee_model_handle(obj: &Bound<'_, PyAny>) -> PyResult<FeeModelHandle> {
556 pyobject_to_fee_model_any(obj).map(Into::into)
557}
558
559fn fee_model_into_py<T>(py: Python<'_>, model: T) -> PyResult<Py<PyAny>>
560where
561 T: PyClass<BaseType = PyFeeModel>,
562{
563 Ok(Py::new(py, PyClassInitializer::from(PyFeeModel).add_subclass(model))?.into_any())
564}
565
566pub fn fee_model_any_to_pyobject(py: Python<'_>, model: &FeeModelAny) -> PyResult<Py<PyAny>> {
572 match model {
573 FeeModelAny::Fixed(model) => fee_model_into_py(py, model.clone()),
574 FeeModelAny::MakerTaker(model) => fee_model_into_py(py, model.clone()),
575 FeeModelAny::PerContract(model) => fee_model_into_py(py, model.clone()),
576 FeeModelAny::ProbabilityPrice(model) => fee_model_into_py(py, model.clone()),
577 FeeModelAny::CappedOption(model) => fee_model_into_py(py, model.clone()),
578 FeeModelAny::TieredNotionalOption(model) => fee_model_into_py(py, model.clone()),
579 FeeModelAny::Python(model) => Ok(model.obj.clone_ref(py)),
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use nautilus_model::{
586 enums::{OrderSide, OrderType},
587 instruments::{Instrument, InstrumentAny, stubs::audusd_sim},
588 orders::{OrderAny, builder::OrderTestBuilder},
589 };
590 use pyo3::{IntoPyObjectExt, ffi::c_str, types::PyDict};
591 use rstest::rstest;
592
593 use super::*;
594
595 #[rstest]
596 fn test_python_fee_model_handle_calls_python_method() {
597 Python::initialize();
598
599 Python::attach(|py| {
600 let expected_commission = Money::from("1.23 USD");
601 let model = fee_model_with_commission(py, expected_commission);
602
603 let handle = pyobject_to_fee_model_handle(&model).unwrap();
604 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
605 let order = OrderTestBuilder::new(OrderType::Market)
606 .instrument_id(instrument.id())
607 .side(OrderSide::Buy)
608 .quantity(Quantity::from(100_000))
609 .build();
610 let commission = handle
611 .get_commission(
612 &order,
613 Quantity::from(100_000),
614 Price::from("0.80000"),
615 &instrument,
616 )
617 .unwrap();
618
619 assert_eq!(commission, expected_commission);
620 });
621 }
622
623 #[rstest]
624 fn test_python_fee_model_any_clones_and_retains_python_model() {
625 Python::initialize();
626
627 Python::attach(|py| {
628 let expected_commission = Money::from("1.23 USD");
629 let model = fee_model_with_commission(py, expected_commission);
630 let fee_model = pyobject_to_fee_model_any(&model).unwrap();
631 let cloned_fee_model = fee_model.clone();
632 let original = fee_model_any_to_pyobject(py, &fee_model).unwrap();
633 let retained = fee_model_any_to_pyobject(py, &cloned_fee_model).unwrap();
634 let (instrument, order) = commission_inputs();
635 let commission = cloned_fee_model
636 .get_commission(
637 &order,
638 Quantity::from(100_000),
639 Price::from("0.80000"),
640 &instrument,
641 )
642 .unwrap();
643
644 assert!(original.bind(py).is(&model));
645 assert!(retained.bind(py).is(&model));
646 assert_eq!(commission, expected_commission);
647 });
648 }
649
650 #[rstest]
651 fn test_python_fee_model_any_rejects_object_without_get_commission() {
652 Python::initialize();
653
654 Python::attach(|py| {
655 let model = PyDict::new(py);
656 let error = pyobject_to_fee_model_any(model.as_any()).unwrap_err();
657
658 assert_eq!(
659 error.to_string(),
660 "TypeError: Cannot convert dict to FeeModel"
661 );
662 });
663 }
664
665 #[rstest]
666 fn test_python_fee_model_context_falls_back_to_get_commission() {
667 Python::initialize();
668
669 Python::attach(|py| {
670 let expected_commission = Money::from("1.23 USD");
671 let locals = PyDict::new(py);
672 locals
673 .set_item("FeeModel", py.get_type::<PyFeeModel>())
674 .unwrap();
675 let model = py
676 .eval(
677 c_str!(
678 "type('CustomFeeModel', (FeeModel,), {\
679 'get_commission': \
680 lambda self, order, fill_quantity, fill_px, instrument: self.commission\
681 })()"
682 ),
683 None,
684 Some(&locals),
685 )
686 .unwrap();
687 model
688 .setattr("commission", expected_commission.into_py_any(py).unwrap())
689 .unwrap();
690
691 let handle = pyobject_to_fee_model_handle(&model).unwrap();
692 let (instrument, order) = commission_inputs();
693 let commission = handle
694 .get_commission_with_context(
695 &order,
696 Quantity::from(100_000),
697 Price::from("0.80000"),
698 &instrument,
699 Some(Price::from("0.70000")),
700 )
701 .unwrap();
702
703 assert_eq!(commission, expected_commission);
704 });
705 }
706
707 #[rstest]
708 fn test_python_fee_model_context_calls_python_override() {
709 Python::initialize();
710
711 Python::attach(|py| {
712 let expected_commission = Money::from("2.34 USD");
713 let locals = PyDict::new(py);
714 locals
715 .set_item("FeeModel", py.get_type::<PyFeeModel>())
716 .unwrap();
717 let model = py
718 .eval(
719 c_str!(
720 "type('CustomFeeModel', (FeeModel,), {\
721 'get_commission': \
722 lambda self, order, fill_quantity, fill_px, instrument: self.base_commission, \
723 'get_commission_with_context': \
724 lambda self, order, fill_quantity, fill_px, instrument, underlying_px=None: self.context_commission\
725 })()"
726 ),
727 None,
728 Some(&locals),
729 )
730 .unwrap();
731 model
732 .setattr(
733 "base_commission",
734 Money::from("1.23 USD").into_py_any(py).unwrap(),
735 )
736 .unwrap();
737 model
738 .setattr(
739 "context_commission",
740 expected_commission.into_py_any(py).unwrap(),
741 )
742 .unwrap();
743
744 let handle = pyobject_to_fee_model_handle(&model).unwrap();
745 let (instrument, order) = commission_inputs();
746 let commission = handle
747 .get_commission_with_context(
748 &order,
749 Quantity::from(100_000),
750 Price::from("0.80000"),
751 &instrument,
752 Some(Price::from("0.70000")),
753 )
754 .unwrap();
755
756 assert_eq!(commission, expected_commission);
757 });
758 }
759
760 #[rstest]
761 fn test_python_fee_model_context_propagates_python_error() {
762 Python::initialize();
763
764 Python::attach(|py| {
765 let locals = PyDict::new(py);
766 locals
767 .set_item("FeeModel", py.get_type::<PyFeeModel>())
768 .unwrap();
769 let model = py
770 .eval(
771 c_str!(
772 "type('CustomFeeModel', (FeeModel,), {\
773 'get_commission': lambda self, order, fill_quantity, fill_px, instrument: \
774 (_ for _ in ()).throw(RuntimeError('boom'))\
775 })()"
776 ),
777 None,
778 Some(&locals),
779 )
780 .unwrap();
781
782 let handle = pyobject_to_fee_model_handle(&model).unwrap();
783 let (instrument, order) = commission_inputs();
784 let error = handle
785 .get_commission_with_context(
786 &order,
787 Quantity::from(100_000),
788 Price::from("0.80000"),
789 &instrument,
790 None,
791 )
792 .unwrap_err();
793 let error = error.to_string();
794
795 assert!(error.contains("Python FeeModel.get_commission_with_context failed"));
796 assert!(error.contains("boom"));
797 });
798 }
799
800 fn commission_inputs() -> (InstrumentAny, OrderAny) {
801 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
802 let order = OrderTestBuilder::new(OrderType::Market)
803 .instrument_id(instrument.id())
804 .side(OrderSide::Buy)
805 .quantity(Quantity::from(100_000))
806 .build();
807
808 (instrument, order)
809 }
810
811 fn fee_model_with_commission(py: Python<'_>, commission: Money) -> Bound<'_, PyAny> {
812 let locals = PyDict::new(py);
813 locals
814 .set_item("FeeModel", py.get_type::<PyFeeModel>())
815 .unwrap();
816 let model = py
817 .eval(
818 c_str!(
819 "type('CustomFeeModel', (FeeModel,), {\
820 'get_commission': \
821 lambda self, order, fill_quantity, fill_px, instrument: self.commission\
822 })()"
823 ),
824 None,
825 Some(&locals),
826 )
827 .unwrap();
828 model
829 .setattr("commission", commission.into_py_any(py).unwrap())
830 .unwrap();
831
832 model
833 }
834}