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, sync::LazyLock};
19
20use bytes::Bytes;
21use nautilus_core::python::to_pyvalue_err;
22#[cfg(feature = "defi")]
23use nautilus_model::defi::{Pool, PoolProfiler};
24use nautilus_model::{
25    data::{
26        Bar, BarType, FundingRateUpdate, InstrumentClose, InstrumentStatus, QuoteTick, TradeTick,
27        prices::{IndexPriceUpdate, MarkPriceUpdate},
28    },
29    enums::{AggregationSource, OmsType, OrderSide, PositionSide, PriceType},
30    identifiers::{
31        AccountId, ClientId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
32        StrategyId, Venue, VenueOrderId,
33    },
34    instruments::SyntheticInstrument,
35    orderbook::{OrderBook, own::OwnOrderBook},
36    orders::OrderList,
37    position::Position,
38    python::{
39        account::account_any_to_pyobject,
40        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
41        orders::{order_any_to_pyobject, pyobject_to_order_any},
42    },
43    types::{Currency, Money, Price, Quantity},
44};
45use pyo3::prelude::*;
46use rust_decimal::prelude::ToPrimitive;
47
48use crate::{
49    cache::{Cache, CacheConfig, database::CacheDatabaseFactory},
50    enums::SerializationEncoding,
51    python::{config_error_to_pyvalue_err, factory::FactoryRegistry},
52};
53
54/// Registry for Python cache database factory extractors.
55pub type CacheDatabaseFactoryRegistry = FactoryRegistry<dyn CacheDatabaseFactory>;
56
57static GLOBAL_CACHE_DATABASE_FACTORY_REGISTRY: LazyLock<CacheDatabaseFactoryRegistry> =
58    LazyLock::new(|| CacheDatabaseFactoryRegistry::new("cache database factory"));
59
60/// Returns the global Python cache database factory registry.
61#[must_use]
62pub fn get_global_cache_database_factory_registry() -> &'static CacheDatabaseFactoryRegistry {
63    &GLOBAL_CACHE_DATABASE_FACTORY_REGISTRY
64}
65
66/// Wrapper providing shared access to [`Cache`] from Python.
67///
68/// This wrapper holds an `Rc<RefCell<Cache>>` allowing actors to share
69/// the same cache instance. All methods delegate to the underlying cache.
70#[allow(non_camel_case_types)]
71#[pyo3::pyclass(
72    module = "nautilus_trader.common",
73    name = "Cache",
74    unsendable,
75    from_py_object
76)]
77#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
78#[derive(Debug, Clone)]
79pub struct PyCache(Rc<RefCell<Cache>>);
80
81impl PyCache {
82    /// Creates a `PyCache` from an `Rc<RefCell<Cache>>`.
83    #[must_use]
84    pub fn from_rc(rc: Rc<RefCell<Cache>>) -> Self {
85        Self(rc)
86    }
87
88    /// Gets the inner `Rc<RefCell<Cache>>` for use in Rust code.
89    #[must_use]
90    pub fn cache_rc(&self) -> Rc<RefCell<Cache>> {
91        self.0.clone()
92    }
93}
94
95#[pymethods]
96#[pyo3_stub_gen::derive::gen_stub_pymethods]
97impl PyCache {
98    #[new]
99    #[pyo3(signature = (config=None))]
100    fn py_new(config: Option<CacheConfig>) -> PyResult<Self> {
101        let cache = Cache::try_new(config, None).map_err(config_error_to_pyvalue_err)?;
102        Ok(Self(Rc::new(RefCell::new(cache))))
103    }
104
105    #[pyo3(name = "reset")]
106    fn py_reset(&mut self) {
107        self.0.borrow_mut().reset();
108    }
109
110    #[pyo3(name = "dispose")]
111    fn py_dispose(&mut self) {
112        self.0.borrow_mut().dispose();
113    }
114
115    #[pyo3(name = "purge_closed_orders", signature = (ts_now, buffer_secs=0))]
116    fn py_purge_closed_orders(&mut self, ts_now: u64, buffer_secs: u64) {
117        self.0
118            .borrow_mut()
119            .purge_closed_orders(ts_now.into(), buffer_secs);
120    }
121
122    #[pyo3(name = "purge_closed_positions", signature = (ts_now, buffer_secs=0))]
123    fn py_purge_closed_positions(&mut self, ts_now: u64, buffer_secs: u64) {
124        self.0
125            .borrow_mut()
126            .purge_closed_positions(ts_now.into(), buffer_secs);
127    }
128
129    #[pyo3(name = "purge_order")]
130    fn py_purge_order(&mut self, client_order_id: ClientOrderId) {
131        self.0.borrow_mut().purge_order(client_order_id);
132    }
133
134    #[pyo3(name = "purge_position")]
135    fn py_purge_position(&mut self, position_id: PositionId) {
136        self.0.borrow_mut().purge_position(position_id);
137    }
138
139    #[pyo3(name = "purge_instrument")]
140    fn py_purge_instrument(&mut self, instrument_id: InstrumentId) {
141        self.0.borrow_mut().purge_instrument(instrument_id);
142    }
143
144    #[pyo3(name = "purge_account_events", signature = (ts_now, lookback_secs=0))]
145    fn py_purge_account_events(&mut self, ts_now: u64, lookback_secs: u64) {
146        self.0
147            .borrow_mut()
148            .purge_account_events(ts_now.into(), lookback_secs);
149    }
150
151    #[pyo3(name = "get")]
152    fn py_get(&self, key: &str) -> PyResult<Option<Vec<u8>>> {
153        match self.0.borrow().get(key).map_err(to_pyvalue_err)? {
154            Some(bytes) => Ok(Some(bytes.to_vec())),
155            None => Ok(None),
156        }
157    }
158
159    #[pyo3(name = "add")]
160    fn py_add_general(&mut self, key: &str, value: Vec<u8>) -> PyResult<()> {
161        self.0
162            .borrow_mut()
163            .add(key, Bytes::from(value))
164            .map_err(to_pyvalue_err)
165    }
166
167    /// Adds an instrument close, replacing any close cached for the same instrument.
168    #[pyo3(name = "add_instrument_close")]
169    fn py_add_instrument_close(&mut self, close: InstrumentClose) -> PyResult<()> {
170        self.0
171            .borrow_mut()
172            .add_instrument_close(close)
173            .map_err(to_pyvalue_err)
174    }
175
176    #[pyo3(name = "quote", signature = (instrument_id, index=0))]
177    fn py_quote(&self, instrument_id: InstrumentId, index: usize) -> Option<QuoteTick> {
178        self.0
179            .borrow()
180            .quote_at_index(&instrument_id, index)
181            .copied()
182    }
183
184    #[pyo3(name = "trade", signature = (instrument_id, index=0))]
185    fn py_trade(&self, instrument_id: InstrumentId, index: usize) -> Option<TradeTick> {
186        self.0
187            .borrow()
188            .trade_at_index(&instrument_id, index)
189            .copied()
190    }
191
192    #[pyo3(name = "bar", signature = (bar_type, index=0))]
193    fn py_bar(&self, bar_type: BarType, index: usize) -> Option<Bar> {
194        self.0.borrow().bar_at_index(&bar_type, index).copied()
195    }
196
197    #[pyo3(name = "quotes")]
198    fn py_quotes(&self, instrument_id: InstrumentId) -> Option<Vec<QuoteTick>> {
199        self.0.borrow().quotes(&instrument_id)
200    }
201
202    #[pyo3(name = "trades")]
203    fn py_trades(&self, instrument_id: InstrumentId) -> Option<Vec<TradeTick>> {
204        self.0.borrow().trades(&instrument_id)
205    }
206
207    #[pyo3(name = "bars")]
208    fn py_bars(&self, bar_type: BarType) -> Option<Vec<Bar>> {
209        self.0.borrow().bars(&bar_type)
210    }
211
212    #[pyo3(name = "bar_types", signature = (aggregation_source, instrument_id=None, price_type=None))]
213    fn py_bar_types(
214        &self,
215        aggregation_source: AggregationSource,
216        instrument_id: Option<InstrumentId>,
217        price_type: Option<PriceType>,
218    ) -> Vec<BarType> {
219        self.0
220            .borrow()
221            .bar_types(
222                instrument_id.as_ref(),
223                price_type.as_ref(),
224                aggregation_source,
225            )
226            .into_iter()
227            .copied()
228            .collect()
229    }
230
231    #[pyo3(name = "mark_price")]
232    fn py_mark_price(&self, instrument_id: InstrumentId) -> Option<MarkPriceUpdate> {
233        self.0.borrow().mark_price(&instrument_id).copied()
234    }
235
236    #[pyo3(name = "mark_prices")]
237    fn py_mark_prices(&self, instrument_id: InstrumentId) -> Option<Vec<MarkPriceUpdate>> {
238        self.0.borrow().mark_prices(&instrument_id)
239    }
240
241    #[pyo3(name = "index_price")]
242    fn py_index_price(&self, instrument_id: InstrumentId) -> Option<IndexPriceUpdate> {
243        self.0.borrow().index_price(&instrument_id).copied()
244    }
245
246    #[pyo3(name = "index_prices")]
247    fn py_index_prices(&self, instrument_id: InstrumentId) -> Option<Vec<IndexPriceUpdate>> {
248        self.0.borrow().index_prices(&instrument_id)
249    }
250
251    #[pyo3(name = "funding_rate")]
252    fn py_funding_rate(&self, instrument_id: InstrumentId) -> Option<FundingRateUpdate> {
253        self.0.borrow().funding_rate(&instrument_id).copied()
254    }
255
256    #[pyo3(name = "funding_rates")]
257    fn py_funding_rates(&self, instrument_id: InstrumentId) -> Option<Vec<FundingRateUpdate>> {
258        self.0.borrow().funding_rates(&instrument_id)
259    }
260
261    #[pyo3(name = "instrument_status")]
262    fn py_instrument_status(&self, instrument_id: InstrumentId) -> Option<InstrumentStatus> {
263        self.0.borrow().instrument_status(&instrument_id).copied()
264    }
265
266    #[pyo3(name = "instrument_statuses")]
267    fn py_instrument_statuses(&self, instrument_id: InstrumentId) -> Option<Vec<InstrumentStatus>> {
268        self.0.borrow().instrument_statuses(&instrument_id)
269    }
270
271    #[pyo3(name = "instrument_close")]
272    fn py_instrument_close(&self, instrument_id: InstrumentId) -> Option<InstrumentClose> {
273        self.0.borrow().instrument_close(&instrument_id).copied()
274    }
275
276    #[pyo3(name = "price")]
277    fn py_price(&self, instrument_id: InstrumentId, price_type: PriceType) -> Option<Price> {
278        self.0.borrow().price(&instrument_id, price_type)
279    }
280
281    #[pyo3(name = "order_book")]
282    fn py_order_book(&self, instrument_id: InstrumentId) -> Option<OrderBook> {
283        self.0.borrow().order_book(&instrument_id).cloned()
284    }
285
286    /// Returns the best bid/ask price and size for the `instrument_id`, without cloning the
287    /// resident order book.
288    ///
289    /// Returns `(bid_price, bid_size, ask_price, ask_size)`, or `None` if the book is
290    /// missing, empty, or one-sided.
291    ///
292    /// For L3 books, each size is the first order's size at the best level, not the
293    /// aggregate level size, consistent with the order book's best-size getters.
294    ///
295    /// Prefer this over `order_book()` in hot paths that only need top-of-book values, since
296    /// `order_book()` clones the full book and its cost scales with depth.
297    #[pyo3(name = "top_of_book")]
298    fn py_top_of_book(
299        &self,
300        instrument_id: InstrumentId,
301    ) -> Option<(Price, Quantity, Price, Quantity)> {
302        let cache = self.0.borrow();
303        let book = cache.order_book(&instrument_id)?;
304        Some((
305            book.best_bid_price()?,
306            book.best_bid_size()?,
307            book.best_ask_price()?,
308            book.best_ask_size()?,
309        ))
310    }
311
312    #[pyo3(name = "has_order_book")]
313    fn py_has_order_book(&self, instrument_id: InstrumentId) -> bool {
314        self.0.borrow().has_order_book(&instrument_id)
315    }
316
317    #[pyo3(name = "book_update_count")]
318    fn py_book_update_count(&self, instrument_id: InstrumentId) -> usize {
319        self.0.borrow().book_update_count(&instrument_id)
320    }
321
322    #[pyo3(name = "has_quote_ticks")]
323    fn py_has_quote_ticks(&self, instrument_id: InstrumentId) -> bool {
324        self.0.borrow().has_quote_ticks(&instrument_id)
325    }
326
327    #[pyo3(name = "has_trade_ticks")]
328    fn py_has_trade_ticks(&self, instrument_id: InstrumentId) -> bool {
329        self.0.borrow().has_trade_ticks(&instrument_id)
330    }
331
332    #[pyo3(name = "has_mark_prices")]
333    fn py_has_mark_prices(&self, instrument_id: InstrumentId) -> bool {
334        self.0.borrow().has_mark_prices(&instrument_id)
335    }
336
337    #[pyo3(name = "has_index_prices")]
338    fn py_has_index_prices(&self, instrument_id: InstrumentId) -> bool {
339        self.0.borrow().has_index_prices(&instrument_id)
340    }
341
342    #[pyo3(name = "has_funding_rates")]
343    fn py_has_funding_rates(&self, instrument_id: InstrumentId) -> bool {
344        self.0.borrow().has_funding_rates(&instrument_id)
345    }
346
347    #[pyo3(name = "has_instrument_statuses")]
348    fn py_has_instrument_statuses(&self, instrument_id: InstrumentId) -> bool {
349        self.0.borrow().has_instrument_statuses(&instrument_id)
350    }
351
352    #[pyo3(name = "has_instrument_close")]
353    fn py_has_instrument_close(&self, instrument_id: InstrumentId) -> bool {
354        self.0.borrow().has_instrument_close(&instrument_id)
355    }
356
357    #[pyo3(name = "has_bars")]
358    fn py_has_bars(&self, bar_type: BarType) -> bool {
359        self.0.borrow().has_bars(&bar_type)
360    }
361
362    #[pyo3(name = "quote_count")]
363    fn py_quote_count(&self, instrument_id: InstrumentId) -> usize {
364        self.0.borrow().quote_count(&instrument_id)
365    }
366
367    #[pyo3(name = "trade_count")]
368    fn py_trade_count(&self, instrument_id: InstrumentId) -> usize {
369        self.0.borrow().trade_count(&instrument_id)
370    }
371
372    #[pyo3(name = "mark_price_count")]
373    fn py_mark_price_count(&self, instrument_id: InstrumentId) -> usize {
374        self.0.borrow().mark_price_count(&instrument_id)
375    }
376
377    #[pyo3(name = "index_price_count")]
378    fn py_index_price_count(&self, instrument_id: InstrumentId) -> usize {
379        self.0.borrow().index_price_count(&instrument_id)
380    }
381
382    #[pyo3(name = "funding_rate_count")]
383    fn py_funding_rate_count(&self, instrument_id: InstrumentId) -> usize {
384        self.0.borrow().funding_rate_count(&instrument_id)
385    }
386
387    #[pyo3(name = "instrument_status_count")]
388    fn py_instrument_status_count(&self, instrument_id: InstrumentId) -> usize {
389        self.0.borrow().instrument_status_count(&instrument_id)
390    }
391
392    #[pyo3(name = "bar_count")]
393    fn py_bar_count(&self, bar_type: BarType) -> usize {
394        self.0.borrow().bar_count(&bar_type)
395    }
396
397    #[pyo3(name = "get_xrate")]
398    fn py_get_xrate(
399        &self,
400        venue: Venue,
401        from_currency: Currency,
402        to_currency: Currency,
403        price_type: PriceType,
404    ) -> Option<f64> {
405        self.0
406            .borrow()
407            .get_xrate(venue, from_currency, to_currency, price_type)
408            .and_then(|rate| rate.to_f64())
409    }
410
411    #[pyo3(name = "get_mark_xrate")]
412    fn py_get_mark_xrate(&self, from_currency: Currency, to_currency: Currency) -> Option<f64> {
413        self.0.borrow().get_mark_xrate(from_currency, to_currency)
414    }
415
416    #[pyo3(name = "own_order_book")]
417    fn py_own_order_book(&self, instrument_id: InstrumentId) -> Option<OwnOrderBook> {
418        self.0.borrow().own_order_book(&instrument_id).cloned()
419    }
420
421    #[pyo3(name = "instrument")]
422    fn py_instrument(
423        &self,
424        py: Python,
425        instrument_id: InstrumentId,
426    ) -> PyResult<Option<Py<PyAny>>> {
427        let cache = self.0.borrow();
428        match cache.instrument(&instrument_id) {
429            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument.clone())?)),
430            None => Ok(None),
431        }
432    }
433
434    #[pyo3(name = "instrument_ids", signature = (venue=None))]
435    fn py_instrument_ids(&self, venue: Option<Venue>) -> Vec<InstrumentId> {
436        self.0
437            .borrow()
438            .instrument_ids(venue.as_ref())
439            .into_iter()
440            .copied()
441            .collect()
442    }
443
444    #[pyo3(name = "instruments", signature = (venue=None))]
445    fn py_instruments(&self, py: Python, venue: Option<Venue>) -> PyResult<Vec<Py<PyAny>>> {
446        let cache = self.0.borrow();
447        let mut py_instruments = Vec::new();
448
449        match venue {
450            Some(venue) => {
451                for instrument in cache.instruments(&venue, None) {
452                    py_instruments.push(instrument_any_to_pyobject(py, (*instrument).clone())?);
453                }
454            }
455            None => {
456                for instrument_id in cache.instrument_ids(None) {
457                    if let Some(instrument) = cache.instrument(instrument_id) {
458                        py_instruments.push(instrument_any_to_pyobject(py, instrument.clone())?);
459                    }
460                }
461            }
462        }
463        Ok(py_instruments)
464    }
465
466    #[pyo3(name = "synthetic")]
467    fn py_synthetic(&self, instrument_id: InstrumentId) -> Option<SyntheticInstrument> {
468        self.0.borrow().synthetic(&instrument_id).cloned()
469    }
470
471    #[pyo3(name = "synthetic_ids")]
472    fn py_synthetic_ids(&self) -> Vec<InstrumentId> {
473        self.0
474            .borrow()
475            .synthetic_ids()
476            .into_iter()
477            .copied()
478            .collect()
479    }
480
481    #[pyo3(name = "account")]
482    fn py_account(&self, py: Python, account_id: AccountId) -> PyResult<Option<Py<PyAny>>> {
483        let cache = self.0.borrow();
484        match cache.account(&account_id) {
485            Some(account) => Ok(Some(account_any_to_pyobject(py, account.clone())?)),
486            None => Ok(None),
487        }
488    }
489
490    #[pyo3(name = "account_for_venue")]
491    fn py_account_for_venue(&self, py: Python, venue: Venue) -> PyResult<Option<Py<PyAny>>> {
492        let cache = self.0.borrow();
493        match cache.account_for_venue(&venue) {
494            Some(account) => Ok(Some(account_any_to_pyobject(py, account.clone())?)),
495            None => Ok(None),
496        }
497    }
498
499    #[pyo3(name = "account_id")]
500    fn py_account_id(&self, venue: Venue) -> Option<AccountId> {
501        self.0.borrow().account_id(&venue).copied()
502    }
503
504    #[pyo3(name = "client_order_ids", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
505    fn py_client_order_ids(
506        &self,
507        venue: Option<Venue>,
508        instrument_id: Option<InstrumentId>,
509        strategy_id: Option<StrategyId>,
510        account_id: Option<AccountId>,
511    ) -> Vec<ClientOrderId> {
512        self.0
513            .borrow()
514            .client_order_ids(
515                venue.as_ref(),
516                instrument_id.as_ref(),
517                strategy_id.as_ref(),
518                account_id.as_ref(),
519            )
520            .into_iter()
521            .collect()
522    }
523
524    #[pyo3(name = "client_order_ids_open", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
525    fn py_client_order_ids_open(
526        &self,
527        venue: Option<Venue>,
528        instrument_id: Option<InstrumentId>,
529        strategy_id: Option<StrategyId>,
530        account_id: Option<AccountId>,
531    ) -> Vec<ClientOrderId> {
532        self.0
533            .borrow()
534            .client_order_ids_open(
535                venue.as_ref(),
536                instrument_id.as_ref(),
537                strategy_id.as_ref(),
538                account_id.as_ref(),
539            )
540            .into_iter()
541            .collect()
542    }
543
544    #[pyo3(name = "client_order_ids_closed", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
545    fn py_client_order_ids_closed(
546        &self,
547        venue: Option<Venue>,
548        instrument_id: Option<InstrumentId>,
549        strategy_id: Option<StrategyId>,
550        account_id: Option<AccountId>,
551    ) -> Vec<ClientOrderId> {
552        self.0
553            .borrow()
554            .client_order_ids_closed(
555                venue.as_ref(),
556                instrument_id.as_ref(),
557                strategy_id.as_ref(),
558                account_id.as_ref(),
559            )
560            .into_iter()
561            .collect()
562    }
563
564    #[pyo3(name = "client_order_ids_emulated", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
565    fn py_client_order_ids_emulated(
566        &self,
567        venue: Option<Venue>,
568        instrument_id: Option<InstrumentId>,
569        strategy_id: Option<StrategyId>,
570        account_id: Option<AccountId>,
571    ) -> Vec<ClientOrderId> {
572        self.0
573            .borrow()
574            .client_order_ids_emulated(
575                venue.as_ref(),
576                instrument_id.as_ref(),
577                strategy_id.as_ref(),
578                account_id.as_ref(),
579            )
580            .into_iter()
581            .collect()
582    }
583
584    #[pyo3(name = "client_order_ids_inflight", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
585    fn py_client_order_ids_inflight(
586        &self,
587        venue: Option<Venue>,
588        instrument_id: Option<InstrumentId>,
589        strategy_id: Option<StrategyId>,
590        account_id: Option<AccountId>,
591    ) -> Vec<ClientOrderId> {
592        self.0
593            .borrow()
594            .client_order_ids_inflight(
595                venue.as_ref(),
596                instrument_id.as_ref(),
597                strategy_id.as_ref(),
598                account_id.as_ref(),
599            )
600            .into_iter()
601            .collect()
602    }
603
604    #[pyo3(name = "position_ids", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
605    fn py_position_ids(
606        &self,
607        venue: Option<Venue>,
608        instrument_id: Option<InstrumentId>,
609        strategy_id: Option<StrategyId>,
610        account_id: Option<AccountId>,
611    ) -> Vec<PositionId> {
612        self.0
613            .borrow()
614            .position_ids(
615                venue.as_ref(),
616                instrument_id.as_ref(),
617                strategy_id.as_ref(),
618                account_id.as_ref(),
619            )
620            .into_iter()
621            .collect()
622    }
623
624    #[pyo3(name = "position_open_ids", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
625    fn py_position_open_ids(
626        &self,
627        venue: Option<Venue>,
628        instrument_id: Option<InstrumentId>,
629        strategy_id: Option<StrategyId>,
630        account_id: Option<AccountId>,
631    ) -> Vec<PositionId> {
632        self.0
633            .borrow()
634            .position_open_ids(
635                venue.as_ref(),
636                instrument_id.as_ref(),
637                strategy_id.as_ref(),
638                account_id.as_ref(),
639            )
640            .into_iter()
641            .collect()
642    }
643
644    #[pyo3(name = "position_closed_ids", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
645    fn py_position_closed_ids(
646        &self,
647        venue: Option<Venue>,
648        instrument_id: Option<InstrumentId>,
649        strategy_id: Option<StrategyId>,
650        account_id: Option<AccountId>,
651    ) -> Vec<PositionId> {
652        self.0
653            .borrow()
654            .position_closed_ids(
655                venue.as_ref(),
656                instrument_id.as_ref(),
657                strategy_id.as_ref(),
658                account_id.as_ref(),
659            )
660            .into_iter()
661            .collect()
662    }
663
664    #[pyo3(name = "strategy_ids")]
665    fn py_strategy_ids(&self) -> Vec<StrategyId> {
666        self.0.borrow().strategy_ids().into_iter().collect()
667    }
668
669    #[pyo3(name = "exec_algorithm_ids")]
670    fn py_exec_algorithm_ids(&self) -> Vec<ExecAlgorithmId> {
671        self.0.borrow().exec_algorithm_ids().into_iter().collect()
672    }
673
674    #[pyo3(name = "order")]
675    fn py_order(&self, py: Python, client_order_id: ClientOrderId) -> PyResult<Option<Py<PyAny>>> {
676        let cache = self.0.borrow();
677        match cache.order(&client_order_id) {
678            Some(order) => Ok(Some(order_any_to_pyobject(py, order.clone())?)),
679            None => Ok(None),
680        }
681    }
682
683    #[pyo3(name = "client_order_id")]
684    fn py_client_order_id(&self, venue_order_id: VenueOrderId) -> Option<ClientOrderId> {
685        self.0.borrow().client_order_id(&venue_order_id).copied()
686    }
687
688    #[pyo3(name = "venue_order_id")]
689    fn py_venue_order_id(&self, client_order_id: ClientOrderId) -> Option<VenueOrderId> {
690        self.0.borrow().venue_order_id(&client_order_id).copied()
691    }
692
693    #[pyo3(name = "client_id")]
694    fn py_client_id(&self, client_order_id: ClientOrderId) -> Option<ClientId> {
695        self.0.borrow().client_id(&client_order_id).copied()
696    }
697
698    #[pyo3(name = "orders", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
699    fn py_orders(
700        &self,
701        py: Python,
702        venue: Option<Venue>,
703        instrument_id: Option<InstrumentId>,
704        strategy_id: Option<StrategyId>,
705        account_id: Option<AccountId>,
706        side: Option<OrderSide>,
707    ) -> PyResult<Vec<Py<PyAny>>> {
708        let cache = self.0.borrow();
709        cache
710            .orders(
711                venue.as_ref(),
712                instrument_id.as_ref(),
713                strategy_id.as_ref(),
714                account_id.as_ref(),
715                side,
716            )
717            .into_iter()
718            .map(|o| order_any_to_pyobject(py, o.clone()))
719            .collect()
720    }
721
722    #[pyo3(name = "orders_open", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
723    fn py_orders_open(
724        &self,
725        py: Python,
726        venue: Option<Venue>,
727        instrument_id: Option<InstrumentId>,
728        strategy_id: Option<StrategyId>,
729        account_id: Option<AccountId>,
730        side: Option<OrderSide>,
731    ) -> PyResult<Vec<Py<PyAny>>> {
732        let cache = self.0.borrow();
733        cache
734            .orders_open(
735                venue.as_ref(),
736                instrument_id.as_ref(),
737                strategy_id.as_ref(),
738                account_id.as_ref(),
739                side,
740            )
741            .into_iter()
742            .map(|o| order_any_to_pyobject(py, o.clone()))
743            .collect()
744    }
745
746    #[pyo3(name = "orders_closed", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
747    fn py_orders_closed(
748        &self,
749        py: Python,
750        venue: Option<Venue>,
751        instrument_id: Option<InstrumentId>,
752        strategy_id: Option<StrategyId>,
753        account_id: Option<AccountId>,
754        side: Option<OrderSide>,
755    ) -> PyResult<Vec<Py<PyAny>>> {
756        let cache = self.0.borrow();
757        cache
758            .orders_closed(
759                venue.as_ref(),
760                instrument_id.as_ref(),
761                strategy_id.as_ref(),
762                account_id.as_ref(),
763                side,
764            )
765            .into_iter()
766            .map(|o| order_any_to_pyobject(py, o.clone()))
767            .collect()
768    }
769
770    #[pyo3(name = "orders_emulated", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
771    fn py_orders_emulated(
772        &self,
773        py: Python,
774        venue: Option<Venue>,
775        instrument_id: Option<InstrumentId>,
776        strategy_id: Option<StrategyId>,
777        account_id: Option<AccountId>,
778        side: Option<OrderSide>,
779    ) -> PyResult<Vec<Py<PyAny>>> {
780        let cache = self.0.borrow();
781        cache
782            .orders_emulated(
783                venue.as_ref(),
784                instrument_id.as_ref(),
785                strategy_id.as_ref(),
786                account_id.as_ref(),
787                side,
788            )
789            .into_iter()
790            .map(|o| order_any_to_pyobject(py, o.clone()))
791            .collect()
792    }
793
794    #[pyo3(name = "orders_inflight", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
795    fn py_orders_inflight(
796        &self,
797        py: Python,
798        venue: Option<Venue>,
799        instrument_id: Option<InstrumentId>,
800        strategy_id: Option<StrategyId>,
801        account_id: Option<AccountId>,
802        side: Option<OrderSide>,
803    ) -> PyResult<Vec<Py<PyAny>>> {
804        let cache = self.0.borrow();
805        cache
806            .orders_inflight(
807                venue.as_ref(),
808                instrument_id.as_ref(),
809                strategy_id.as_ref(),
810                account_id.as_ref(),
811                side,
812            )
813            .into_iter()
814            .map(|o| order_any_to_pyobject(py, o.clone()))
815            .collect()
816    }
817
818    #[pyo3(name = "orders_for_position")]
819    fn py_orders_for_position(
820        &self,
821        py: Python,
822        position_id: PositionId,
823    ) -> PyResult<Vec<Py<PyAny>>> {
824        let cache = self.0.borrow();
825        cache
826            .orders_for_position(&position_id)
827            .into_iter()
828            .map(|o| order_any_to_pyobject(py, o.clone()))
829            .collect()
830    }
831
832    #[pyo3(name = "order_exists")]
833    fn py_order_exists(&self, client_order_id: ClientOrderId) -> bool {
834        self.0.borrow().order_exists(&client_order_id)
835    }
836
837    #[pyo3(name = "is_order_open")]
838    fn py_is_order_open(&self, client_order_id: ClientOrderId) -> bool {
839        self.0.borrow().is_order_open(&client_order_id)
840    }
841
842    #[pyo3(name = "is_order_closed")]
843    fn py_is_order_closed(&self, client_order_id: ClientOrderId) -> bool {
844        self.0.borrow().is_order_closed(&client_order_id)
845    }
846
847    #[pyo3(name = "is_order_emulated")]
848    fn py_is_order_emulated(&self, client_order_id: ClientOrderId) -> bool {
849        self.0.borrow().is_order_emulated(&client_order_id)
850    }
851
852    #[pyo3(name = "is_order_inflight")]
853    fn py_is_order_inflight(&self, client_order_id: ClientOrderId) -> bool {
854        self.0.borrow().is_order_inflight(&client_order_id)
855    }
856
857    #[pyo3(name = "is_order_pending_cancel_local")]
858    fn py_is_order_pending_cancel_local(&self, client_order_id: ClientOrderId) -> bool {
859        self.0
860            .borrow()
861            .is_order_pending_cancel_local(&client_order_id)
862    }
863
864    #[pyo3(name = "orders_open_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
865    fn py_orders_open_count(
866        &self,
867        venue: Option<Venue>,
868        instrument_id: Option<InstrumentId>,
869        strategy_id: Option<StrategyId>,
870        account_id: Option<AccountId>,
871        side: Option<OrderSide>,
872    ) -> usize {
873        self.0.borrow().orders_open_count(
874            venue.as_ref(),
875            instrument_id.as_ref(),
876            strategy_id.as_ref(),
877            account_id.as_ref(),
878            side,
879        )
880    }
881
882    #[pyo3(name = "orders_closed_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
883    fn py_orders_closed_count(
884        &self,
885        venue: Option<Venue>,
886        instrument_id: Option<InstrumentId>,
887        strategy_id: Option<StrategyId>,
888        account_id: Option<AccountId>,
889        side: Option<OrderSide>,
890    ) -> usize {
891        self.0.borrow().orders_closed_count(
892            venue.as_ref(),
893            instrument_id.as_ref(),
894            strategy_id.as_ref(),
895            account_id.as_ref(),
896            side,
897        )
898    }
899
900    #[pyo3(name = "orders_emulated_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
901    fn py_orders_emulated_count(
902        &self,
903        venue: Option<Venue>,
904        instrument_id: Option<InstrumentId>,
905        strategy_id: Option<StrategyId>,
906        account_id: Option<AccountId>,
907        side: Option<OrderSide>,
908    ) -> usize {
909        self.0.borrow().orders_emulated_count(
910            venue.as_ref(),
911            instrument_id.as_ref(),
912            strategy_id.as_ref(),
913            account_id.as_ref(),
914            side,
915        )
916    }
917
918    #[pyo3(name = "orders_inflight_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
919    fn py_orders_inflight_count(
920        &self,
921        venue: Option<Venue>,
922        instrument_id: Option<InstrumentId>,
923        strategy_id: Option<StrategyId>,
924        account_id: Option<AccountId>,
925        side: Option<OrderSide>,
926    ) -> usize {
927        self.0.borrow().orders_inflight_count(
928            venue.as_ref(),
929            instrument_id.as_ref(),
930            strategy_id.as_ref(),
931            account_id.as_ref(),
932            side,
933        )
934    }
935
936    #[pyo3(name = "orders_total_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
937    fn py_orders_total_count(
938        &self,
939        venue: Option<Venue>,
940        instrument_id: Option<InstrumentId>,
941        strategy_id: Option<StrategyId>,
942        account_id: Option<AccountId>,
943        side: Option<OrderSide>,
944    ) -> usize {
945        self.0.borrow().orders_total_count(
946            venue.as_ref(),
947            instrument_id.as_ref(),
948            strategy_id.as_ref(),
949            account_id.as_ref(),
950            side,
951        )
952    }
953
954    #[pyo3(name = "order_list")]
955    fn py_order_list(&self, order_list_id: OrderListId) -> Option<OrderList> {
956        self.0.borrow().order_list(&order_list_id).cloned()
957    }
958
959    #[pyo3(name = "order_lists", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None))]
960    fn py_order_lists(
961        &self,
962        venue: Option<Venue>,
963        instrument_id: Option<InstrumentId>,
964        strategy_id: Option<StrategyId>,
965        account_id: Option<AccountId>,
966    ) -> Vec<OrderList> {
967        let cache = self.0.borrow();
968        cache
969            .order_lists(
970                venue.as_ref(),
971                instrument_id.as_ref(),
972                strategy_id.as_ref(),
973                account_id.as_ref(),
974            )
975            .into_iter()
976            .cloned()
977            .collect()
978    }
979
980    #[pyo3(name = "order_list_exists")]
981    fn py_order_list_exists(&self, order_list_id: OrderListId) -> bool {
982        self.0.borrow().order_list_exists(&order_list_id)
983    }
984
985    #[pyo3(name = "orders_for_exec_algorithm", signature = (exec_algorithm_id, venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
986    #[expect(clippy::too_many_arguments)]
987    fn py_orders_for_exec_algorithm(
988        &self,
989        py: Python,
990        exec_algorithm_id: ExecAlgorithmId,
991        venue: Option<Venue>,
992        instrument_id: Option<InstrumentId>,
993        strategy_id: Option<StrategyId>,
994        account_id: Option<AccountId>,
995        side: Option<OrderSide>,
996    ) -> PyResult<Vec<Py<PyAny>>> {
997        let cache = self.0.borrow();
998        cache
999            .orders_for_exec_algorithm(
1000                &exec_algorithm_id,
1001                venue.as_ref(),
1002                instrument_id.as_ref(),
1003                strategy_id.as_ref(),
1004                account_id.as_ref(),
1005                side,
1006            )
1007            .into_iter()
1008            .map(|o| order_any_to_pyobject(py, o.clone()))
1009            .collect()
1010    }
1011
1012    #[pyo3(name = "orders_for_exec_spawn")]
1013    fn py_orders_for_exec_spawn(
1014        &self,
1015        py: Python,
1016        exec_spawn_id: ClientOrderId,
1017    ) -> PyResult<Vec<Py<PyAny>>> {
1018        let cache = self.0.borrow();
1019        cache
1020            .orders_for_exec_spawn(&exec_spawn_id)
1021            .into_iter()
1022            .map(|o| order_any_to_pyobject(py, o.clone()))
1023            .collect()
1024    }
1025
1026    #[pyo3(name = "exec_spawn_total_quantity")]
1027    fn py_exec_spawn_total_quantity(
1028        &self,
1029        exec_spawn_id: ClientOrderId,
1030        active_only: bool,
1031    ) -> Option<Quantity> {
1032        self.0
1033            .borrow()
1034            .exec_spawn_total_quantity(&exec_spawn_id, active_only)
1035    }
1036
1037    #[pyo3(name = "exec_spawn_total_filled_qty")]
1038    fn py_exec_spawn_total_filled_qty(
1039        &self,
1040        exec_spawn_id: ClientOrderId,
1041        active_only: bool,
1042    ) -> Option<Quantity> {
1043        self.0
1044            .borrow()
1045            .exec_spawn_total_filled_qty(&exec_spawn_id, active_only)
1046    }
1047
1048    #[pyo3(name = "exec_spawn_total_leaves_qty")]
1049    fn py_exec_spawn_total_leaves_qty(
1050        &self,
1051        exec_spawn_id: ClientOrderId,
1052        active_only: bool,
1053    ) -> Option<Quantity> {
1054        self.0
1055            .borrow()
1056            .exec_spawn_total_leaves_qty(&exec_spawn_id, active_only)
1057    }
1058
1059    #[pyo3(name = "position")]
1060    fn py_position(&self, py: Python, position_id: PositionId) -> PyResult<Option<Py<PyAny>>> {
1061        let cache = self.0.borrow();
1062        match cache.position(&position_id) {
1063            Some(position) => Ok(Some(position.clone().into_pyobject(py)?.into())),
1064            None => Ok(None),
1065        }
1066    }
1067
1068    #[pyo3(name = "position_for_order")]
1069    fn py_position_for_order(
1070        &self,
1071        py: Python,
1072        client_order_id: ClientOrderId,
1073    ) -> PyResult<Option<Py<PyAny>>> {
1074        let cache = self.0.borrow();
1075        match cache.position_for_order(&client_order_id) {
1076            Some(position) => Ok(Some(position.clone().into_pyobject(py)?.into())),
1077            None => Ok(None),
1078        }
1079    }
1080
1081    #[pyo3(name = "position_id")]
1082    fn py_position_id(&self, client_order_id: ClientOrderId) -> Option<PositionId> {
1083        self.0.borrow().position_id(&client_order_id).copied()
1084    }
1085
1086    #[pyo3(name = "positions", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
1087    fn py_positions(
1088        &self,
1089        py: Python,
1090        venue: Option<Venue>,
1091        instrument_id: Option<InstrumentId>,
1092        strategy_id: Option<StrategyId>,
1093        account_id: Option<AccountId>,
1094        side: Option<PositionSide>,
1095    ) -> PyResult<Vec<Py<PyAny>>> {
1096        let cache = self.0.borrow();
1097        cache
1098            .positions(
1099                venue.as_ref(),
1100                instrument_id.as_ref(),
1101                strategy_id.as_ref(),
1102                account_id.as_ref(),
1103                side,
1104            )
1105            .into_iter()
1106            .map(|p| Ok(p.clone().into_pyobject(py)?.into()))
1107            .collect()
1108    }
1109
1110    #[pyo3(name = "positions_open", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
1111    fn py_positions_open(
1112        &self,
1113        py: Python,
1114        venue: Option<Venue>,
1115        instrument_id: Option<InstrumentId>,
1116        strategy_id: Option<StrategyId>,
1117        account_id: Option<AccountId>,
1118        side: Option<PositionSide>,
1119    ) -> PyResult<Vec<Py<PyAny>>> {
1120        let cache = self.0.borrow();
1121        cache
1122            .positions_open(
1123                venue.as_ref(),
1124                instrument_id.as_ref(),
1125                strategy_id.as_ref(),
1126                account_id.as_ref(),
1127                side,
1128            )
1129            .into_iter()
1130            .map(|p| Ok(p.clone().into_pyobject(py)?.into()))
1131            .collect()
1132    }
1133
1134    #[pyo3(name = "positions_closed", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
1135    fn py_positions_closed(
1136        &self,
1137        py: Python,
1138        venue: Option<Venue>,
1139        instrument_id: Option<InstrumentId>,
1140        strategy_id: Option<StrategyId>,
1141        account_id: Option<AccountId>,
1142        side: Option<PositionSide>,
1143    ) -> PyResult<Vec<Py<PyAny>>> {
1144        let cache = self.0.borrow();
1145        cache
1146            .positions_closed(
1147                venue.as_ref(),
1148                instrument_id.as_ref(),
1149                strategy_id.as_ref(),
1150                account_id.as_ref(),
1151                side,
1152            )
1153            .into_iter()
1154            .map(|p| Ok(p.clone().into_pyobject(py)?.into()))
1155            .collect()
1156    }
1157
1158    #[pyo3(name = "position_exists")]
1159    fn py_position_exists(&self, position_id: PositionId) -> bool {
1160        self.0.borrow().position_exists(&position_id)
1161    }
1162
1163    #[pyo3(name = "is_position_open")]
1164    fn py_is_position_open(&self, position_id: PositionId) -> bool {
1165        self.0.borrow().is_position_open(&position_id)
1166    }
1167
1168    #[pyo3(name = "is_position_closed")]
1169    fn py_is_position_closed(&self, position_id: PositionId) -> bool {
1170        self.0.borrow().is_position_closed(&position_id)
1171    }
1172
1173    #[pyo3(name = "positions_open_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
1174    fn py_positions_open_count(
1175        &self,
1176        venue: Option<Venue>,
1177        instrument_id: Option<InstrumentId>,
1178        strategy_id: Option<StrategyId>,
1179        account_id: Option<AccountId>,
1180        side: Option<PositionSide>,
1181    ) -> usize {
1182        self.0.borrow().positions_open_count(
1183            venue.as_ref(),
1184            instrument_id.as_ref(),
1185            strategy_id.as_ref(),
1186            account_id.as_ref(),
1187            side,
1188        )
1189    }
1190
1191    #[pyo3(name = "positions_closed_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
1192    fn py_positions_closed_count(
1193        &self,
1194        venue: Option<Venue>,
1195        instrument_id: Option<InstrumentId>,
1196        strategy_id: Option<StrategyId>,
1197        account_id: Option<AccountId>,
1198        side: Option<PositionSide>,
1199    ) -> usize {
1200        self.0.borrow().positions_closed_count(
1201            venue.as_ref(),
1202            instrument_id.as_ref(),
1203            strategy_id.as_ref(),
1204            account_id.as_ref(),
1205            side,
1206        )
1207    }
1208
1209    #[pyo3(name = "positions_total_count", signature = (venue=None, instrument_id=None, strategy_id=None, account_id=None, side=None))]
1210    fn py_positions_total_count(
1211        &self,
1212        venue: Option<Venue>,
1213        instrument_id: Option<InstrumentId>,
1214        strategy_id: Option<StrategyId>,
1215        account_id: Option<AccountId>,
1216        side: Option<PositionSide>,
1217    ) -> usize {
1218        self.0.borrow().positions_total_count(
1219            venue.as_ref(),
1220            instrument_id.as_ref(),
1221            strategy_id.as_ref(),
1222            account_id.as_ref(),
1223            side,
1224        )
1225    }
1226
1227    #[pyo3(name = "strategy_id_for_order")]
1228    fn py_strategy_id_for_order(&self, client_order_id: ClientOrderId) -> Option<StrategyId> {
1229        self.0
1230            .borrow()
1231            .strategy_id_for_order(&client_order_id)
1232            .copied()
1233    }
1234
1235    #[pyo3(name = "strategy_id_for_position")]
1236    fn py_strategy_id_for_position(&self, position_id: PositionId) -> Option<StrategyId> {
1237        self.0
1238            .borrow()
1239            .strategy_id_for_position(&position_id)
1240            .copied()
1241    }
1242
1243    #[pyo3(name = "position_snapshot_bytes")]
1244    fn py_position_snapshot_bytes(&self, position_id: PositionId) -> Option<Vec<Vec<u8>>> {
1245        self.0.borrow().position_snapshot_bytes(&position_id)
1246    }
1247
1248    #[pyo3(name = "snapshot_position")]
1249    #[expect(clippy::needless_pass_by_value)]
1250    fn py_snapshot_position(&self, py: Python, position: Py<PyAny>) -> PyResult<()> {
1251        let position_obj = position.extract::<Position>(py)?;
1252        self.0
1253            .borrow_mut()
1254            .snapshot_position(&position_obj)
1255            .map_err(to_pyvalue_err)
1256    }
1257
1258    #[pyo3(name = "position_snapshots", signature = (position_id=None, account_id=None))]
1259    fn py_position_snapshots(
1260        &self,
1261        py: Python,
1262        position_id: Option<PositionId>,
1263        account_id: Option<AccountId>,
1264    ) -> PyResult<Vec<Py<PyAny>>> {
1265        let cache = self.0.borrow();
1266        cache
1267            .position_snapshots(position_id.as_ref(), account_id.as_ref())
1268            .into_iter()
1269            .map(|p| Ok(p.into_pyobject(py)?.into()))
1270            .collect()
1271    }
1272}
1273
1274#[cfg(test)]
1275mod tests {
1276    use nautilus_core::UnixNanos;
1277    use nautilus_model::{
1278        data::{BookOrder, stubs::stub_instrument_close},
1279        enums::{BookType, InstrumentCloseType},
1280    };
1281    use pyo3::exceptions::PyValueError;
1282    use rstest::rstest;
1283
1284    use super::*;
1285
1286    fn book_order(side: OrderSide, price: &str, size: &str, id: u64) -> BookOrder {
1287        BookOrder::new(side, Price::from(price), Quantity::from(size), id)
1288    }
1289
1290    #[rstest]
1291    fn test_top_of_book_missing() {
1292        let cache = PyCache::from_rc(Rc::new(RefCell::new(Cache::default())));
1293
1294        assert_eq!(
1295            cache.py_top_of_book(InstrumentId::from("AUD/USD.SIM")),
1296            None
1297        );
1298    }
1299
1300    #[rstest]
1301    #[case::empty(None)]
1302    #[case::bid_only(Some(OrderSide::Buy))]
1303    #[case::ask_only(Some(OrderSide::Sell))]
1304    fn test_top_of_book_incomplete(#[case] side: Option<OrderSide>) {
1305        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1306        let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
1307        if let Some(side) = side {
1308            book.add(book_order(side, "0.70000", "10", 1), 0, 1, 1.into());
1309        }
1310        let mut cache = Cache::default();
1311        cache.add_order_book(book).unwrap();
1312        let cache = PyCache::from_rc(Rc::new(RefCell::new(cache)));
1313
1314        assert_eq!(cache.py_top_of_book(instrument_id), None);
1315    }
1316
1317    #[rstest]
1318    fn test_top_of_book_python_values_and_resident_updates() {
1319        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1320        let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
1321        book.add(
1322            book_order(OrderSide::Buy, "0.70000", "10", 1),
1323            0,
1324            1,
1325            1.into(),
1326        );
1327        book.add(
1328            book_order(OrderSide::Sell, "0.70010", "20", 2),
1329            0,
1330            2,
1331            2.into(),
1332        );
1333        let mut cache = Cache::default();
1334        cache.add_order_book(book).unwrap();
1335        let cache = Rc::new(RefCell::new(cache));
1336
1337        Python::initialize();
1338        Python::attach(|py| {
1339            let py_cache = Py::new(py, PyCache::from_rc(cache.clone())).unwrap();
1340            let original = py_cache
1341                .call_method1(py, "top_of_book", (instrument_id,))
1342                .unwrap();
1343            let expected = (
1344                Price::from("0.70000"),
1345                Quantity::from("10"),
1346                Price::from("0.70010"),
1347                Quantity::from("20"),
1348            );
1349            assert_eq!(
1350                original
1351                    .extract::<(Price, Quantity, Price, Quantity)>(py)
1352                    .unwrap(),
1353                expected
1354            );
1355
1356            {
1357                let mut cache = cache.borrow_mut();
1358                let book = cache.order_book_mut(&instrument_id).unwrap();
1359                book.update(
1360                    book_order(OrderSide::Buy, "0.70000", "15", 1),
1361                    0,
1362                    3,
1363                    3.into(),
1364                );
1365                book.add(
1366                    book_order(OrderSide::Sell, "0.70005", "25", 3),
1367                    0,
1368                    4,
1369                    4.into(),
1370                );
1371            }
1372            let updated = py_cache
1373                .call_method1(py, "top_of_book", (instrument_id,))
1374                .unwrap()
1375                .extract::<(Price, Quantity, Price, Quantity)>(py)
1376                .unwrap();
1377            assert_eq!(
1378                updated,
1379                (
1380                    Price::from("0.70000"),
1381                    Quantity::from("15"),
1382                    Price::from("0.70005"),
1383                    Quantity::from("25"),
1384                )
1385            );
1386            assert_eq!(
1387                original
1388                    .extract::<(Price, Quantity, Price, Quantity)>(py)
1389                    .unwrap(),
1390                expected
1391            );
1392
1393            cache
1394                .borrow_mut()
1395                .order_book_mut(&instrument_id)
1396                .unwrap()
1397                .clear_asks(5, 5.into());
1398            assert!(
1399                py_cache
1400                    .call_method1(py, "top_of_book", (instrument_id,))
1401                    .unwrap()
1402                    .is_none(py)
1403            );
1404        });
1405    }
1406
1407    #[rstest]
1408    fn test_top_of_book_l3_returns_first_order_sizes() {
1409        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1410        let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
1411        let first_bid = book_order(OrderSide::Buy, "0.70000", "10", 1);
1412        let first_ask = book_order(OrderSide::Sell, "0.70010", "20", 3);
1413        book.add(first_bid, 0, 1, 1.into());
1414        book.add(
1415            book_order(OrderSide::Buy, "0.70000", "30", 2),
1416            0,
1417            2,
1418            2.into(),
1419        );
1420        book.add(first_ask, 0, 3, 3.into());
1421        book.add(
1422            book_order(OrderSide::Sell, "0.70010", "40", 4),
1423            0,
1424            4,
1425            4.into(),
1426        );
1427        let mut cache = Cache::default();
1428        cache.add_order_book(book).unwrap();
1429        let cache = PyCache::from_rc(Rc::new(RefCell::new(cache)));
1430
1431        assert_eq!(
1432            cache.py_top_of_book(instrument_id),
1433            Some((
1434                Price::from("0.70000"),
1435                Quantity::from("10"),
1436                Price::from("0.70010"),
1437                Quantity::from("20"),
1438            ))
1439        );
1440
1441        {
1442            let mut inner = cache.0.borrow_mut();
1443            let book = inner.order_book_mut(&instrument_id).unwrap();
1444            book.delete(first_bid, 0, 5, 5.into());
1445            book.delete(first_ask, 0, 6, 6.into());
1446        }
1447        assert_eq!(
1448            cache.py_top_of_book(instrument_id),
1449            Some((
1450                Price::from("0.70000"),
1451                Quantity::from("30"),
1452                Price::from("0.70010"),
1453                Quantity::from("40"),
1454            ))
1455        );
1456    }
1457
1458    fn create_order_list() -> OrderList {
1459        OrderList::new(
1460            OrderListId::from("OL-001"),
1461            InstrumentId::from("AUD/USD.SIM"),
1462            StrategyId::from("S-001"),
1463            vec![ClientOrderId::from("O-001")],
1464            UnixNanos::from(42_u64),
1465        )
1466    }
1467
1468    #[rstest]
1469    fn test_order_list_queries_preserve_concrete_type() {
1470        let order_list = create_order_list();
1471        let order_list_id = order_list.id;
1472        let cache = Rc::new(RefCell::new(Cache::default()));
1473        cache
1474            .borrow_mut()
1475            .add_order_list(order_list.clone())
1476            .unwrap();
1477        let py_cache = PyCache::from_rc(cache);
1478
1479        assert_eq!(
1480            py_cache.py_order_list(order_list_id),
1481            Some(order_list.clone()),
1482        );
1483        assert_eq!(
1484            py_cache.py_order_lists(None, None, None, None),
1485            vec![order_list],
1486        );
1487    }
1488
1489    #[rstest]
1490    fn test_add_instrument_close_replaces_existing() {
1491        let first = stub_instrument_close();
1492        let replacement = InstrumentClose::new(
1493            first.instrument_id,
1494            Price::from("0.00000"),
1495            InstrumentCloseType::EndOfSession,
1496            UnixNanos::from(3_u64),
1497            UnixNanos::from(4_u64),
1498        );
1499        let mut py_cache = PyCache::from_rc(Rc::new(RefCell::new(Cache::default())));
1500
1501        py_cache.py_add_instrument_close(first).unwrap();
1502        py_cache.py_add_instrument_close(replacement).unwrap();
1503
1504        assert_eq!(
1505            py_cache.py_instrument_close(first.instrument_id),
1506            Some(replacement)
1507        );
1508    }
1509
1510    #[rstest]
1511    fn test_py_cache_constructor_returns_value_error_for_invalid_config() {
1512        Python::initialize();
1513        let config = CacheConfig {
1514            tick_capacity: 0,
1515            ..Default::default()
1516        };
1517
1518        let err = PyCache::py_new(Some(config)).expect_err("invalid capacity must be rejected");
1519
1520        Python::attach(|py| assert!(err.is_instance_of::<PyValueError>(py)));
1521    }
1522
1523    #[rstest]
1524    fn test_native_cache_binding_returns_value_error_for_invalid_config() {
1525        Python::initialize();
1526        let config = CacheConfig {
1527            bar_capacity: 0,
1528            ..Default::default()
1529        };
1530
1531        let err = Cache::py_new(Some(config)).expect_err("invalid capacity must be rejected");
1532
1533        Python::attach(|py| assert!(err.is_instance_of::<PyValueError>(py)));
1534    }
1535}
1536
1537#[cfg(feature = "defi")]
1538#[pymethods]
1539#[pyo3_stub_gen::derive::gen_stub_pymethods]
1540impl PyCache {
1541    #[pyo3(name = "pool")]
1542    fn py_pool(&self, instrument_id: InstrumentId) -> Option<Pool> {
1543        self.0
1544            .try_borrow()
1545            .ok()
1546            .and_then(|cache| cache.pool(&instrument_id).cloned())
1547    }
1548
1549    #[pyo3(name = "pool_profiler")]
1550    fn py_pool_profiler(&self, instrument_id: InstrumentId) -> Option<PoolProfiler> {
1551        self.0
1552            .try_borrow()
1553            .ok()
1554            .and_then(|cache| cache.pool_profiler(&instrument_id).cloned())
1555    }
1556}
1557
1558#[pymethods]
1559#[pyo3_stub_gen::derive::gen_stub_pymethods]
1560impl CacheConfig {
1561    /// Configuration for `Cache` instances.
1562    #[new]
1563    #[expect(clippy::too_many_arguments)]
1564    #[pyo3(signature = (
1565        encoding=None,
1566        timestamps_as_iso8601=None,
1567        buffer_interval_ms=None,
1568        bulk_read_batch_size=None,
1569        use_trader_prefix=None,
1570        use_instance_id=None,
1571        flush_on_start=None,
1572        drop_instruments_on_reset=None,
1573        tick_capacity=None,
1574        bar_capacity=None,
1575        save_market_data=None,
1576        persist_account_events=None,
1577    ))]
1578    fn py_new(
1579        encoding: Option<SerializationEncoding>,
1580        timestamps_as_iso8601: Option<bool>,
1581        buffer_interval_ms: Option<usize>,
1582        bulk_read_batch_size: Option<usize>,
1583        use_trader_prefix: Option<bool>,
1584        use_instance_id: Option<bool>,
1585        flush_on_start: Option<bool>,
1586        drop_instruments_on_reset: Option<bool>,
1587        tick_capacity: Option<usize>,
1588        bar_capacity: Option<usize>,
1589        save_market_data: Option<bool>,
1590        persist_account_events: Option<bool>,
1591    ) -> PyResult<Self> {
1592        let config = Self {
1593            encoding: encoding.unwrap_or_default(),
1594            timestamps_as_iso8601: timestamps_as_iso8601.unwrap_or(false),
1595            buffer_interval_ms,
1596            bulk_read_batch_size,
1597            use_trader_prefix: use_trader_prefix.unwrap_or(true),
1598            use_instance_id: use_instance_id.unwrap_or(false),
1599            flush_on_start: flush_on_start.unwrap_or(false),
1600            drop_instruments_on_reset: drop_instruments_on_reset.unwrap_or(true),
1601            tick_capacity: tick_capacity.unwrap_or(10_000),
1602            bar_capacity: bar_capacity.unwrap_or(10_000),
1603            persist_account_events: persist_account_events.unwrap_or(true),
1604            save_market_data: save_market_data.unwrap_or(false),
1605        };
1606        config.validate().map_err(config_error_to_pyvalue_err)?;
1607        Ok(config)
1608    }
1609
1610    fn __str__(&self) -> String {
1611        format!("{self:?}")
1612    }
1613
1614    fn __repr__(&self) -> String {
1615        format!("{self:?}")
1616    }
1617
1618    #[getter]
1619    fn encoding(&self) -> SerializationEncoding {
1620        self.encoding
1621    }
1622
1623    #[getter]
1624    fn timestamps_as_iso8601(&self) -> bool {
1625        self.timestamps_as_iso8601
1626    }
1627
1628    #[getter]
1629    fn buffer_interval_ms(&self) -> Option<usize> {
1630        self.buffer_interval_ms
1631    }
1632
1633    #[getter]
1634    fn bulk_read_batch_size(&self) -> Option<usize> {
1635        self.bulk_read_batch_size
1636    }
1637
1638    #[getter]
1639    fn use_trader_prefix(&self) -> bool {
1640        self.use_trader_prefix
1641    }
1642
1643    #[getter]
1644    fn use_instance_id(&self) -> bool {
1645        self.use_instance_id
1646    }
1647
1648    #[getter]
1649    fn flush_on_start(&self) -> bool {
1650        self.flush_on_start
1651    }
1652
1653    #[getter]
1654    fn drop_instruments_on_reset(&self) -> bool {
1655        self.drop_instruments_on_reset
1656    }
1657
1658    #[getter]
1659    fn tick_capacity(&self) -> usize {
1660        self.tick_capacity
1661    }
1662
1663    #[getter]
1664    fn bar_capacity(&self) -> usize {
1665        self.bar_capacity
1666    }
1667
1668    #[getter]
1669    fn persist_account_events(&self) -> bool {
1670        self.persist_account_events
1671    }
1672
1673    #[getter]
1674    fn save_market_data(&self) -> bool {
1675        self.save_market_data
1676    }
1677}
1678
1679#[pymethods]
1680impl Cache {
1681    /// A common in-memory `Cache` for market and execution related data.
1682    #[new]
1683    fn py_new(config: Option<CacheConfig>) -> PyResult<Self> {
1684        Self::try_new(config, None).map_err(config_error_to_pyvalue_err)
1685    }
1686
1687    fn __repr__(&self) -> String {
1688        format!("{self:?}")
1689    }
1690
1691    /// Resets the cache.
1692    ///
1693    /// All stateful fields are reset to their initial value. Instruments,
1694    /// currencies, and synthetics are retained when `drop_instruments_on_reset`
1695    /// is `false` so that repeated backtest runs can reuse the same dataset. External order claims
1696    /// are retained so registered strategy routing remains configured across resets.
1697    #[pyo3(name = "reset")]
1698    fn py_reset(&mut self) {
1699        self.reset();
1700    }
1701
1702    /// Dispose of the cache which will close any underlying database adapter.
1703    ///
1704    /// If closing the database connection fails, an error is logged.
1705    #[pyo3(name = "dispose")]
1706    fn py_dispose(&mut self) {
1707        self.dispose();
1708    }
1709
1710    /// Purges all closed orders from the cache that are older than `buffer_secs`.
1711    ///
1712    ///
1713    /// Only orders that have been closed for at least this amount of time will be purged.
1714    /// A value of 0 means purge all closed orders regardless of when they were closed.
1715    #[pyo3(name = "purge_closed_orders", signature = (ts_now, buffer_secs=0))]
1716    fn py_purge_closed_orders(&mut self, ts_now: u64, buffer_secs: u64) {
1717        self.purge_closed_orders(ts_now.into(), buffer_secs);
1718    }
1719
1720    /// Purges all closed positions from the cache that are older than `buffer_secs`.
1721    #[pyo3(name = "purge_closed_positions", signature = (ts_now, buffer_secs=0))]
1722    fn py_purge_closed_positions(&mut self, ts_now: u64, buffer_secs: u64) {
1723        self.purge_closed_positions(ts_now.into(), buffer_secs);
1724    }
1725
1726    /// Purges the order with the `client_order_id` from the cache (if found).
1727    ///
1728    /// For safety, an order is prevented from being purged if it's open.
1729    #[pyo3(name = "purge_order")]
1730    fn py_purge_order(&mut self, client_order_id: ClientOrderId) {
1731        self.purge_order(client_order_id);
1732    }
1733
1734    /// Purges the position with the `position_id` from the cache (if found).
1735    ///
1736    /// For safety, a position is prevented from being purged if it's open.
1737    #[pyo3(name = "purge_position")]
1738    fn py_purge_position(&mut self, position_id: PositionId) {
1739        self.purge_position(position_id);
1740    }
1741
1742    /// Purges the instrument with the `instrument_id` from the cache.
1743    ///
1744    /// This refuses to purge when associated orders or positions remain in
1745    /// non-terminal state.
1746    #[pyo3(name = "purge_instrument")]
1747    fn py_purge_instrument(&mut self, instrument_id: InstrumentId) {
1748        self.purge_instrument(instrument_id);
1749    }
1750
1751    /// Purges all account state events which are outside the lookback window.
1752    ///
1753    /// Only events which are outside the lookback window will be purged.
1754    /// A value of 0 means purge all account state events.
1755    #[pyo3(name = "purge_account_events", signature = (ts_now, lookback_secs=0))]
1756    fn py_purge_account_events(&mut self, ts_now: u64, lookback_secs: u64) {
1757        self.purge_account_events(ts_now.into(), lookback_secs);
1758    }
1759
1760    /// Adds the `currency` to the cache.
1761    ///
1762    /// # Errors
1763    ///
1764    /// Returns an error if persisting the currency to the backing database fails.
1765    #[pyo3(name = "add_currency")]
1766    fn py_add_currency(&mut self, currency: Currency) -> PyResult<()> {
1767        self.add_currency(currency).map_err(to_pyvalue_err)
1768    }
1769
1770    /// Adds the `instrument` to the cache.
1771    ///
1772    /// # Errors
1773    ///
1774    /// Returns an error if persisting the instrument to the backing database fails.
1775    #[pyo3(name = "add_instrument")]
1776    fn py_add_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
1777        let instrument_any = pyobject_to_instrument_any(py, instrument)?;
1778        self.add_instrument(instrument_any).map_err(to_pyvalue_err)
1779    }
1780
1781    /// Returns a reference to the instrument for the `instrument_id` (if found).
1782    #[pyo3(name = "instrument")]
1783    fn py_instrument(
1784        &self,
1785        py: Python,
1786        instrument_id: InstrumentId,
1787    ) -> PyResult<Option<Py<PyAny>>> {
1788        match self.instrument(&instrument_id) {
1789            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument.clone())?)),
1790            None => Ok(None),
1791        }
1792    }
1793
1794    /// Returns references to all instrument IDs for the `venue`.
1795    #[pyo3(name = "instrument_ids")]
1796    fn py_instrument_ids(&self, venue: Option<Venue>) -> Vec<InstrumentId> {
1797        self.instrument_ids(venue.as_ref())
1798            .into_iter()
1799            .copied()
1800            .collect()
1801    }
1802
1803    /// Returns references to all instruments for the `venue`.
1804    #[pyo3(name = "instruments")]
1805    fn py_instruments(&self, py: Python, venue: Option<Venue>) -> PyResult<Vec<Py<PyAny>>> {
1806        let mut py_instruments = Vec::new();
1807
1808        if let Some(venue) = venue {
1809            let instruments = self.instruments(&venue, None);
1810            for instrument in instruments {
1811                py_instruments.push(instrument_any_to_pyobject(py, (*instrument).clone())?);
1812            }
1813        } else {
1814            let instrument_ids = self.instrument_ids(None);
1815            for instrument_id in instrument_ids {
1816                if let Some(instrument) = self.instrument(instrument_id) {
1817                    py_instruments.push(instrument_any_to_pyobject(py, instrument.clone())?);
1818                }
1819            }
1820        }
1821
1822        Ok(py_instruments)
1823    }
1824
1825    /// Adds the `order` to the cache indexed with any given identifiers.
1826    ///
1827    /// # Parameters
1828    ///
1829    /// `override_existing`: If the added order should 'override' any existing order and replace
1830    /// it in the cache. This is currently used for emulated orders which are
1831    /// being released and transformed into another type.
1832    ///
1833    /// # Errors
1834    ///
1835    /// Returns an error if not `replace_existing` and the `order.client_order_id` is already contained in the cache,
1836    /// or if persisting the order to the backing database fails. The order and every index are
1837    /// committed to memory before persistence is attempted, so a persistence error leaves the
1838    /// cache internally consistent.
1839    #[pyo3(name = "add_order")]
1840    fn py_add_order(
1841        &mut self,
1842        py: Python,
1843        order: Py<PyAny>,
1844        position_id: Option<PositionId>,
1845        client_id: Option<ClientId>,
1846        replace_existing: Option<bool>,
1847    ) -> PyResult<()> {
1848        let order_any = pyobject_to_order_any(py, order)?;
1849        self.add_order(
1850            order_any,
1851            position_id,
1852            client_id,
1853            replace_existing.unwrap_or(false),
1854        )
1855        .map_err(to_pyvalue_err)
1856    }
1857
1858    /// Gets a borrow of the order with the `client_order_id` (if found).
1859    ///
1860    /// Prefer `Self.order_ref` in new native code.
1861    #[pyo3(name = "order")]
1862    fn py_order(&self, py: Python, client_order_id: ClientOrderId) -> PyResult<Option<Py<PyAny>>> {
1863        match self.order(&client_order_id) {
1864            Some(order) => Ok(Some(order_any_to_pyobject(py, order.clone())?)),
1865            None => Ok(None),
1866        }
1867    }
1868
1869    /// Returns whether an order with the `client_order_id` exists.
1870    #[pyo3(name = "order_exists")]
1871    fn py_order_exists(&self, client_order_id: ClientOrderId) -> bool {
1872        self.order_exists(&client_order_id)
1873    }
1874
1875    /// Returns whether an order with the `client_order_id` is open.
1876    #[pyo3(name = "is_order_open")]
1877    fn py_is_order_open(&self, client_order_id: ClientOrderId) -> bool {
1878        self.is_order_open(&client_order_id)
1879    }
1880
1881    /// Returns whether an order with the `client_order_id` is closed.
1882    #[pyo3(name = "is_order_closed")]
1883    fn py_is_order_closed(&self, client_order_id: ClientOrderId) -> bool {
1884        self.is_order_closed(&client_order_id)
1885    }
1886
1887    /// Returns whether an order with the `client_order_id` is locally active.
1888    ///
1889    /// Locally active orders are in the `INITIALIZED`, `EMULATED`, or `RELEASED` state
1890    /// (a superset of emulated orders).
1891    #[pyo3(name = "is_order_active_local")]
1892    fn py_is_order_active_local(&self, client_order_id: ClientOrderId) -> bool {
1893        self.is_order_active_local(&client_order_id)
1894    }
1895
1896    /// Returns borrows of all locally active orders matching the optional filter parameters.
1897    ///
1898    /// Prefer `Self.orders_active_local_refs` in new native code.
1899    #[pyo3(name = "orders_active_local")]
1900    fn py_orders_active_local(
1901        &self,
1902        py: Python,
1903        venue: Option<Venue>,
1904        instrument_id: Option<InstrumentId>,
1905        strategy_id: Option<StrategyId>,
1906        account_id: Option<AccountId>,
1907        side: Option<OrderSide>,
1908    ) -> PyResult<Vec<Py<PyAny>>> {
1909        self.orders_active_local(
1910            venue.as_ref(),
1911            instrument_id.as_ref(),
1912            strategy_id.as_ref(),
1913            account_id.as_ref(),
1914            side,
1915        )
1916        .into_iter()
1917        .map(|order| order_any_to_pyobject(py, order.clone()))
1918        .collect()
1919    }
1920
1921    /// Returns the count of all locally active orders.
1922    ///
1923    /// Locally active orders are in the `INITIALIZED`, `EMULATED`, or `RELEASED` state
1924    /// (a superset of emulated orders).
1925    #[pyo3(name = "orders_active_local_count")]
1926    fn py_orders_active_local_count(
1927        &self,
1928        venue: Option<Venue>,
1929        instrument_id: Option<InstrumentId>,
1930        strategy_id: Option<StrategyId>,
1931        account_id: Option<AccountId>,
1932        side: Option<OrderSide>,
1933    ) -> usize {
1934        self.orders_active_local_count(
1935            venue.as_ref(),
1936            instrument_id.as_ref(),
1937            strategy_id.as_ref(),
1938            account_id.as_ref(),
1939            side,
1940        )
1941    }
1942
1943    /// Returns the count of all open orders.
1944    #[pyo3(name = "orders_open_count")]
1945    fn py_orders_open_count(
1946        &self,
1947        venue: Option<Venue>,
1948        instrument_id: Option<InstrumentId>,
1949        strategy_id: Option<StrategyId>,
1950        account_id: Option<AccountId>,
1951        side: Option<OrderSide>,
1952    ) -> usize {
1953        self.orders_open_count(
1954            venue.as_ref(),
1955            instrument_id.as_ref(),
1956            strategy_id.as_ref(),
1957            account_id.as_ref(),
1958            side,
1959        )
1960    }
1961
1962    /// Returns the count of all closed orders.
1963    #[pyo3(name = "orders_closed_count")]
1964    fn py_orders_closed_count(
1965        &self,
1966        venue: Option<Venue>,
1967        instrument_id: Option<InstrumentId>,
1968        strategy_id: Option<StrategyId>,
1969        account_id: Option<AccountId>,
1970        side: Option<OrderSide>,
1971    ) -> usize {
1972        self.orders_closed_count(
1973            venue.as_ref(),
1974            instrument_id.as_ref(),
1975            strategy_id.as_ref(),
1976            account_id.as_ref(),
1977            side,
1978        )
1979    }
1980
1981    /// Returns the count of all orders.
1982    #[pyo3(name = "orders_total_count")]
1983    fn py_orders_total_count(
1984        &self,
1985        venue: Option<Venue>,
1986        instrument_id: Option<InstrumentId>,
1987        strategy_id: Option<StrategyId>,
1988        account_id: Option<AccountId>,
1989        side: Option<OrderSide>,
1990    ) -> usize {
1991        self.orders_total_count(
1992            venue.as_ref(),
1993            instrument_id.as_ref(),
1994            strategy_id.as_ref(),
1995            account_id.as_ref(),
1996            side,
1997        )
1998    }
1999
2000    /// Adds the `position` to the cache.
2001    ///
2002    /// # Errors
2003    ///
2004    /// Returns an error if persisting the position to the backing database fails. After
2005    /// serialization succeeds, the complete operation is committed to memory before persistence
2006    /// is attempted, so a persistence error leaves the cache internally consistent.
2007    #[pyo3(name = "add_position")]
2008    #[expect(clippy::needless_pass_by_value)]
2009    fn py_add_position(
2010        &mut self,
2011        py: Python,
2012        position: Py<PyAny>,
2013        oms_type: OmsType,
2014    ) -> PyResult<()> {
2015        let position_obj = position.extract::<Position>(py)?;
2016        self.add_position(&position_obj, oms_type)
2017            .map_err(to_pyvalue_err)
2018    }
2019
2020    /// Creates a snapshot of the `position` by cloning it, assigning a new ID, and storing it
2021    /// in the position snapshots.
2022    ///
2023    /// The copy excludes `replay_events` and `fill_voids`, which no snapshot consumer reads,
2024    /// so snapshot size stays independent of the fills applied to the position ID. The copy
2025    /// encodes only when a consumer asks for the bytes, so this call stays off the encode path
2026    /// unless a backing database has to persist the frame.
2027    ///
2028    /// # Errors
2029    ///
2030    /// Returns an error if serializing or storing the position snapshot fails.
2031    #[pyo3(name = "snapshot_position")]
2032    #[expect(clippy::needless_pass_by_value)]
2033    fn py_snapshot_position(&mut self, py: Python, position: Py<PyAny>) -> PyResult<()> {
2034        let position_obj = position.extract::<Position>(py)?;
2035        self.snapshot_position(&position_obj)
2036            .map_err(to_pyvalue_err)
2037    }
2038
2039    /// Returns a borrow of the position with the `position_id` (if found).
2040    ///
2041    /// Prefer `Self.position_ref` in new native code.
2042    #[pyo3(name = "position")]
2043    fn py_position(&self, py: Python, position_id: PositionId) -> PyResult<Option<Py<PyAny>>> {
2044        match self.position(&position_id) {
2045            Some(position) => Ok(Some(position.clone().into_pyobject(py)?.into())),
2046            None => Ok(None),
2047        }
2048    }
2049
2050    /// Returns whether a position with the `position_id` exists.
2051    #[pyo3(name = "position_exists")]
2052    fn py_position_exists(&self, position_id: PositionId) -> bool {
2053        self.position_exists(&position_id)
2054    }
2055
2056    /// Returns whether a position with the `position_id` is open.
2057    #[pyo3(name = "is_position_open")]
2058    fn py_is_position_open(&self, position_id: PositionId) -> bool {
2059        self.is_position_open(&position_id)
2060    }
2061
2062    /// Returns whether a position with the `position_id` is closed.
2063    #[pyo3(name = "is_position_closed")]
2064    fn py_is_position_closed(&self, position_id: PositionId) -> bool {
2065        self.is_position_closed(&position_id)
2066    }
2067
2068    /// Returns the count of all open positions.
2069    #[pyo3(name = "positions_open_count")]
2070    fn py_positions_open_count(
2071        &self,
2072        venue: Option<Venue>,
2073        instrument_id: Option<InstrumentId>,
2074        strategy_id: Option<StrategyId>,
2075        account_id: Option<AccountId>,
2076        side: Option<PositionSide>,
2077    ) -> usize {
2078        self.positions_open_count(
2079            venue.as_ref(),
2080            instrument_id.as_ref(),
2081            strategy_id.as_ref(),
2082            account_id.as_ref(),
2083            side,
2084        )
2085    }
2086
2087    /// Returns the count of all closed positions.
2088    #[pyo3(name = "positions_closed_count")]
2089    fn py_positions_closed_count(
2090        &self,
2091        venue: Option<Venue>,
2092        instrument_id: Option<InstrumentId>,
2093        strategy_id: Option<StrategyId>,
2094        account_id: Option<AccountId>,
2095        side: Option<PositionSide>,
2096    ) -> usize {
2097        self.positions_closed_count(
2098            venue.as_ref(),
2099            instrument_id.as_ref(),
2100            strategy_id.as_ref(),
2101            account_id.as_ref(),
2102            side,
2103        )
2104    }
2105
2106    /// Returns the count of all positions.
2107    #[pyo3(name = "positions_total_count")]
2108    fn py_positions_total_count(
2109        &self,
2110        venue: Option<Venue>,
2111        instrument_id: Option<InstrumentId>,
2112        strategy_id: Option<StrategyId>,
2113        account_id: Option<AccountId>,
2114        side: Option<PositionSide>,
2115    ) -> usize {
2116        self.positions_total_count(
2117            venue.as_ref(),
2118            instrument_id.as_ref(),
2119            strategy_id.as_ref(),
2120            account_id.as_ref(),
2121            side,
2122        )
2123    }
2124
2125    /// Adds the `quote` tick to the cache.
2126    ///
2127    /// # Errors
2128    ///
2129    /// Returns an error if persisting the quote tick to the backing database fails.
2130    #[pyo3(name = "add_quote")]
2131    fn py_add_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
2132        self.add_quote(quote).map_err(to_pyvalue_err)
2133    }
2134
2135    /// Adds the `trade` tick to the cache.
2136    ///
2137    /// # Errors
2138    ///
2139    /// Returns an error if persisting the trade tick to the backing database fails.
2140    #[pyo3(name = "add_trade")]
2141    fn py_add_trade(&mut self, trade: TradeTick) -> PyResult<()> {
2142        self.add_trade(trade).map_err(to_pyvalue_err)
2143    }
2144
2145    /// Adds the `bar` to the cache.
2146    ///
2147    /// # Errors
2148    ///
2149    /// Returns an error if persisting the bar to the backing database fails.
2150    #[pyo3(name = "add_bar")]
2151    fn py_add_bar(&mut self, bar: Bar) -> PyResult<()> {
2152        self.add_bar(bar).map_err(to_pyvalue_err)
2153    }
2154
2155    /// Gets a reference to the latest quote for the `instrument_id`.
2156    #[pyo3(name = "quote")]
2157    fn py_quote(&self, instrument_id: InstrumentId) -> Option<QuoteTick> {
2158        self.quote(&instrument_id).copied()
2159    }
2160
2161    /// Gets a reference to the latest trade for the `instrument_id`.
2162    #[pyo3(name = "trade")]
2163    fn py_trade(&self, instrument_id: InstrumentId) -> Option<TradeTick> {
2164        self.trade(&instrument_id).copied()
2165    }
2166
2167    /// Gets a reference to the latest bar for the `bar_type`.
2168    #[pyo3(name = "bar")]
2169    fn py_bar(&self, bar_type: BarType) -> Option<Bar> {
2170        self.bar(&bar_type).copied()
2171    }
2172
2173    /// Gets all quotes for the `instrument_id`.
2174    #[pyo3(name = "quotes")]
2175    fn py_quotes(&self, instrument_id: InstrumentId) -> Option<Vec<QuoteTick>> {
2176        self.quotes(&instrument_id)
2177    }
2178
2179    /// Gets all trades for the `instrument_id`.
2180    #[pyo3(name = "trades")]
2181    fn py_trades(&self, instrument_id: InstrumentId) -> Option<Vec<TradeTick>> {
2182        self.trades(&instrument_id)
2183    }
2184
2185    /// Gets all bars for the `bar_type`.
2186    #[pyo3(name = "bars")]
2187    fn py_bars(&self, bar_type: BarType) -> Option<Vec<Bar>> {
2188        self.bars(&bar_type)
2189    }
2190
2191    /// Returns whether the cache contains quotes for the `instrument_id`.
2192    #[pyo3(name = "has_quote_ticks")]
2193    fn py_has_quote_ticks(&self, instrument_id: InstrumentId) -> bool {
2194        self.has_quote_ticks(&instrument_id)
2195    }
2196
2197    /// Returns whether the cache contains trades for the `instrument_id`.
2198    #[pyo3(name = "has_trade_ticks")]
2199    fn py_has_trade_ticks(&self, instrument_id: InstrumentId) -> bool {
2200        self.has_trade_ticks(&instrument_id)
2201    }
2202
2203    /// Returns whether the cache contains mark price updates for the `instrument_id`.
2204    #[pyo3(name = "has_mark_prices")]
2205    fn py_has_mark_prices(&self, instrument_id: InstrumentId) -> bool {
2206        self.has_mark_prices(&instrument_id)
2207    }
2208
2209    /// Returns whether the cache contains index price updates for the `instrument_id`.
2210    #[pyo3(name = "has_index_prices")]
2211    fn py_has_index_prices(&self, instrument_id: InstrumentId) -> bool {
2212        self.has_index_prices(&instrument_id)
2213    }
2214
2215    /// Returns whether the cache contains funding rate updates for the `instrument_id`.
2216    #[pyo3(name = "has_funding_rates")]
2217    fn py_has_funding_rates(&self, instrument_id: InstrumentId) -> bool {
2218        self.has_funding_rates(&instrument_id)
2219    }
2220
2221    /// Returns whether the cache contains instrument status updates for the `instrument_id`.
2222    #[pyo3(name = "has_instrument_statuses")]
2223    fn py_has_instrument_statuses(&self, instrument_id: InstrumentId) -> bool {
2224        self.has_instrument_statuses(&instrument_id)
2225    }
2226
2227    /// Returns whether the cache contains a close for the `instrument_id`.
2228    #[pyo3(name = "has_instrument_close")]
2229    fn py_has_instrument_close(&self, instrument_id: InstrumentId) -> bool {
2230        self.has_instrument_close(&instrument_id)
2231    }
2232
2233    /// Returns whether the cache contains bars for the `bar_type`.
2234    #[pyo3(name = "has_bars")]
2235    fn py_has_bars(&self, bar_type: BarType) -> bool {
2236        self.has_bars(&bar_type)
2237    }
2238
2239    /// Gets the quote tick count for the `instrument_id`.
2240    #[pyo3(name = "quote_count")]
2241    fn py_quote_count(&self, instrument_id: InstrumentId) -> usize {
2242        self.quote_count(&instrument_id)
2243    }
2244
2245    /// Gets the trade tick count for the `instrument_id`.
2246    #[pyo3(name = "trade_count")]
2247    fn py_trade_count(&self, instrument_id: InstrumentId) -> usize {
2248        self.trade_count(&instrument_id)
2249    }
2250
2251    /// Gets the mark price update count for the `instrument_id`.
2252    #[pyo3(name = "mark_price_count")]
2253    fn py_mark_price_count(&self, instrument_id: InstrumentId) -> usize {
2254        self.mark_price_count(&instrument_id)
2255    }
2256
2257    /// Gets the index price update count for the `instrument_id`.
2258    #[pyo3(name = "index_price_count")]
2259    fn py_index_price_count(&self, instrument_id: InstrumentId) -> usize {
2260        self.index_price_count(&instrument_id)
2261    }
2262
2263    /// Gets the funding rate update count for the `instrument_id`.
2264    #[pyo3(name = "funding_rate_count")]
2265    fn py_funding_rate_count(&self, instrument_id: InstrumentId) -> usize {
2266        self.funding_rate_count(&instrument_id)
2267    }
2268
2269    /// Gets the instrument status update count for the `instrument_id`.
2270    #[pyo3(name = "instrument_status_count")]
2271    fn py_instrument_status_count(&self, instrument_id: InstrumentId) -> usize {
2272        self.instrument_status_count(&instrument_id)
2273    }
2274
2275    /// Gets the bar count for the `instrument_id`.
2276    #[pyo3(name = "bar_count")]
2277    fn py_bar_count(&self, bar_type: BarType) -> usize {
2278        self.bar_count(&bar_type)
2279    }
2280
2281    /// Gets a reference to the latest mark price update for the `instrument_id`.
2282    #[pyo3(name = "mark_price")]
2283    fn py_mark_price(&self, instrument_id: InstrumentId) -> Option<MarkPriceUpdate> {
2284        self.mark_price(&instrument_id).copied()
2285    }
2286
2287    /// Gets all mark price updates for the `instrument_id`.
2288    #[pyo3(name = "mark_prices")]
2289    fn py_mark_prices(&self, instrument_id: InstrumentId) -> Option<Vec<MarkPriceUpdate>> {
2290        self.mark_prices(&instrument_id)
2291    }
2292
2293    /// Gets a reference to the latest index price update for the `instrument_id`.
2294    #[pyo3(name = "index_price")]
2295    fn py_index_price(&self, instrument_id: InstrumentId) -> Option<IndexPriceUpdate> {
2296        self.index_price(&instrument_id).copied()
2297    }
2298
2299    /// Gets all index price updates for the `instrument_id`.
2300    #[pyo3(name = "index_prices")]
2301    fn py_index_prices(&self, instrument_id: InstrumentId) -> Option<Vec<IndexPriceUpdate>> {
2302        self.index_prices(&instrument_id)
2303    }
2304
2305    /// Gets a reference to the latest funding rate update for the `instrument_id`.
2306    #[pyo3(name = "funding_rate")]
2307    fn py_funding_rate(&self, instrument_id: InstrumentId) -> Option<FundingRateUpdate> {
2308        self.funding_rate(&instrument_id).copied()
2309    }
2310
2311    /// Gets all funding rate updates for the `instrument_id`.
2312    #[pyo3(name = "funding_rates")]
2313    fn py_funding_rates(&self, instrument_id: InstrumentId) -> Option<Vec<FundingRateUpdate>> {
2314        self.funding_rates(&instrument_id)
2315    }
2316
2317    /// Gets a reference to the latest instrument status update for the `instrument_id`.
2318    #[pyo3(name = "instrument_status")]
2319    fn py_instrument_status(&self, instrument_id: InstrumentId) -> Option<InstrumentStatus> {
2320        self.instrument_status(&instrument_id).copied()
2321    }
2322
2323    /// Gets all instrument status updates for the `instrument_id`.
2324    #[pyo3(name = "instrument_statuses")]
2325    fn py_instrument_statuses(&self, instrument_id: InstrumentId) -> Option<Vec<InstrumentStatus>> {
2326        self.instrument_statuses(&instrument_id)
2327    }
2328
2329    /// Returns the close cached for `instrument_id`, if present.
2330    #[pyo3(name = "instrument_close")]
2331    fn py_instrument_close(&self, instrument_id: InstrumentId) -> Option<InstrumentClose> {
2332        self.instrument_close(&instrument_id).copied()
2333    }
2334
2335    /// Gets a reference to the order book for the `instrument_id`.
2336    #[pyo3(name = "order_book")]
2337    fn py_order_book(&self, instrument_id: InstrumentId) -> Option<OrderBook> {
2338        self.order_book(&instrument_id).cloned()
2339    }
2340
2341    /// Returns whether the cache contains an order book for the `instrument_id`.
2342    #[pyo3(name = "has_order_book")]
2343    fn py_has_order_book(&self, instrument_id: InstrumentId) -> bool {
2344        self.has_order_book(&instrument_id)
2345    }
2346
2347    /// Gets the order book update count for the `instrument_id`.
2348    #[pyo3(name = "book_update_count")]
2349    fn py_book_update_count(&self, instrument_id: InstrumentId) -> usize {
2350        self.book_update_count(&instrument_id)
2351    }
2352
2353    /// Returns a reference to the synthetic instrument for the `instrument_id` (if found).
2354    #[pyo3(name = "synthetic")]
2355    fn py_synthetic(&self, instrument_id: InstrumentId) -> Option<SyntheticInstrument> {
2356        self.synthetic(&instrument_id).cloned()
2357    }
2358
2359    /// Returns references to instrument IDs for all synthetic instruments contained in the cache.
2360    #[pyo3(name = "synthetic_ids")]
2361    fn py_synthetic_ids(&self) -> Vec<InstrumentId> {
2362        self.synthetic_ids().into_iter().copied().collect()
2363    }
2364
2365    /// Returns the `ClientOrderId`s of all orders.
2366    #[pyo3(name = "client_order_ids")]
2367    fn py_client_order_ids(
2368        &self,
2369        venue: Option<Venue>,
2370        instrument_id: Option<InstrumentId>,
2371        strategy_id: Option<StrategyId>,
2372        account_id: Option<AccountId>,
2373    ) -> Vec<ClientOrderId> {
2374        self.client_order_ids(
2375            venue.as_ref(),
2376            instrument_id.as_ref(),
2377            strategy_id.as_ref(),
2378            account_id.as_ref(),
2379        )
2380        .into_iter()
2381        .collect()
2382    }
2383
2384    /// Returns the `ClientOrderId`s of all open orders.
2385    #[pyo3(name = "client_order_ids_open")]
2386    fn py_client_order_ids_open(
2387        &self,
2388        venue: Option<Venue>,
2389        instrument_id: Option<InstrumentId>,
2390        strategy_id: Option<StrategyId>,
2391        account_id: Option<AccountId>,
2392    ) -> Vec<ClientOrderId> {
2393        self.client_order_ids_open(
2394            venue.as_ref(),
2395            instrument_id.as_ref(),
2396            strategy_id.as_ref(),
2397            account_id.as_ref(),
2398        )
2399        .into_iter()
2400        .collect()
2401    }
2402
2403    /// Returns the `ClientOrderId`s of all closed orders.
2404    #[pyo3(name = "client_order_ids_closed")]
2405    fn py_client_order_ids_closed(
2406        &self,
2407        venue: Option<Venue>,
2408        instrument_id: Option<InstrumentId>,
2409        strategy_id: Option<StrategyId>,
2410        account_id: Option<AccountId>,
2411    ) -> Vec<ClientOrderId> {
2412        self.client_order_ids_closed(
2413            venue.as_ref(),
2414            instrument_id.as_ref(),
2415            strategy_id.as_ref(),
2416            account_id.as_ref(),
2417        )
2418        .into_iter()
2419        .collect()
2420    }
2421
2422    /// Returns the `ClientOrderId`s of all emulated orders.
2423    #[pyo3(name = "client_order_ids_emulated")]
2424    fn py_client_order_ids_emulated(
2425        &self,
2426        venue: Option<Venue>,
2427        instrument_id: Option<InstrumentId>,
2428        strategy_id: Option<StrategyId>,
2429        account_id: Option<AccountId>,
2430    ) -> Vec<ClientOrderId> {
2431        self.client_order_ids_emulated(
2432            venue.as_ref(),
2433            instrument_id.as_ref(),
2434            strategy_id.as_ref(),
2435            account_id.as_ref(),
2436        )
2437        .into_iter()
2438        .collect()
2439    }
2440
2441    /// Returns the `ClientOrderId`s of all in-flight orders.
2442    #[pyo3(name = "client_order_ids_inflight")]
2443    fn py_client_order_ids_inflight(
2444        &self,
2445        venue: Option<Venue>,
2446        instrument_id: Option<InstrumentId>,
2447        strategy_id: Option<StrategyId>,
2448        account_id: Option<AccountId>,
2449    ) -> Vec<ClientOrderId> {
2450        self.client_order_ids_inflight(
2451            venue.as_ref(),
2452            instrument_id.as_ref(),
2453            strategy_id.as_ref(),
2454            account_id.as_ref(),
2455        )
2456        .into_iter()
2457        .collect()
2458    }
2459
2460    /// Returns `PositionId`s of all positions.
2461    #[pyo3(name = "position_ids")]
2462    fn py_position_ids(
2463        &self,
2464        venue: Option<Venue>,
2465        instrument_id: Option<InstrumentId>,
2466        strategy_id: Option<StrategyId>,
2467        account_id: Option<AccountId>,
2468    ) -> Vec<PositionId> {
2469        self.position_ids(
2470            venue.as_ref(),
2471            instrument_id.as_ref(),
2472            strategy_id.as_ref(),
2473            account_id.as_ref(),
2474        )
2475        .into_iter()
2476        .collect()
2477    }
2478
2479    /// Returns the `PositionId`s of all open positions.
2480    #[pyo3(name = "position_open_ids")]
2481    fn py_position_open_ids(
2482        &self,
2483        venue: Option<Venue>,
2484        instrument_id: Option<InstrumentId>,
2485        strategy_id: Option<StrategyId>,
2486        account_id: Option<AccountId>,
2487    ) -> Vec<PositionId> {
2488        self.position_open_ids(
2489            venue.as_ref(),
2490            instrument_id.as_ref(),
2491            strategy_id.as_ref(),
2492            account_id.as_ref(),
2493        )
2494        .into_iter()
2495        .collect()
2496    }
2497
2498    /// Returns the `PositionId`s of all closed positions.
2499    #[pyo3(name = "position_closed_ids")]
2500    fn py_position_closed_ids(
2501        &self,
2502        venue: Option<Venue>,
2503        instrument_id: Option<InstrumentId>,
2504        strategy_id: Option<StrategyId>,
2505        account_id: Option<AccountId>,
2506    ) -> Vec<PositionId> {
2507        self.position_closed_ids(
2508            venue.as_ref(),
2509            instrument_id.as_ref(),
2510            strategy_id.as_ref(),
2511            account_id.as_ref(),
2512        )
2513        .into_iter()
2514        .collect()
2515    }
2516
2517    /// Returns the `StrategyId`s of all strategies.
2518    #[pyo3(name = "strategy_ids")]
2519    fn py_strategy_ids(&self) -> Vec<StrategyId> {
2520        self.strategy_ids().into_iter().collect()
2521    }
2522
2523    /// Returns the `ExecAlgorithmId`s of all execution algorithms.
2524    #[pyo3(name = "exec_algorithm_ids")]
2525    fn py_exec_algorithm_ids(&self) -> Vec<ExecAlgorithmId> {
2526        self.exec_algorithm_ids().into_iter().collect()
2527    }
2528
2529    /// Gets a reference to the client order ID for the `venue_order_id` (if found).
2530    #[pyo3(name = "client_order_id")]
2531    fn py_client_order_id(&self, venue_order_id: VenueOrderId) -> Option<ClientOrderId> {
2532        self.client_order_id(&venue_order_id).copied()
2533    }
2534
2535    /// Gets a reference to the venue order ID for the `client_order_id` (if found).
2536    #[pyo3(name = "venue_order_id")]
2537    fn py_venue_order_id(&self, client_order_id: ClientOrderId) -> Option<VenueOrderId> {
2538        self.venue_order_id(&client_order_id).copied()
2539    }
2540
2541    /// Gets a reference to the client ID indexed for then `client_order_id` (if found).
2542    #[pyo3(name = "client_id")]
2543    fn py_client_id(&self, client_order_id: ClientOrderId) -> Option<ClientId> {
2544        self.client_id(&client_order_id).copied()
2545    }
2546
2547    /// Returns borrows of all orders matching the optional filter parameters.
2548    ///
2549    /// Prefer `Self.orders_refs` in new native code.
2550    #[pyo3(name = "orders")]
2551    fn py_orders(
2552        &self,
2553        py: Python,
2554        venue: Option<Venue>,
2555        instrument_id: Option<InstrumentId>,
2556        strategy_id: Option<StrategyId>,
2557        account_id: Option<AccountId>,
2558        side: Option<OrderSide>,
2559    ) -> PyResult<Vec<Py<PyAny>>> {
2560        self.orders(
2561            venue.as_ref(),
2562            instrument_id.as_ref(),
2563            strategy_id.as_ref(),
2564            account_id.as_ref(),
2565            side,
2566        )
2567        .into_iter()
2568        .map(|o| order_any_to_pyobject(py, o.clone()))
2569        .collect()
2570    }
2571
2572    /// Returns borrows of all open orders matching the optional filter parameters.
2573    ///
2574    /// Prefer `Self.orders_open_refs` in new native code.
2575    #[pyo3(name = "orders_open")]
2576    fn py_orders_open(
2577        &self,
2578        py: Python,
2579        venue: Option<Venue>,
2580        instrument_id: Option<InstrumentId>,
2581        strategy_id: Option<StrategyId>,
2582        account_id: Option<AccountId>,
2583        side: Option<OrderSide>,
2584    ) -> PyResult<Vec<Py<PyAny>>> {
2585        self.orders_open(
2586            venue.as_ref(),
2587            instrument_id.as_ref(),
2588            strategy_id.as_ref(),
2589            account_id.as_ref(),
2590            side,
2591        )
2592        .into_iter()
2593        .map(|o| order_any_to_pyobject(py, o.clone()))
2594        .collect()
2595    }
2596
2597    /// Returns borrows of all closed orders matching the optional filter parameters.
2598    ///
2599    /// Prefer `Self.orders_closed_refs` in new native code.
2600    #[pyo3(name = "orders_closed")]
2601    fn py_orders_closed(
2602        &self,
2603        py: Python,
2604        venue: Option<Venue>,
2605        instrument_id: Option<InstrumentId>,
2606        strategy_id: Option<StrategyId>,
2607        account_id: Option<AccountId>,
2608        side: Option<OrderSide>,
2609    ) -> PyResult<Vec<Py<PyAny>>> {
2610        self.orders_closed(
2611            venue.as_ref(),
2612            instrument_id.as_ref(),
2613            strategy_id.as_ref(),
2614            account_id.as_ref(),
2615            side,
2616        )
2617        .into_iter()
2618        .map(|o| order_any_to_pyobject(py, o.clone()))
2619        .collect()
2620    }
2621
2622    /// Returns borrows of all emulated orders matching the optional filter parameters.
2623    ///
2624    /// Prefer `Self.orders_emulated_refs` in new native code.
2625    #[pyo3(name = "orders_emulated")]
2626    fn py_orders_emulated(
2627        &self,
2628        py: Python,
2629        venue: Option<Venue>,
2630        instrument_id: Option<InstrumentId>,
2631        strategy_id: Option<StrategyId>,
2632        account_id: Option<AccountId>,
2633        side: Option<OrderSide>,
2634    ) -> PyResult<Vec<Py<PyAny>>> {
2635        self.orders_emulated(
2636            venue.as_ref(),
2637            instrument_id.as_ref(),
2638            strategy_id.as_ref(),
2639            account_id.as_ref(),
2640            side,
2641        )
2642        .into_iter()
2643        .map(|o| order_any_to_pyobject(py, o.clone()))
2644        .collect()
2645    }
2646
2647    /// Returns borrows of all in-flight orders matching the optional filter parameters.
2648    ///
2649    /// Prefer `Self.orders_inflight_refs` in new native code.
2650    #[pyo3(name = "orders_inflight")]
2651    fn py_orders_inflight(
2652        &self,
2653        py: Python,
2654        venue: Option<Venue>,
2655        instrument_id: Option<InstrumentId>,
2656        strategy_id: Option<StrategyId>,
2657        account_id: Option<AccountId>,
2658        side: Option<OrderSide>,
2659    ) -> PyResult<Vec<Py<PyAny>>> {
2660        self.orders_inflight(
2661            venue.as_ref(),
2662            instrument_id.as_ref(),
2663            strategy_id.as_ref(),
2664            account_id.as_ref(),
2665            side,
2666        )
2667        .into_iter()
2668        .map(|o| order_any_to_pyobject(py, o.clone()))
2669        .collect()
2670    }
2671
2672    /// Returns borrows of all orders for the `position_id`.
2673    #[pyo3(name = "orders_for_position")]
2674    fn py_orders_for_position(
2675        &self,
2676        py: Python,
2677        position_id: PositionId,
2678    ) -> PyResult<Vec<Py<PyAny>>> {
2679        self.orders_for_position(&position_id)
2680            .into_iter()
2681            .map(|o| order_any_to_pyobject(py, o.clone()))
2682            .collect()
2683    }
2684
2685    /// Returns whether an order with the `client_order_id` is emulated.
2686    #[pyo3(name = "is_order_emulated")]
2687    fn py_is_order_emulated(&self, client_order_id: ClientOrderId) -> bool {
2688        self.is_order_emulated(&client_order_id)
2689    }
2690
2691    /// Returns whether an order with the `client_order_id` is in-flight.
2692    #[pyo3(name = "is_order_inflight")]
2693    fn py_is_order_inflight(&self, client_order_id: ClientOrderId) -> bool {
2694        self.is_order_inflight(&client_order_id)
2695    }
2696
2697    /// Returns whether an order with the `client_order_id` is `PENDING_CANCEL` locally.
2698    #[pyo3(name = "is_order_pending_cancel_local")]
2699    fn py_is_order_pending_cancel_local(&self, client_order_id: ClientOrderId) -> bool {
2700        self.is_order_pending_cancel_local(&client_order_id)
2701    }
2702
2703    /// Returns the count of all emulated orders.
2704    #[pyo3(name = "orders_emulated_count")]
2705    fn py_orders_emulated_count(
2706        &self,
2707        venue: Option<Venue>,
2708        instrument_id: Option<InstrumentId>,
2709        strategy_id: Option<StrategyId>,
2710        account_id: Option<AccountId>,
2711        side: Option<OrderSide>,
2712    ) -> usize {
2713        self.orders_emulated_count(
2714            venue.as_ref(),
2715            instrument_id.as_ref(),
2716            strategy_id.as_ref(),
2717            account_id.as_ref(),
2718            side,
2719        )
2720    }
2721
2722    /// Returns the count of all in-flight orders.
2723    #[pyo3(name = "orders_inflight_count")]
2724    fn py_orders_inflight_count(
2725        &self,
2726        venue: Option<Venue>,
2727        instrument_id: Option<InstrumentId>,
2728        strategy_id: Option<StrategyId>,
2729        account_id: Option<AccountId>,
2730        side: Option<OrderSide>,
2731    ) -> usize {
2732        self.orders_inflight_count(
2733            venue.as_ref(),
2734            instrument_id.as_ref(),
2735            strategy_id.as_ref(),
2736            account_id.as_ref(),
2737            side,
2738        )
2739    }
2740
2741    /// Returns the order list for the `order_list_id`.
2742    #[pyo3(name = "order_list")]
2743    fn py_order_list(&self, order_list_id: OrderListId) -> Option<OrderList> {
2744        self.order_list(&order_list_id).cloned()
2745    }
2746
2747    /// Returns all order lists matching the optional filter parameters.
2748    #[pyo3(name = "order_lists")]
2749    fn py_order_lists(
2750        &self,
2751        venue: Option<Venue>,
2752        instrument_id: Option<InstrumentId>,
2753        strategy_id: Option<StrategyId>,
2754        account_id: Option<AccountId>,
2755    ) -> Vec<OrderList> {
2756        self.order_lists(
2757            venue.as_ref(),
2758            instrument_id.as_ref(),
2759            strategy_id.as_ref(),
2760            account_id.as_ref(),
2761        )
2762        .into_iter()
2763        .cloned()
2764        .collect()
2765    }
2766
2767    /// Returns whether an order list with the `order_list_id` exists.
2768    #[pyo3(name = "order_list_exists")]
2769    fn py_order_list_exists(&self, order_list_id: OrderListId) -> bool {
2770        self.order_list_exists(&order_list_id)
2771    }
2772
2773    /// Returns references to all orders associated with the `exec_algorithm_id` matching the
2774    /// optional filter parameters.
2775    #[pyo3(name = "orders_for_exec_algorithm")]
2776    #[expect(clippy::too_many_arguments)]
2777    fn py_orders_for_exec_algorithm(
2778        &self,
2779        py: Python,
2780        exec_algorithm_id: ExecAlgorithmId,
2781        venue: Option<Venue>,
2782        instrument_id: Option<InstrumentId>,
2783        strategy_id: Option<StrategyId>,
2784        account_id: Option<AccountId>,
2785        side: Option<OrderSide>,
2786    ) -> PyResult<Vec<Py<PyAny>>> {
2787        self.orders_for_exec_algorithm(
2788            &exec_algorithm_id,
2789            venue.as_ref(),
2790            instrument_id.as_ref(),
2791            strategy_id.as_ref(),
2792            account_id.as_ref(),
2793            side,
2794        )
2795        .into_iter()
2796        .map(|o| order_any_to_pyobject(py, o.clone()))
2797        .collect()
2798    }
2799
2800    /// Returns references to all orders with the `exec_spawn_id`.
2801    #[pyo3(name = "orders_for_exec_spawn")]
2802    fn py_orders_for_exec_spawn(
2803        &self,
2804        py: Python,
2805        exec_spawn_id: ClientOrderId,
2806    ) -> PyResult<Vec<Py<PyAny>>> {
2807        self.orders_for_exec_spawn(&exec_spawn_id)
2808            .into_iter()
2809            .map(|o| order_any_to_pyobject(py, o.clone()))
2810            .collect()
2811    }
2812
2813    /// Returns the total order quantity for the `exec_spawn_id`.
2814    #[pyo3(name = "exec_spawn_total_quantity")]
2815    fn py_exec_spawn_total_quantity(
2816        &self,
2817        exec_spawn_id: ClientOrderId,
2818        active_only: bool,
2819    ) -> Option<Quantity> {
2820        self.exec_spawn_total_quantity(&exec_spawn_id, active_only)
2821    }
2822
2823    /// Returns the total filled quantity for all orders with the `exec_spawn_id`.
2824    #[pyo3(name = "exec_spawn_total_filled_qty")]
2825    fn py_exec_spawn_total_filled_qty(
2826        &self,
2827        exec_spawn_id: ClientOrderId,
2828        active_only: bool,
2829    ) -> Option<Quantity> {
2830        self.exec_spawn_total_filled_qty(&exec_spawn_id, active_only)
2831    }
2832
2833    /// Returns the total leaves quantity for all orders with the `exec_spawn_id`.
2834    #[pyo3(name = "exec_spawn_total_leaves_qty")]
2835    fn py_exec_spawn_total_leaves_qty(
2836        &self,
2837        exec_spawn_id: ClientOrderId,
2838        active_only: bool,
2839    ) -> Option<Quantity> {
2840        self.exec_spawn_total_leaves_qty(&exec_spawn_id, active_only)
2841    }
2842
2843    /// Returns a borrow of the position for the `client_order_id` (if found).
2844    ///
2845    /// Prefer `Self.position_for_order_ref` in new native code.
2846    #[pyo3(name = "position_for_order")]
2847    fn py_position_for_order(
2848        &self,
2849        py: Python,
2850        client_order_id: ClientOrderId,
2851    ) -> PyResult<Option<Py<PyAny>>> {
2852        match self.position_for_order(&client_order_id) {
2853            Some(position) => Ok(Some(position.clone().into_pyobject(py)?.into())),
2854            None => Ok(None),
2855        }
2856    }
2857
2858    /// Returns a reference to the position ID for the `client_order_id` (if found).
2859    #[pyo3(name = "position_id")]
2860    fn py_position_id(&self, client_order_id: ClientOrderId) -> Option<PositionId> {
2861        self.position_id(&client_order_id).copied()
2862    }
2863
2864    /// Returns borrows of all positions matching the optional filter parameters.
2865    ///
2866    /// Prefer `Self.positions_refs` in new native code.
2867    #[pyo3(name = "positions")]
2868    fn py_positions(
2869        &self,
2870        py: Python,
2871        venue: Option<Venue>,
2872        instrument_id: Option<InstrumentId>,
2873        strategy_id: Option<StrategyId>,
2874        account_id: Option<AccountId>,
2875        side: Option<PositionSide>,
2876    ) -> PyResult<Vec<Py<PyAny>>> {
2877        self.positions(
2878            venue.as_ref(),
2879            instrument_id.as_ref(),
2880            strategy_id.as_ref(),
2881            account_id.as_ref(),
2882            side,
2883        )
2884        .into_iter()
2885        .map(|p| Ok(p.clone().into_pyobject(py)?.into()))
2886        .collect()
2887    }
2888
2889    /// Returns borrows of all open positions matching the optional filter parameters.
2890    ///
2891    /// Prefer `Self.positions_open_refs` in new native code.
2892    #[pyo3(name = "positions_open")]
2893    fn py_positions_open(
2894        &self,
2895        py: Python,
2896        venue: Option<Venue>,
2897        instrument_id: Option<InstrumentId>,
2898        strategy_id: Option<StrategyId>,
2899        account_id: Option<AccountId>,
2900        side: Option<PositionSide>,
2901    ) -> PyResult<Vec<Py<PyAny>>> {
2902        self.positions_open(
2903            venue.as_ref(),
2904            instrument_id.as_ref(),
2905            strategy_id.as_ref(),
2906            account_id.as_ref(),
2907            side,
2908        )
2909        .into_iter()
2910        .map(|p| Ok(p.clone().into_pyobject(py)?.into()))
2911        .collect()
2912    }
2913
2914    /// Returns borrows of all closed positions matching the optional filter parameters.
2915    ///
2916    /// Prefer `Self.positions_closed_refs` in new native code.
2917    #[pyo3(name = "positions_closed")]
2918    fn py_positions_closed(
2919        &self,
2920        py: Python,
2921        venue: Option<Venue>,
2922        instrument_id: Option<InstrumentId>,
2923        strategy_id: Option<StrategyId>,
2924        account_id: Option<AccountId>,
2925        side: Option<PositionSide>,
2926    ) -> PyResult<Vec<Py<PyAny>>> {
2927        self.positions_closed(
2928            venue.as_ref(),
2929            instrument_id.as_ref(),
2930            strategy_id.as_ref(),
2931            account_id.as_ref(),
2932            side,
2933        )
2934        .into_iter()
2935        .map(|p| Ok(p.clone().into_pyobject(py)?.into()))
2936        .collect()
2937    }
2938
2939    /// Gets a reference to the strategy ID for the `client_order_id` (if found).
2940    #[pyo3(name = "strategy_id_for_order")]
2941    fn py_strategy_id_for_order(&self, client_order_id: ClientOrderId) -> Option<StrategyId> {
2942        self.strategy_id_for_order(&client_order_id).copied()
2943    }
2944
2945    /// Gets a reference to the strategy ID for the `position_id` (if found).
2946    #[pyo3(name = "strategy_id_for_position")]
2947    fn py_strategy_id_for_position(&self, position_id: PositionId) -> Option<StrategyId> {
2948        self.strategy_id_for_position(&position_id).copied()
2949    }
2950
2951    /// Gets the serialized position snapshot frames for the `position_id`.
2952    ///
2953    /// Each element in the returned vector is one JSON-encoded `Position` snapshot,
2954    /// in the order they were taken. Frames that fail to serialize are skipped with a warning.
2955    #[pyo3(name = "position_snapshot_bytes")]
2956    fn py_position_snapshot_bytes(&self, position_id: PositionId) -> Option<Vec<Vec<u8>>> {
2957        self.position_snapshot_bytes(&position_id)
2958    }
2959
2960    /// Returns all position snapshots with the given optional filters.
2961    ///
2962    /// When `position_id` is `Some`, only snapshots for that position are returned.
2963    /// When `account_id` is `Some`, snapshots are filtered to that account.
2964    #[pyo3(name = "position_snapshots", signature = (position_id=None, account_id=None))]
2965    fn py_position_snapshots(
2966        &self,
2967        py: Python,
2968        position_id: Option<PositionId>,
2969        account_id: Option<AccountId>,
2970    ) -> PyResult<Vec<Py<PyAny>>> {
2971        self.position_snapshots(position_id.as_ref(), account_id.as_ref())
2972            .into_iter()
2973            .map(|p| Ok(p.into_pyobject(py)?.into()))
2974            .collect()
2975    }
2976
2977    /// Returns a borrow of the account for the `account_id` (if found).
2978    ///
2979    /// Prefer `Self.account_ref` in new native code.
2980    #[pyo3(name = "account")]
2981    fn py_account(&self, py: Python, account_id: AccountId) -> PyResult<Option<Py<PyAny>>> {
2982        match self.account(&account_id) {
2983            Some(account) => Ok(Some(account_any_to_pyobject(py, account.clone())?)),
2984            None => Ok(None),
2985        }
2986    }
2987
2988    /// Returns a borrow of the account for the `venue` (if found).
2989    #[pyo3(name = "account_for_venue")]
2990    fn py_account_for_venue(&self, py: Python, venue: Venue) -> PyResult<Option<Py<PyAny>>> {
2991        match self.account_for_venue(&venue) {
2992            Some(account) => Ok(Some(account_any_to_pyobject(py, account.clone())?)),
2993            None => Ok(None),
2994        }
2995    }
2996
2997    /// Returns a reference to the account ID for the `venue` (if found).
2998    #[pyo3(name = "account_id")]
2999    fn py_account_id(&self, venue: Venue) -> Option<AccountId> {
3000        self.account_id(&venue).copied()
3001    }
3002
3003    /// Gets a reference to the general value for the `key` (if found).
3004    ///
3005    /// # Errors
3006    ///
3007    /// Returns an error if the `key` is invalid.
3008    #[pyo3(name = "get")]
3009    fn py_get(&self, key: &str) -> PyResult<Option<Vec<u8>>> {
3010        match self.get(key).map_err(to_pyvalue_err)? {
3011            Some(bytes) => Ok(Some(bytes.to_vec())),
3012            None => Ok(None),
3013        }
3014    }
3015
3016    /// Adds a general `value` to the cache for the given `key`.
3017    #[pyo3(name = "add")]
3018    fn py_add_general(&mut self, key: &str, value: Vec<u8>) -> PyResult<()> {
3019        self.add(key, Bytes::from(value)).map_err(to_pyvalue_err)
3020    }
3021
3022    /// Returns the price for the `instrument_id` and `price_type` (if found).
3023    #[pyo3(name = "price")]
3024    fn py_price(&self, instrument_id: InstrumentId, price_type: PriceType) -> Option<Price> {
3025        self.price(&instrument_id, price_type)
3026    }
3027
3028    /// Returns the exchange rate for the given parameters.
3029    #[pyo3(name = "get_xrate")]
3030    fn py_get_xrate(
3031        &self,
3032        venue: Venue,
3033        from_currency: Currency,
3034        to_currency: Currency,
3035        price_type: PriceType,
3036    ) -> Option<f64> {
3037        self.get_xrate(venue, from_currency, to_currency, price_type)
3038            .and_then(|rate| rate.to_f64())
3039    }
3040
3041    /// Returns the mark exchange rate for the given currency pair, or `None` if not set.
3042    #[pyo3(name = "get_mark_xrate")]
3043    fn py_get_mark_xrate(&self, from_currency: Currency, to_currency: Currency) -> Option<f64> {
3044        self.get_mark_xrate(from_currency, to_currency)
3045    }
3046
3047    /// Sets the mark exchange rate for the given currency pair and automatically sets the inverse rate.
3048    #[pyo3(name = "set_mark_xrate")]
3049    fn py_set_mark_xrate(&mut self, from_currency: Currency, to_currency: Currency, xrate: f64) {
3050        self.set_mark_xrate(from_currency, to_currency, xrate);
3051    }
3052
3053    /// Clears the mark exchange rate for the given currency pair direction.
3054    ///
3055    /// Removes only the `(from_currency, to_currency)` entry; the inverse rate written
3056    /// by `Self.set_mark_xrate` is retained until cleared separately or
3057    /// `Self.clear_mark_xrates` is called.
3058    #[pyo3(name = "clear_mark_xrate")]
3059    fn py_clear_mark_xrate(&mut self, from_currency: Currency, to_currency: Currency) {
3060        self.clear_mark_xrate(from_currency, to_currency);
3061    }
3062
3063    /// Clears all mark exchange rates.
3064    #[pyo3(name = "clear_mark_xrates")]
3065    fn py_clear_mark_xrates(&mut self) {
3066        self.clear_mark_xrates();
3067    }
3068
3069    /// Calculates the unrealized PnL for the given position.
3070    #[pyo3(name = "calculate_unrealized_pnl")]
3071    #[expect(clippy::needless_pass_by_value)]
3072    fn py_calculate_unrealized_pnl(
3073        &self,
3074        py: Python,
3075        position: Py<PyAny>,
3076    ) -> PyResult<Option<Money>> {
3077        let position = position.extract::<Position>(py)?;
3078        Ok(self.calculate_unrealized_pnl(&position))
3079    }
3080
3081    /// Gets a reference to the own order book for the `instrument_id`.
3082    #[pyo3(name = "own_order_book")]
3083    fn py_own_order_book(&self, instrument_id: InstrumentId) -> Option<OwnOrderBook> {
3084        self.own_order_book(&instrument_id).cloned()
3085    }
3086
3087    /// Updates the own order book with an order.
3088    ///
3089    /// This method adds, updates, or removes an order from the own order book
3090    /// based on the order's current state.
3091    ///
3092    /// Orders without prices (MARKET, etc.) are skipped as they cannot be
3093    /// represented in own books.
3094    #[pyo3(name = "update_own_order_book")]
3095    fn py_update_own_order_book(&mut self, py: Python, order: Py<PyAny>) -> PyResult<()> {
3096        let order_any = pyobject_to_order_any(py, order)?;
3097        self.update_own_order_book(&order_any);
3098        Ok(())
3099    }
3100
3101    /// Force removal of an order from own order books and clean up all indexes.
3102    ///
3103    /// This method is used when order event application fails and we need to ensure
3104    /// terminal orders are properly cleaned up from own books and all relevant indexes.
3105    /// Replicates the index cleanup that `update_order` performs for closed orders.
3106    #[pyo3(name = "force_remove_from_own_order_book")]
3107    fn py_force_remove_from_own_order_book(&mut self, client_order_id: ClientOrderId) {
3108        self.force_remove_from_own_order_book(&client_order_id);
3109    }
3110
3111    /// Audit all own order books against active order indexes.
3112    ///
3113    /// Ensures orders absent from the open, inflight, and active-local indexes are removed from
3114    /// own order books.
3115    #[pyo3(name = "audit_own_order_books")]
3116    fn py_audit_own_order_books(&mut self) {
3117        self.audit_own_order_books();
3118    }
3119}
3120
3121#[cfg(feature = "defi")]
3122#[pymethods]
3123impl Cache {
3124    /// Adds a `Pool` to the cache.
3125    ///
3126    /// # Errors
3127    ///
3128    /// This function currently does not return errors but follows the same pattern as other add methods for consistency.
3129    #[pyo3(name = "add_pool")]
3130    fn py_add_pool(&mut self, pool: Pool) -> PyResult<()> {
3131        self.add_pool(pool).map_err(to_pyvalue_err)
3132    }
3133
3134    /// Gets a reference to the pool for the `instrument_id`.
3135    #[pyo3(name = "pool")]
3136    fn py_pool(&self, instrument_id: InstrumentId) -> Option<Pool> {
3137        self.pool(&instrument_id).cloned()
3138    }
3139
3140    /// Returns the instrument IDs of all pools in the cache, optionally filtered by `venue`.
3141    #[pyo3(name = "pool_ids")]
3142    fn py_pool_ids(&self, venue: Option<Venue>) -> Vec<InstrumentId> {
3143        self.pool_ids(venue.as_ref())
3144    }
3145
3146    /// Returns references to all pools in the cache, optionally filtered by `venue`.
3147    #[pyo3(name = "pools")]
3148    fn py_pools(&self, venue: Option<Venue>) -> Vec<Pool> {
3149        self.pools(venue.as_ref()).into_iter().cloned().collect()
3150    }
3151
3152    /// Adds a `PoolProfiler` to the cache.
3153    ///
3154    /// # Errors
3155    ///
3156    /// This function currently does not return errors but follows the same pattern as other add methods for consistency.
3157    #[pyo3(name = "add_pool_profiler")]
3158    fn py_add_pool_profiler(&mut self, pool_profiler: PoolProfiler) -> PyResult<()> {
3159        self.add_pool_profiler(pool_profiler)
3160            .map_err(to_pyvalue_err)
3161    }
3162
3163    /// Gets a reference to the pool profiler for the `instrument_id`.
3164    #[pyo3(name = "pool_profiler")]
3165    fn py_pool_profiler(&self, instrument_id: InstrumentId) -> Option<PoolProfiler> {
3166        self.pool_profiler(&instrument_id).cloned()
3167    }
3168
3169    /// Returns the instrument IDs of all pool profilers in the cache, optionally filtered by `venue`.
3170    #[pyo3(name = "pool_profiler_ids")]
3171    fn py_pool_profiler_ids(&self, venue: Option<Venue>) -> Vec<InstrumentId> {
3172        self.pool_profiler_ids(venue.as_ref())
3173    }
3174
3175    /// Returns references to all pool profilers in the cache, optionally filtered by `venue`.
3176    #[pyo3(name = "pool_profilers")]
3177    fn py_pool_profilers(&self, venue: Option<Venue>) -> Vec<PoolProfiler> {
3178        self.pool_profilers(venue.as_ref())
3179            .into_iter()
3180            .cloned()
3181            .collect()
3182    }
3183}