nautilus_common/python/
cache.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 the [`Cache`] component.
17
18use nautilus_core::python::to_pyvalue_err;
19use nautilus_model::{
20    data::{
21        Bar, BarType, FundingRateUpdate, QuoteTick, TradeTick,
22        prices::{IndexPriceUpdate, MarkPriceUpdate},
23    },
24    enums::{OmsType, OrderSide, PositionSide},
25    identifiers::{ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, Venue},
26    instruments::SyntheticInstrument,
27    orderbook::OrderBook,
28    position::Position,
29    python::{
30        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
31        orders::{order_any_to_pyobject, pyobject_to_order_any},
32    },
33    types::Currency,
34};
35use pyo3::prelude::*;
36
37use crate::{
38    cache::{Cache, CacheConfig},
39    enums::SerializationEncoding,
40};
41
42#[pymethods]
43impl CacheConfig {
44    #[new]
45    #[allow(clippy::too_many_arguments)]
46    fn py_new(
47        encoding: Option<SerializationEncoding>,
48        timestamps_as_iso8601: Option<bool>,
49        buffer_interval_ms: Option<usize>,
50        use_trader_prefix: Option<bool>,
51        use_instance_id: Option<bool>,
52        flush_on_start: Option<bool>,
53        drop_instruments_on_reset: Option<bool>,
54        tick_capacity: Option<usize>,
55        bar_capacity: Option<usize>,
56        save_market_data: Option<bool>,
57    ) -> Self {
58        Self::new(
59            None, // database is None since we can't expose it to Python yet
60            encoding.unwrap_or(SerializationEncoding::MsgPack),
61            timestamps_as_iso8601.unwrap_or(false),
62            buffer_interval_ms,
63            use_trader_prefix.unwrap_or(true),
64            use_instance_id.unwrap_or(false),
65            flush_on_start.unwrap_or(false),
66            drop_instruments_on_reset.unwrap_or(true),
67            tick_capacity.unwrap_or(10_000),
68            bar_capacity.unwrap_or(10_000),
69            save_market_data.unwrap_or(false),
70        )
71    }
72
73    fn __str__(&self) -> String {
74        format!("{self:?}")
75    }
76
77    fn __repr__(&self) -> String {
78        format!("{self:?}")
79    }
80
81    #[getter]
82    fn encoding(&self) -> SerializationEncoding {
83        self.encoding
84    }
85
86    #[getter]
87    fn timestamps_as_iso8601(&self) -> bool {
88        self.timestamps_as_iso8601
89    }
90
91    #[getter]
92    fn buffer_interval_ms(&self) -> Option<usize> {
93        self.buffer_interval_ms
94    }
95
96    #[getter]
97    fn use_trader_prefix(&self) -> bool {
98        self.use_trader_prefix
99    }
100
101    #[getter]
102    fn use_instance_id(&self) -> bool {
103        self.use_instance_id
104    }
105
106    #[getter]
107    fn flush_on_start(&self) -> bool {
108        self.flush_on_start
109    }
110
111    #[getter]
112    fn drop_instruments_on_reset(&self) -> bool {
113        self.drop_instruments_on_reset
114    }
115
116    #[getter]
117    fn tick_capacity(&self) -> usize {
118        self.tick_capacity
119    }
120
121    #[getter]
122    fn bar_capacity(&self) -> usize {
123        self.bar_capacity
124    }
125
126    #[getter]
127    fn save_market_data(&self) -> bool {
128        self.save_market_data
129    }
130}
131
132#[pymethods]
133impl Cache {
134    #[new]
135    fn py_new(config: Option<CacheConfig>) -> Self {
136        Self::new(config, None)
137    }
138
139    fn __repr__(&self) -> String {
140        format!("{self:?}")
141    }
142
143    #[pyo3(name = "reset")]
144    fn py_reset(&mut self) {
145        self.reset();
146    }
147
148    #[pyo3(name = "dispose")]
149    fn py_dispose(&mut self) {
150        self.dispose();
151    }
152
153    #[pyo3(name = "add_currency")]
154    fn py_add_currency(&mut self, currency: Currency) -> PyResult<()> {
155        self.add_currency(currency).map_err(to_pyvalue_err)
156    }
157
158    #[pyo3(name = "add_instrument")]
159    fn py_add_instrument(&mut self, py: Python, instrument: PyObject) -> PyResult<()> {
160        let instrument_any = pyobject_to_instrument_any(py, instrument)?;
161        self.add_instrument(instrument_any).map_err(to_pyvalue_err)
162    }
163
164    #[pyo3(name = "instrument")]
165    fn py_instrument(&self, py: Python, instrument_id: InstrumentId) -> PyResult<Option<PyObject>> {
166        match self.instrument(&instrument_id) {
167            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument.clone())?)),
168            None => Ok(None),
169        }
170    }
171
172    #[pyo3(name = "instrument_ids")]
173    fn py_instrument_ids(&self, venue: Option<Venue>) -> Vec<InstrumentId> {
174        self.instrument_ids(venue.as_ref())
175            .into_iter()
176            .cloned()
177            .collect()
178    }
179
180    #[pyo3(name = "instruments")]
181    fn py_instruments(&self, py: Python, venue: Option<Venue>) -> PyResult<Vec<PyObject>> {
182        let mut py_instruments = Vec::new();
183
184        match venue {
185            Some(venue) => {
186                let instruments = self.instruments(&venue, None);
187                for instrument in instruments {
188                    py_instruments.push(instrument_any_to_pyobject(py, (*instrument).clone())?);
189                }
190            }
191            None => {
192                // Get all instruments by iterating through instrument_ids and getting each instrument
193                let instrument_ids = self.instrument_ids(None);
194                for instrument_id in instrument_ids {
195                    if let Some(instrument) = self.instrument(instrument_id) {
196                        py_instruments.push(instrument_any_to_pyobject(py, instrument.clone())?);
197                    }
198                }
199            }
200        }
201
202        Ok(py_instruments)
203    }
204
205    #[pyo3(name = "add_order")]
206    fn py_add_order(
207        &mut self,
208        py: Python,
209        order: PyObject,
210        position_id: Option<PositionId>,
211        client_id: Option<ClientId>,
212        replace_existing: Option<bool>,
213    ) -> PyResult<()> {
214        let order_any = pyobject_to_order_any(py, order)?;
215        self.add_order(
216            order_any,
217            position_id,
218            client_id,
219            replace_existing.unwrap_or(false),
220        )
221        .map_err(to_pyvalue_err)
222    }
223
224    #[pyo3(name = "order")]
225    fn py_order(&self, py: Python, client_order_id: ClientOrderId) -> PyResult<Option<PyObject>> {
226        match self.order(&client_order_id) {
227            Some(order) => Ok(Some(order_any_to_pyobject(py, order.clone())?)),
228            None => Ok(None),
229        }
230    }
231
232    #[pyo3(name = "order_exists")]
233    fn py_order_exists(&self, client_order_id: ClientOrderId) -> bool {
234        self.order_exists(&client_order_id)
235    }
236
237    #[pyo3(name = "is_order_open")]
238    fn py_is_order_open(&self, client_order_id: ClientOrderId) -> bool {
239        self.is_order_open(&client_order_id)
240    }
241
242    #[pyo3(name = "is_order_closed")]
243    fn py_is_order_closed(&self, client_order_id: ClientOrderId) -> bool {
244        self.is_order_closed(&client_order_id)
245    }
246
247    #[pyo3(name = "orders_open_count")]
248    fn py_orders_open_count(
249        &self,
250        venue: Option<Venue>,
251        instrument_id: Option<InstrumentId>,
252        strategy_id: Option<StrategyId>,
253        side: Option<OrderSide>,
254    ) -> usize {
255        self.orders_open_count(
256            venue.as_ref(),
257            instrument_id.as_ref(),
258            strategy_id.as_ref(),
259            side,
260        )
261    }
262
263    #[pyo3(name = "orders_closed_count")]
264    fn py_orders_closed_count(
265        &self,
266        venue: Option<Venue>,
267        instrument_id: Option<InstrumentId>,
268        strategy_id: Option<StrategyId>,
269        side: Option<OrderSide>,
270    ) -> usize {
271        self.orders_closed_count(
272            venue.as_ref(),
273            instrument_id.as_ref(),
274            strategy_id.as_ref(),
275            side,
276        )
277    }
278
279    #[pyo3(name = "orders_total_count")]
280    fn py_orders_total_count(
281        &self,
282        venue: Option<Venue>,
283        instrument_id: Option<InstrumentId>,
284        strategy_id: Option<StrategyId>,
285        side: Option<OrderSide>,
286    ) -> usize {
287        self.orders_total_count(
288            venue.as_ref(),
289            instrument_id.as_ref(),
290            strategy_id.as_ref(),
291            side,
292        )
293    }
294
295    #[pyo3(name = "add_position")]
296    fn py_add_position(
297        &mut self,
298        py: Python,
299        position: PyObject,
300        oms_type: OmsType,
301    ) -> PyResult<()> {
302        let position_obj = position.extract::<Position>(py)?;
303        self.add_position(position_obj, oms_type)
304            .map_err(to_pyvalue_err)
305    }
306
307    #[pyo3(name = "position")]
308    fn py_position(&self, py: Python, position_id: PositionId) -> PyResult<Option<PyObject>> {
309        match self.position(&position_id) {
310            Some(position) => Ok(Some(position.clone().into_pyobject(py)?.into())),
311            None => Ok(None),
312        }
313    }
314
315    #[pyo3(name = "position_exists")]
316    fn py_position_exists(&self, position_id: PositionId) -> bool {
317        self.position_exists(&position_id)
318    }
319
320    #[pyo3(name = "is_position_open")]
321    fn py_is_position_open(&self, position_id: PositionId) -> bool {
322        self.is_position_open(&position_id)
323    }
324
325    #[pyo3(name = "is_position_closed")]
326    fn py_is_position_closed(&self, position_id: PositionId) -> bool {
327        self.is_position_closed(&position_id)
328    }
329
330    #[pyo3(name = "positions_open_count")]
331    fn py_positions_open_count(
332        &self,
333        venue: Option<Venue>,
334        instrument_id: Option<InstrumentId>,
335        strategy_id: Option<StrategyId>,
336        side: Option<PositionSide>,
337    ) -> usize {
338        self.positions_open_count(
339            venue.as_ref(),
340            instrument_id.as_ref(),
341            strategy_id.as_ref(),
342            side,
343        )
344    }
345
346    #[pyo3(name = "positions_closed_count")]
347    fn py_positions_closed_count(
348        &self,
349        venue: Option<Venue>,
350        instrument_id: Option<InstrumentId>,
351        strategy_id: Option<StrategyId>,
352        side: Option<PositionSide>,
353    ) -> usize {
354        self.positions_closed_count(
355            venue.as_ref(),
356            instrument_id.as_ref(),
357            strategy_id.as_ref(),
358            side,
359        )
360    }
361
362    #[pyo3(name = "positions_total_count")]
363    fn py_positions_total_count(
364        &self,
365        venue: Option<Venue>,
366        instrument_id: Option<InstrumentId>,
367        strategy_id: Option<StrategyId>,
368        side: Option<PositionSide>,
369    ) -> usize {
370        self.positions_total_count(
371            venue.as_ref(),
372            instrument_id.as_ref(),
373            strategy_id.as_ref(),
374            side,
375        )
376    }
377
378    #[pyo3(name = "add_quote")]
379    fn py_add_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
380        self.add_quote(quote).map_err(to_pyvalue_err)
381    }
382
383    #[pyo3(name = "add_trade")]
384    fn py_add_trade(&mut self, trade: TradeTick) -> PyResult<()> {
385        self.add_trade(trade).map_err(to_pyvalue_err)
386    }
387
388    #[pyo3(name = "add_bar")]
389    fn py_add_bar(&mut self, bar: Bar) -> PyResult<()> {
390        self.add_bar(bar).map_err(to_pyvalue_err)
391    }
392
393    #[pyo3(name = "quote")]
394    fn py_quote(&self, instrument_id: InstrumentId) -> Option<QuoteTick> {
395        self.quote(&instrument_id).cloned()
396    }
397
398    #[pyo3(name = "trade")]
399    fn py_trade(&self, instrument_id: InstrumentId) -> Option<TradeTick> {
400        self.trade(&instrument_id).cloned()
401    }
402
403    #[pyo3(name = "bar")]
404    fn py_bar(&self, bar_type: BarType) -> Option<Bar> {
405        self.bar(&bar_type).cloned()
406    }
407
408    #[pyo3(name = "quotes")]
409    fn py_quotes(&self, instrument_id: InstrumentId) -> Option<Vec<QuoteTick>> {
410        self.quotes(&instrument_id).map(|deque| deque.to_vec())
411    }
412
413    #[pyo3(name = "trades")]
414    fn py_trades(&self, instrument_id: InstrumentId) -> Option<Vec<TradeTick>> {
415        self.trades(&instrument_id).map(|deque| deque.to_vec())
416    }
417
418    #[pyo3(name = "bars")]
419    fn py_bars(&self, bar_type: BarType) -> Option<Vec<Bar>> {
420        self.bars(&bar_type).map(|deque| deque.to_vec())
421    }
422
423    #[pyo3(name = "has_quote_ticks")]
424    fn py_has_quote_ticks(&self, instrument_id: InstrumentId) -> bool {
425        self.has_quote_ticks(&instrument_id)
426    }
427
428    #[pyo3(name = "has_trade_ticks")]
429    fn py_has_trade_ticks(&self, instrument_id: InstrumentId) -> bool {
430        self.has_trade_ticks(&instrument_id)
431    }
432
433    #[pyo3(name = "has_bars")]
434    fn py_has_bars(&self, bar_type: BarType) -> bool {
435        self.has_bars(&bar_type)
436    }
437
438    #[pyo3(name = "quote_count")]
439    fn py_quote_count(&self, instrument_id: InstrumentId) -> usize {
440        self.quote_count(&instrument_id)
441    }
442
443    #[pyo3(name = "trade_count")]
444    fn py_trade_count(&self, instrument_id: InstrumentId) -> usize {
445        self.trade_count(&instrument_id)
446    }
447
448    #[pyo3(name = "bar_count")]
449    fn py_bar_count(&self, bar_type: BarType) -> usize {
450        self.bar_count(&bar_type)
451    }
452
453    #[pyo3(name = "mark_price")]
454    fn py_mark_price(&self, instrument_id: InstrumentId) -> Option<MarkPriceUpdate> {
455        self.mark_price(&instrument_id).cloned()
456    }
457
458    #[pyo3(name = "mark_prices")]
459    fn py_mark_prices(&self, instrument_id: InstrumentId) -> Option<Vec<MarkPriceUpdate>> {
460        self.mark_prices(&instrument_id)
461    }
462
463    #[pyo3(name = "index_price")]
464    fn py_index_price(&self, instrument_id: InstrumentId) -> Option<IndexPriceUpdate> {
465        self.index_price(&instrument_id).cloned()
466    }
467
468    #[pyo3(name = "index_prices")]
469    fn py_index_prices(&self, instrument_id: InstrumentId) -> Option<Vec<IndexPriceUpdate>> {
470        self.index_prices(&instrument_id)
471    }
472
473    #[pyo3(name = "funding_rate")]
474    fn py_funding_rate(&self, instrument_id: InstrumentId) -> Option<FundingRateUpdate> {
475        self.funding_rate(&instrument_id).cloned()
476    }
477
478    #[pyo3(name = "order_book")]
479    fn py_order_book(&self, instrument_id: InstrumentId) -> Option<OrderBook> {
480        self.order_book(&instrument_id).cloned()
481    }
482
483    #[pyo3(name = "has_order_book")]
484    fn py_has_order_book(&self, instrument_id: InstrumentId) -> bool {
485        self.has_order_book(&instrument_id)
486    }
487
488    #[pyo3(name = "book_update_count")]
489    fn py_book_update_count(&self, instrument_id: InstrumentId) -> usize {
490        self.book_update_count(&instrument_id)
491    }
492
493    #[pyo3(name = "synthetic")]
494    fn py_synthetic(&self, instrument_id: InstrumentId) -> Option<SyntheticInstrument> {
495        self.synthetic(&instrument_id).cloned()
496    }
497
498    #[pyo3(name = "synthetic_ids")]
499    fn py_synthetic_ids(&self) -> Vec<InstrumentId> {
500        self.synthetic_ids().into_iter().copied().collect()
501    }
502}