Skip to main content

nautilus_lighter/python/
config.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 Lighter configuration.
17
18use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
19use nautilus_model::identifiers::{AccountId, Venue};
20use nautilus_network::websocket::TransportBackend;
21use pyo3::{PyResult, pymethods};
22
23use crate::{
24    common::enums::{LighterDeployment, LighterEnvironment},
25    config::{
26        LighterDataClientConfig, LighterExecutionClientConfig, validate_scheduled_cancel_timeout,
27    },
28};
29
30#[pymethods]
31#[pyo3_stub_gen::derive::gen_stub_pymethods]
32impl LighterDataClientConfig {
33    /// Configuration for the Lighter live data client.
34    #[new]
35    #[pyo3(signature = (
36        base_url_http = None,
37        base_url_ws = None,
38        proxy_url = None,
39        environment = None,
40        account_index = None,
41        api_key_index = None,
42        private_key = None,
43        http_timeout_secs = None,
44        ws_timeout_secs = None,
45        update_instruments_interval_mins = None,
46        rest_quota_per_min = None,
47        transport_backend = None,
48        deployment = None,
49        venue = None,
50        book_snapshot_timeout_secs = None,
51    ))]
52    #[expect(clippy::too_many_arguments)]
53    fn py_new(
54        base_url_http: Option<String>,
55        base_url_ws: Option<String>,
56        proxy_url: Option<String>,
57        environment: Option<LighterEnvironment>,
58        account_index: Option<u64>,
59        api_key_index: Option<u8>,
60        private_key: Option<String>,
61        http_timeout_secs: Option<u64>,
62        ws_timeout_secs: Option<u64>,
63        update_instruments_interval_mins: Option<u64>,
64        rest_quota_per_min: Option<u32>,
65        transport_backend: Option<TransportBackend>,
66        deployment: Option<LighterDeployment>,
67        venue: Option<Venue>,
68        book_snapshot_timeout_secs: Option<u64>,
69    ) -> Self {
70        let defaults = Self::default();
71        Self {
72            environment: environment.unwrap_or(defaults.environment),
73            deployment: deployment.unwrap_or(defaults.deployment),
74            venue,
75            account_index,
76            api_key_index,
77            private_key: private_key.map(SecretString::from),
78            base_url_http,
79            base_url_ws,
80            proxy_url: proxy_url.map(SecretString::from),
81            http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
82            ws_timeout_secs: ws_timeout_secs.unwrap_or(defaults.ws_timeout_secs),
83            update_instruments_interval_mins: update_instruments_interval_mins
84                .unwrap_or(defaults.update_instruments_interval_mins),
85            book_snapshot_timeout_secs: book_snapshot_timeout_secs
86                .unwrap_or(defaults.book_snapshot_timeout_secs),
87            rest_quota_per_min,
88            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
89        }
90    }
91
92    #[getter]
93    const fn has_proxy_url(&self) -> bool {
94        self.proxy_url.is_some()
95    }
96
97    fn __repr__(&self) -> String {
98        stringify!(LighterDataClientConfig).to_string()
99    }
100}
101
102#[pymethods]
103#[pyo3_stub_gen::derive::gen_stub_pymethods]
104impl LighterExecutionClientConfig {
105    /// Configuration for the Lighter live execution client.
106    #[new]
107    #[pyo3(signature = (
108        account_id,
109        account_index = None,
110        api_key_index = None,
111        private_key = None,
112        base_url_http = None,
113        base_url_ws = None,
114        proxy_url = None,
115        environment = None,
116        http_timeout_secs = None,
117        ws_timeout_secs = None,
118        market_order_slippage_bps = None,
119        rest_quota_per_min = None,
120        sendtx_quota_per_min = None,
121        transport_backend = None,
122        deployment = None,
123        venue = None,
124        use_gtd = true,
125        scheduled_cancel_timeout_secs = None,
126    ))]
127    #[expect(clippy::too_many_arguments)]
128    fn py_new(
129        account_id: AccountId,
130        account_index: Option<u64>,
131        api_key_index: Option<u8>,
132        private_key: Option<String>,
133        base_url_http: Option<String>,
134        base_url_ws: Option<String>,
135        proxy_url: Option<String>,
136        environment: Option<LighterEnvironment>,
137        http_timeout_secs: Option<u64>,
138        ws_timeout_secs: Option<u64>,
139        market_order_slippage_bps: Option<u32>,
140        rest_quota_per_min: Option<u32>,
141        sendtx_quota_per_min: Option<u32>,
142        transport_backend: Option<TransportBackend>,
143        deployment: Option<LighterDeployment>,
144        venue: Option<Venue>,
145        use_gtd: bool,
146        scheduled_cancel_timeout_secs: Option<u64>,
147    ) -> PyResult<Self> {
148        validate_scheduled_cancel_timeout(scheduled_cancel_timeout_secs).map_err(to_pyvalue_err)?;
149        let defaults = Self::default();
150        Ok(Self {
151            environment: environment.unwrap_or(defaults.environment),
152            deployment: deployment.unwrap_or(defaults.deployment),
153            venue,
154            account_id,
155            account_index,
156            api_key_index,
157            private_key: private_key.map(SecretString::from),
158            base_url_http,
159            base_url_ws,
160            proxy_url: proxy_url.map(SecretString::from),
161            http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
162            ws_timeout_secs: ws_timeout_secs.unwrap_or(defaults.ws_timeout_secs),
163            market_order_slippage_bps: market_order_slippage_bps
164                .unwrap_or(defaults.market_order_slippage_bps),
165            rest_quota_per_min,
166            sendtx_quota_per_min,
167            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
168            use_gtd,
169            scheduled_cancel_timeout_secs,
170        })
171    }
172
173    #[getter]
174    const fn has_proxy_url(&self) -> bool {
175        self.proxy_url.is_some()
176    }
177
178    fn __repr__(&self) -> String {
179        stringify!(LighterExecutionClientConfig).to_string()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use pyo3::{
186        PyRef, Python,
187        types::{PyAnyMethods, PyDict, PyDictMethods},
188    };
189    use rstest::rstest;
190
191    use super::*;
192
193    #[rstest]
194    #[case(None, true)]
195    #[case(Some(420), true)]
196    #[case(Some(1_295_940), true)]
197    #[case(Some(419), false)]
198    #[case(Some(1_295_941), false)]
199    fn test_python_scheduled_cancel(#[case] timeout: Option<u64>, #[case] valid: bool) {
200        Python::initialize();
201        Python::attach(|py| {
202            let kwargs = PyDict::new(py);
203            kwargs
204                .set_item("account_id", AccountId::from("LIGHTER-123"))
205                .unwrap();
206
207            if let Some(timeout) = timeout {
208                kwargs
209                    .set_item("scheduled_cancel_timeout_secs", timeout)
210                    .unwrap();
211            }
212
213            let result = py
214                .get_type::<LighterExecutionClientConfig>()
215                .call((), Some(&kwargs));
216            assert_eq!(result.is_ok(), valid);
217
218            if valid {
219                let config = result.unwrap();
220                assert_eq!(
221                    config
222                        .getattr("scheduled_cancel_timeout_secs")
223                        .unwrap()
224                        .extract::<Option<u64>>()
225                        .unwrap(),
226                    timeout
227                );
228                assert_eq!(
229                    config
230                        .extract::<PyRef<LighterExecutionClientConfig>>()
231                        .unwrap()
232                        .scheduled_cancel_timeout_secs,
233                    timeout
234                );
235            } else {
236                assert!(
237                    result
238                        .unwrap_err()
239                        .is_instance_of::<pyo3::exceptions::PyValueError>(py)
240                );
241            }
242        });
243    }
244
245    #[rstest]
246    fn test_data_config_py_new_book_timeout() {
247        let config = LighterDataClientConfig::py_new(
248            None,
249            None,
250            None,
251            None,
252            None,
253            None,
254            None,
255            None,
256            None,
257            None,
258            Some(7),
259            None,
260            None,
261            None,
262            Some(42),
263        );
264
265        assert_eq!(config.rest_quota_per_min, Some(7));
266        assert_eq!(config.book_snapshot_timeout_secs, 42);
267    }
268
269    #[rstest]
270    fn test_data_config_py_new_book_timeout_default() {
271        let config = LighterDataClientConfig::py_new(
272            None, None, None, None, None, None, None, None, None, None, None, None, None, None,
273            None,
274        );
275
276        assert_eq!(config.book_snapshot_timeout_secs, 10);
277    }
278}