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