Skip to main content

nautilus_common/python/
cache.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 the [`Cache`] component.
17
18use std::{cell::RefCell, rc::Rc};
19
20use nautilus_core::python::to_pyvalue_err;
21#[cfg(feature = "defi")]
22use nautilus_model::defi::{Pool, PoolProfiler};
23use nautilus_model::{
24    data::{
25        Bar, BarType, FundingRateUpdate, QuoteTick, TradeTick,
26        prices::{IndexPriceUpdate, MarkPriceUpdate},
27    },
28    enums::{OmsType, OrderSide, PositionSide},
29    identifiers::{
30        AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, Venue,
31    },
32    instruments::SyntheticInstrument,
33    orderbook::OrderBook,
34    position::Position,
35    python::{
36        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
37        orders::{order_any_to_pyobject, pyobject_to_order_any},
38    },
39    types::Currency,
40};
41use pyo3::prelude::*;
42
43use crate::{
44    cache::{Cache, CacheConfig},
45    enums::SerializationEncoding,
46};
47
48/// Wrapper providing shared access to [`Cache`] from Python.
49///
50/// This wrapper holds an `Rc<RefCell<Cache>>` allowing actors to share
51/// the same cache instance. All methods delegate to the underlying cache.
52#[allow(non_camel_case_types)]
53#[pyo3::pyclass(
54    module = "nautilus_trader.core.nautilus_pyo3.common",
55    unsendable,
56    from_py_object
57)]
58#[derive(Debug, Clone)]
59pub struct PyCache(Rc<RefCell<Cache>>);
60
61impl PyCache {
62    /// Creates a `PyCache` from an `Rc<RefCell<Cache>>`.
63    #[must_use]
64    pub fn from_rc(rc: Rc<RefCell<Cache>>) -> Self {
65        Self(rc)
66    }
67}
68
69#[pymethods]
70impl PyCache {
71    #[pyo3(name = "instrument")]
72    fn py_instrument(
73        &self,
74        py: Python,
75        instrument_id: InstrumentId,
76    ) -> PyResult<Option<Py<PyAny>>> {
77        let cache = self.0.borrow();
78        match cache.instrument(&instrument_id) {
79            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument.clone())?)),
80            None => Ok(None),
81        }
82    }
83
84    #[pyo3(name = "quote")]
85    fn py_quote(&self, instrument_id: InstrumentId) -> Option<QuoteTick> {
86        self.0.borrow().quote(&instrument_id).copied()
87    }
88
89    #[pyo3(name = "trade")]
90    fn py_trade(&self, instrument_id: InstrumentId) -> Option<TradeTick> {
91        self.0.borrow().trade(&instrument_id).copied()
92    }
93
94    #[pyo3(name = "bar")]
95    fn py_bar(&self, bar_type: BarType) -> Option<Bar> {
96        self.0.borrow().bar(&bar_type).copied()
97    }
98
99    #[pyo3(name = "order_book")]
100    fn py_order_book(&self, instrument_id: InstrumentId) -> Option<OrderBook> {
101        self.0.borrow().order_book(&instrument_id).cloned()
102    }
103
104    #[cfg(feature = "defi")]
105    #[pyo3(name = "pool")]
106    fn py_pool(&self, instrument_id: InstrumentId) -> Option<Pool> {
107        self.0
108            .try_borrow()
109            .ok()
110            .and_then(|cache| cache.pool(&instrument_id).cloned())
111    }
112
113    #[cfg(feature = "defi")]
114    #[pyo3(name = "pool_profiler")]
115    fn py_pool_profiler(&self, instrument_id: InstrumentId) -> Option<PoolProfiler> {
116        self.0
117            .try_borrow()
118            .ok()
119            .and_then(|cache| cache.pool_profiler(&instrument_id).cloned())
120    }
121}
122
123#[pymethods]
124impl CacheConfig {
125    #[new]
126    #[allow(clippy::too_many_arguments)]
127    fn py_new(
128        encoding: Option<SerializationEncoding>,
129        timestamps_as_iso8601: Option<bool>,
130        buffer_interval_ms: Option<usize>,
131        bulk_read_batch_size: Option<usize>,
132        use_trader_prefix: Option<bool>,
133        use_instance_id: Option<bool>,
134        flush_on_start: Option<bool>,
135        drop_instruments_on_reset: Option<bool>,
136        tick_capacity: Option<usize>,
137        bar_capacity: Option<usize>,
138        save_market_data: Option<bool>,
139    ) -> Self {
140        Self::new(
141            None, // database is None since we can't expose it to Python yet
142            encoding.unwrap_or(SerializationEncoding::MsgPack),
143            timestamps_as_iso8601.unwrap_or(false),
144            buffer_interval_ms,
145            bulk_read_batch_size,
146            use_trader_prefix.unwrap_or(true),
147            use_instance_id.unwrap_or(false),
148            flush_on_start.unwrap_or(false),
149            drop_instruments_on_reset.unwrap_or(true),
150            tick_capacity.unwrap_or(10_000),
151            bar_capacity.unwrap_or(10_000),
152            save_market_data.unwrap_or(false),
153        )
154    }
155
156    fn __str__(&self) -> String {
157        format!("{self:?}")
158    }
159
160    fn __repr__(&self) -> String {
161        format!("{self:?}")
162    }
163
164    #[getter]
165    fn encoding(&self) -> SerializationEncoding {
166        self.encoding
167    }
168
169    #[getter]
170    fn timestamps_as_iso8601(&self) -> bool {
171        self.timestamps_as_iso8601
172    }
173
174    #[getter]
175    fn buffer_interval_ms(&self) -> Option<usize> {
176        self.buffer_interval_ms
177    }
178
179    #[getter]
180    fn bulk_read_batch_size(&self) -> Option<usize> {
181        self.bulk_read_batch_size
182    }
183
184    #[getter]
185    fn use_trader_prefix(&self) -> bool {
186        self.use_trader_prefix
187    }
188
189    #[getter]
190    fn use_instance_id(&self) -> bool {
191        self.use_instance_id
192    }
193
194    #[getter]
195    fn flush_on_start(&self) -> bool {
196        self.flush_on_start
197    }
198
199    #[getter]
200    fn drop_instruments_on_reset(&self) -> bool {
201        self.drop_instruments_on_reset
202    }
203
204    #[getter]
205    fn tick_capacity(&self) -> usize {
206        self.tick_capacity
207    }
208
209    #[getter]
210    fn bar_capacity(&self) -> usize {
211        self.bar_capacity
212    }
213
214    #[getter]
215    fn save_market_data(&self) -> bool {
216        self.save_market_data
217    }
218}
219
220#[pymethods]
221impl Cache {
222    #[new]
223    fn py_new(config: Option<CacheConfig>) -> Self {
224        Self::new(config, None)
225    }
226
227    fn __repr__(&self) -> String {
228        format!("{self:?}")
229    }
230
231    #[pyo3(name = "reset")]
232    fn py_reset(&mut self) {
233        self.reset();
234    }
235
236    #[pyo3(name = "dispose")]
237    fn py_dispose(&mut self) {
238        self.dispose();
239    }
240
241    #[pyo3(name = "add_currency")]
242    fn py_add_currency(&mut self, currency: Currency) -> PyResult<()> {
243        self.add_currency(currency).map_err(to_pyvalue_err)
244    }
245
246    #[pyo3(name = "add_instrument")]
247    fn py_add_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
248        let instrument_any = pyobject_to_instrument_any(py, instrument)?;
249        self.add_instrument(instrument_any).map_err(to_pyvalue_err)
250    }
251
252    #[pyo3(name = "instrument")]
253    fn py_instrument(
254        &self,
255        py: Python,
256        instrument_id: InstrumentId,
257    ) -> PyResult<Option<Py<PyAny>>> {
258        match self.instrument(&instrument_id) {
259            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument.clone())?)),
260            None => Ok(None),
261        }
262    }
263
264    #[pyo3(name = "instrument_ids")]
265    fn py_instrument_ids(&self, venue: Option<Venue>) -> Vec<InstrumentId> {
266        self.instrument_ids(venue.as_ref())
267            .into_iter()
268            .copied()
269            .collect()
270    }
271
272    #[pyo3(name = "instruments")]
273    fn py_instruments(&self, py: Python, venue: Option<Venue>) -> PyResult<Vec<Py<PyAny>>> {
274        let mut py_instruments = Vec::new();
275
276        match venue {
277            Some(venue) => {
278                let instruments = self.instruments(&venue, None);
279                for instrument in instruments {
280                    py_instruments.push(instrument_any_to_pyobject(py, (*instrument).clone())?);
281                }
282            }
283            None => {
284                // Get all instruments by iterating through instrument_ids and getting each instrument
285                let instrument_ids = self.instrument_ids(None);
286                for instrument_id in instrument_ids {
287                    if let Some(instrument) = self.instrument(instrument_id) {
288                        py_instruments.push(instrument_any_to_pyobject(py, instrument.clone())?);
289                    }
290                }
291            }
292        }
293
294        Ok(py_instruments)
295    }
296
297    #[pyo3(name = "add_order")]
298    fn py_add_order(
299        &mut self,
300        py: Python,
301        order: Py<PyAny>,
302        position_id: Option<PositionId>,
303        client_id: Option<ClientId>,
304        replace_existing: Option<bool>,
305    ) -> PyResult<()> {
306        let order_any = pyobject_to_order_any(py, order)?;
307        self.add_order(
308            order_any,
309            position_id,
310            client_id,
311            replace_existing.unwrap_or(false),
312        )
313        .map_err(to_pyvalue_err)
314    }
315
316    #[pyo3(name = "order")]
317    fn py_order(&self, py: Python, client_order_id: ClientOrderId) -> PyResult<Option<Py<PyAny>>> {
318        match self.order(&client_order_id) {
319            Some(order) => Ok(Some(order_any_to_pyobject(py, order.clone())?)),
320            None => Ok(None),
321        }
322    }
323
324    #[pyo3(name = "order_exists")]
325    fn py_order_exists(&self, client_order_id: ClientOrderId) -> bool {
326        self.order_exists(&client_order_id)
327    }
328
329    #[pyo3(name = "is_order_open")]
330    fn py_is_order_open(&self, client_order_id: ClientOrderId) -> bool {
331        self.is_order_open(&client_order_id)
332    }
333
334    #[pyo3(name = "is_order_closed")]
335    fn py_is_order_closed(&self, client_order_id: ClientOrderId) -> bool {
336        self.is_order_closed(&client_order_id)
337    }
338
339    #[pyo3(name = "orders_open_count")]
340    fn py_orders_open_count(
341        &self,
342        venue: Option<Venue>,
343        instrument_id: Option<InstrumentId>,
344        strategy_id: Option<StrategyId>,
345        account_id: Option<AccountId>,
346        side: Option<OrderSide>,
347    ) -> usize {
348        self.orders_open_count(
349            venue.as_ref(),
350            instrument_id.as_ref(),
351            strategy_id.as_ref(),
352            account_id.as_ref(),
353            side,
354        )
355    }
356
357    #[pyo3(name = "orders_closed_count")]
358    fn py_orders_closed_count(
359        &self,
360        venue: Option<Venue>,
361        instrument_id: Option<InstrumentId>,
362        strategy_id: Option<StrategyId>,
363        account_id: Option<AccountId>,
364        side: Option<OrderSide>,
365    ) -> usize {
366        self.orders_closed_count(
367            venue.as_ref(),
368            instrument_id.as_ref(),
369            strategy_id.as_ref(),
370            account_id.as_ref(),
371            side,
372        )
373    }
374
375    #[pyo3(name = "orders_total_count")]
376    fn py_orders_total_count(
377        &self,
378        venue: Option<Venue>,
379        instrument_id: Option<InstrumentId>,
380        strategy_id: Option<StrategyId>,
381        account_id: Option<AccountId>,
382        side: Option<OrderSide>,
383    ) -> usize {
384        self.orders_total_count(
385            venue.as_ref(),
386            instrument_id.as_ref(),
387            strategy_id.as_ref(),
388            account_id.as_ref(),
389            side,
390        )
391    }
392
393    #[pyo3(name = "add_position")]
394    fn py_add_position(
395        &mut self,
396        py: Python,
397        position: Py<PyAny>,
398        oms_type: OmsType,
399    ) -> PyResult<()> {
400        let position_obj = position.extract::<Position>(py)?;
401        self.add_position(position_obj, oms_type)
402            .map_err(to_pyvalue_err)
403    }
404
405    #[pyo3(name = "position")]
406    fn py_position(&self, py: Python, position_id: PositionId) -> PyResult<Option<Py<PyAny>>> {
407        match self.position(&position_id) {
408            Some(position) => Ok(Some(position.clone().into_pyobject(py)?.into())),
409            None => Ok(None),
410        }
411    }
412
413    #[pyo3(name = "position_exists")]
414    fn py_position_exists(&self, position_id: PositionId) -> bool {
415        self.position_exists(&position_id)
416    }
417
418    #[pyo3(name = "is_position_open")]
419    fn py_is_position_open(&self, position_id: PositionId) -> bool {
420        self.is_position_open(&position_id)
421    }
422
423    #[pyo3(name = "is_position_closed")]
424    fn py_is_position_closed(&self, position_id: PositionId) -> bool {
425        self.is_position_closed(&position_id)
426    }
427
428    #[pyo3(name = "positions_open_count")]
429    fn py_positions_open_count(
430        &self,
431        venue: Option<Venue>,
432        instrument_id: Option<InstrumentId>,
433        strategy_id: Option<StrategyId>,
434        account_id: Option<AccountId>,
435        side: Option<PositionSide>,
436    ) -> usize {
437        self.positions_open_count(
438            venue.as_ref(),
439            instrument_id.as_ref(),
440            strategy_id.as_ref(),
441            account_id.as_ref(),
442            side,
443        )
444    }
445
446    #[pyo3(name = "positions_closed_count")]
447    fn py_positions_closed_count(
448        &self,
449        venue: Option<Venue>,
450        instrument_id: Option<InstrumentId>,
451        strategy_id: Option<StrategyId>,
452        account_id: Option<AccountId>,
453        side: Option<PositionSide>,
454    ) -> usize {
455        self.positions_closed_count(
456            venue.as_ref(),
457            instrument_id.as_ref(),
458            strategy_id.as_ref(),
459            account_id.as_ref(),
460            side,
461        )
462    }
463
464    #[pyo3(name = "positions_total_count")]
465    fn py_positions_total_count(
466        &self,
467        venue: Option<Venue>,
468        instrument_id: Option<InstrumentId>,
469        strategy_id: Option<StrategyId>,
470        account_id: Option<AccountId>,
471        side: Option<PositionSide>,
472    ) -> usize {
473        self.positions_total_count(
474            venue.as_ref(),
475            instrument_id.as_ref(),
476            strategy_id.as_ref(),
477            account_id.as_ref(),
478            side,
479        )
480    }
481
482    #[pyo3(name = "add_quote")]
483    fn py_add_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
484        self.add_quote(quote).map_err(to_pyvalue_err)
485    }
486
487    #[pyo3(name = "add_trade")]
488    fn py_add_trade(&mut self, trade: TradeTick) -> PyResult<()> {
489        self.add_trade(trade).map_err(to_pyvalue_err)
490    }
491
492    #[pyo3(name = "add_bar")]
493    fn py_add_bar(&mut self, bar: Bar) -> PyResult<()> {
494        self.add_bar(bar).map_err(to_pyvalue_err)
495    }
496
497    #[pyo3(name = "quote")]
498    fn py_quote(&self, instrument_id: InstrumentId) -> Option<QuoteTick> {
499        self.quote(&instrument_id).copied()
500    }
501
502    #[pyo3(name = "trade")]
503    fn py_trade(&self, instrument_id: InstrumentId) -> Option<TradeTick> {
504        self.trade(&instrument_id).copied()
505    }
506
507    #[pyo3(name = "bar")]
508    fn py_bar(&self, bar_type: BarType) -> Option<Bar> {
509        self.bar(&bar_type).copied()
510    }
511
512    #[pyo3(name = "quotes")]
513    fn py_quotes(&self, instrument_id: InstrumentId) -> Option<Vec<QuoteTick>> {
514        self.quotes(&instrument_id)
515    }
516
517    #[pyo3(name = "trades")]
518    fn py_trades(&self, instrument_id: InstrumentId) -> Option<Vec<TradeTick>> {
519        self.trades(&instrument_id)
520    }
521
522    #[pyo3(name = "bars")]
523    fn py_bars(&self, bar_type: BarType) -> Option<Vec<Bar>> {
524        self.bars(&bar_type)
525    }
526
527    #[pyo3(name = "has_quote_ticks")]
528    fn py_has_quote_ticks(&self, instrument_id: InstrumentId) -> bool {
529        self.has_quote_ticks(&instrument_id)
530    }
531
532    #[pyo3(name = "has_trade_ticks")]
533    fn py_has_trade_ticks(&self, instrument_id: InstrumentId) -> bool {
534        self.has_trade_ticks(&instrument_id)
535    }
536
537    #[pyo3(name = "has_bars")]
538    fn py_has_bars(&self, bar_type: BarType) -> bool {
539        self.has_bars(&bar_type)
540    }
541
542    #[pyo3(name = "quote_count")]
543    fn py_quote_count(&self, instrument_id: InstrumentId) -> usize {
544        self.quote_count(&instrument_id)
545    }
546
547    #[pyo3(name = "trade_count")]
548    fn py_trade_count(&self, instrument_id: InstrumentId) -> usize {
549        self.trade_count(&instrument_id)
550    }
551
552    #[pyo3(name = "bar_count")]
553    fn py_bar_count(&self, bar_type: BarType) -> usize {
554        self.bar_count(&bar_type)
555    }
556
557    #[pyo3(name = "mark_price")]
558    fn py_mark_price(&self, instrument_id: InstrumentId) -> Option<MarkPriceUpdate> {
559        self.mark_price(&instrument_id).copied()
560    }
561
562    #[pyo3(name = "mark_prices")]
563    fn py_mark_prices(&self, instrument_id: InstrumentId) -> Option<Vec<MarkPriceUpdate>> {
564        self.mark_prices(&instrument_id)
565    }
566
567    #[pyo3(name = "index_price")]
568    fn py_index_price(&self, instrument_id: InstrumentId) -> Option<IndexPriceUpdate> {
569        self.index_price(&instrument_id).copied()
570    }
571
572    #[pyo3(name = "index_prices")]
573    fn py_index_prices(&self, instrument_id: InstrumentId) -> Option<Vec<IndexPriceUpdate>> {
574        self.index_prices(&instrument_id)
575    }
576
577    #[pyo3(name = "funding_rate")]
578    fn py_funding_rate(&self, instrument_id: InstrumentId) -> Option<FundingRateUpdate> {
579        self.funding_rate(&instrument_id).copied()
580    }
581
582    #[pyo3(name = "order_book")]
583    fn py_order_book(&self, instrument_id: InstrumentId) -> Option<OrderBook> {
584        self.order_book(&instrument_id).cloned()
585    }
586
587    #[pyo3(name = "has_order_book")]
588    fn py_has_order_book(&self, instrument_id: InstrumentId) -> bool {
589        self.has_order_book(&instrument_id)
590    }
591
592    #[pyo3(name = "book_update_count")]
593    fn py_book_update_count(&self, instrument_id: InstrumentId) -> usize {
594        self.book_update_count(&instrument_id)
595    }
596
597    #[pyo3(name = "synthetic")]
598    fn py_synthetic(&self, instrument_id: InstrumentId) -> Option<SyntheticInstrument> {
599        self.synthetic(&instrument_id).cloned()
600    }
601
602    #[pyo3(name = "synthetic_ids")]
603    fn py_synthetic_ids(&self) -> Vec<InstrumentId> {
604        self.synthetic_ids().into_iter().copied().collect()
605    }
606
607    #[cfg(feature = "defi")]
608    #[pyo3(name = "add_pool")]
609    fn py_add_pool(&mut self, pool: Pool) -> PyResult<()> {
610        self.add_pool(pool).map_err(to_pyvalue_err)
611    }
612
613    #[cfg(feature = "defi")]
614    #[pyo3(name = "pool")]
615    fn py_pool(&self, instrument_id: InstrumentId) -> Option<Pool> {
616        self.pool(&instrument_id).cloned()
617    }
618
619    #[cfg(feature = "defi")]
620    #[pyo3(name = "pool_ids")]
621    fn py_pool_ids(&self, venue: Option<Venue>) -> Vec<InstrumentId> {
622        self.pool_ids(venue.as_ref())
623    }
624
625    #[cfg(feature = "defi")]
626    #[pyo3(name = "pools")]
627    fn py_pools(&self, venue: Option<Venue>) -> Vec<Pool> {
628        self.pools(venue.as_ref()).into_iter().cloned().collect()
629    }
630
631    #[cfg(feature = "defi")]
632    #[pyo3(name = "add_pool_profiler")]
633    fn py_add_pool_profiler(&mut self, pool_profiler: PoolProfiler) -> PyResult<()> {
634        self.add_pool_profiler(pool_profiler)
635            .map_err(to_pyvalue_err)
636    }
637
638    #[cfg(feature = "defi")]
639    #[pyo3(name = "pool_profiler")]
640    fn py_pool_profiler(&self, instrument_id: InstrumentId) -> Option<PoolProfiler> {
641        self.pool_profiler(&instrument_id).cloned()
642    }
643
644    #[cfg(feature = "defi")]
645    #[pyo3(name = "pool_profiler_ids")]
646    fn py_pool_profiler_ids(&self, venue: Option<Venue>) -> Vec<InstrumentId> {
647        self.pool_profiler_ids(venue.as_ref())
648    }
649
650    #[cfg(feature = "defi")]
651    #[pyo3(name = "pool_profilers")]
652    fn py_pool_profilers(&self, venue: Option<Venue>) -> Vec<PoolProfiler> {
653        self.pool_profilers(venue.as_ref())
654            .into_iter()
655            .cloned()
656            .collect()
657    }
658}