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