Skip to main content

nautilus_execution/python/
fee.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings for fee model types.
17
18use nautilus_core::python::{to_pyruntime_err, to_pytype_err};
19use nautilus_model::types::Money;
20use pyo3::{IntoPyObjectExt, prelude::*};
21use rust_decimal::Decimal;
22
23use crate::models::fee::{
24    CappedOptionFeeModel, FeeModelAny, FixedFeeModel, MakerTakerFeeModel, PerContractFeeModel,
25    ProbabilityPriceFeeModel, TieredNotionalOptionFeeModel,
26};
27
28#[pymethods]
29#[pyo3_stub_gen::derive::gen_stub_pymethods]
30impl FixedFeeModel {
31    /// Creates a new `FixedFeeModel` instance.
32    #[new]
33    #[pyo3(signature = (commission, charge_commission_once=None, change_commission_once=None))]
34    fn py_new(
35        commission: Money,
36        charge_commission_once: Option<bool>,
37        change_commission_once: Option<bool>,
38    ) -> PyResult<Self> {
39        let charge_commission_once = resolve_fixed_fee_charge_commission_once(
40            charge_commission_once,
41            change_commission_once,
42        )?;
43        Self::new(commission, charge_commission_once).map_err(to_pyruntime_err)
44    }
45
46    fn __repr__(&self) -> String {
47        format!("{self:?}")
48    }
49}
50
51fn resolve_fixed_fee_charge_commission_once(
52    charge_commission_once: Option<bool>,
53    change_commission_once: Option<bool>,
54) -> PyResult<Option<bool>> {
55    if charge_commission_once.is_some() && change_commission_once.is_some() {
56        return Err(to_pytype_err(
57            "Provide only one of `charge_commission_once` or `change_commission_once`",
58        ));
59    }
60
61    Ok(charge_commission_once.or(change_commission_once))
62}
63
64#[pymethods]
65#[pyo3_stub_gen::derive::gen_stub_pymethods]
66impl MakerTakerFeeModel {
67    #[new]
68    fn py_new() -> Self {
69        Self
70    }
71
72    fn __repr__(&self) -> String {
73        format!("{self:?}")
74    }
75}
76
77#[pymethods]
78#[pyo3_stub_gen::derive::gen_stub_pymethods]
79impl PerContractFeeModel {
80    /// Creates a new `PerContractFeeModel` instance.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if `commission` is negative.
85    #[new]
86    fn py_new(commission: Money) -> PyResult<Self> {
87        Self::new(commission).map_err(to_pyruntime_err)
88    }
89
90    fn __repr__(&self) -> String {
91        format!("{self:?}")
92    }
93}
94
95#[pymethods]
96#[pyo3_stub_gen::derive::gen_stub_pymethods]
97impl ProbabilityPriceFeeModel {
98    /// Fee model for probability-priced outcome shares.
99    ///
100    /// Applies `qty * fee_rate * p * (1 - p)` using the instrument's maker or
101    /// taker fee rate. This matches venues that represent outcome shares as
102    /// `InstrumentAny.BinaryOption` instruments quoted on a `[0, 1]`
103    /// probability scale.
104    ///
105    /// This model covers quote-currency match-time exchange fees only.
106    /// Venue-specific rebate programs or non-quote fee assets remain outside the
107    /// core execution layer.
108    #[new]
109    fn py_new() -> Self {
110        Self
111    }
112
113    fn __repr__(&self) -> String {
114        format!("{self:?}")
115    }
116}
117
118#[pymethods]
119#[pyo3_stub_gen::derive::gen_stub_pymethods]
120impl CappedOptionFeeModel {
121    /// Creates a new `CappedOptionFeeModel` instance.
122    #[new]
123    #[pyo3(signature = (maker_rate=None, taker_rate=None, cap_rate=None))]
124    fn py_new(
125        maker_rate: Option<Decimal>,
126        taker_rate: Option<Decimal>,
127        cap_rate: Option<Decimal>,
128    ) -> PyResult<Self> {
129        Self::new(maker_rate, taker_rate, cap_rate).map_err(to_pyruntime_err)
130    }
131
132    fn __repr__(&self) -> String {
133        format!("{self:?}")
134    }
135}
136
137#[pymethods]
138#[pyo3_stub_gen::derive::gen_stub_pymethods]
139impl TieredNotionalOptionFeeModel {
140    /// Creates a new `TieredNotionalOptionFeeModel` instance.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if any supplied rate is negative.
145    #[new]
146    #[pyo3(signature = (maker_rate=None, taker_rate=None))]
147    fn py_new(maker_rate: Option<Decimal>, taker_rate: Option<Decimal>) -> PyResult<Self> {
148        Self::new(maker_rate, taker_rate).map_err(to_pyruntime_err)
149    }
150
151    fn __repr__(&self) -> String {
152        format!("{self:?}")
153    }
154}
155
156/// Extracts a Python fee model object into a Rust [`FeeModelAny`].
157///
158/// # Errors
159///
160/// Returns an error if `obj` is not a supported fee model binding.
161pub fn pyobject_to_fee_model_any(obj: &Bound<'_, PyAny>) -> PyResult<FeeModelAny> {
162    if let Ok(m) = obj.extract::<FixedFeeModel>() {
163        return Ok(FeeModelAny::Fixed(m));
164    }
165
166    if let Ok(m) = obj.extract::<MakerTakerFeeModel>() {
167        return Ok(FeeModelAny::MakerTaker(m));
168    }
169
170    if let Ok(m) = obj.extract::<PerContractFeeModel>() {
171        return Ok(FeeModelAny::PerContract(m));
172    }
173
174    if let Ok(m) = obj.extract::<ProbabilityPriceFeeModel>() {
175        return Ok(FeeModelAny::ProbabilityPrice(m));
176    }
177
178    if let Ok(m) = obj.extract::<CappedOptionFeeModel>() {
179        return Ok(FeeModelAny::CappedOption(m));
180    }
181
182    if let Ok(m) = obj.extract::<TieredNotionalOptionFeeModel>() {
183        return Ok(FeeModelAny::TieredNotionalOption(m));
184    }
185
186    let type_name = obj.get_type().name()?;
187    Err(to_pytype_err(format!(
188        "Cannot convert {type_name} to FeeModel"
189    )))
190}
191
192/// Converts a Rust [`FeeModelAny`] into its Python binding object.
193///
194/// # Errors
195///
196/// Returns an error if conversion to a Python object fails.
197pub fn fee_model_any_to_pyobject(py: Python<'_>, model: &FeeModelAny) -> PyResult<Py<PyAny>> {
198    match model {
199        FeeModelAny::Fixed(model) => model.clone().into_py_any(py),
200        FeeModelAny::MakerTaker(model) => model.clone().into_py_any(py),
201        FeeModelAny::PerContract(model) => model.clone().into_py_any(py),
202        FeeModelAny::ProbabilityPrice(model) => model.clone().into_py_any(py),
203        FeeModelAny::CappedOption(model) => model.clone().into_py_any(py),
204        FeeModelAny::TieredNotionalOption(model) => model.clone().into_py_any(py),
205    }
206}