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