Skip to main content

nautilus_infrastructure/python/sql/
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
16use bytes::Bytes;
17use nautilus_common::{
18    cache::database::{CacheDatabaseAdapter, CacheDatabaseFactory},
19    live::get_runtime,
20    python::cache::get_global_cache_database_factory_registry,
21    signal::Signal,
22};
23use nautilus_core::python::to_pyruntime_err;
24use nautilus_model::{
25    data::{Bar, CustomData, DataType, QuoteTick, TradeTick},
26    events::{OrderSnapshot, PositionSnapshot},
27    identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, TraderId},
28    python::{
29        account::{account_any_to_pyobject, pyobject_to_account_any},
30        events::order::pyobject_to_order_event,
31        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
32        orders::{order_any_to_pyobject, pyobject_to_order_any},
33    },
34    types::Currency,
35};
36use pyo3::{IntoPyObjectExt, prelude::*};
37
38use crate::sql::{
39    cache::{PostgresCacheConfig, PostgresCacheDatabase},
40    queries::DatabaseQueries,
41};
42
43#[pymethods]
44impl PostgresCacheDatabase {
45    /// Connects to the Postgres cache database using the provided connection parameters.
46    ///
47    /// Loads, trader-owned writes and flushes are scoped to `trader_id`, and account events are
48    /// stamped with it.
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if establishing the database connection fails, if the schema is out of
53    /// date, if any account events have no trader (the error lists the accounts to assign), or if
54    /// checking account ownership fails.
55    #[staticmethod]
56    #[pyo3(name = "connect")]
57    #[pyo3(signature = (host=None, port=None, username=None, password=None, database=None, *, trader_id))]
58    fn py_connect(
59        host: Option<String>,
60        port: Option<u16>,
61        username: Option<String>,
62        password: Option<String>,
63        database: Option<String>,
64        trader_id: TraderId,
65    ) -> PyResult<Self> {
66        let result = get_runtime().block_on(async {
67            Self::connect(host, port, username, password, database, trader_id).await
68        });
69        result.map_err(to_pyruntime_err)
70    }
71
72    #[pyo3(name = "close")]
73    fn py_close(&mut self) -> PyResult<()> {
74        self.close().map_err(to_pyruntime_err)
75    }
76
77    #[pyo3(name = "flush_db")]
78    fn py_flush_db(&mut self) -> PyResult<()> {
79        self.flush().map_err(to_pyruntime_err)
80    }
81
82    #[pyo3(name = "load")]
83    fn py_load(&self) -> PyResult<std::collections::HashMap<String, Vec<u8>>> {
84        get_runtime()
85            .block_on(async { DatabaseQueries::load(&self.pool).await })
86            .map(|m| m.into_iter().collect())
87            .map_err(to_pyruntime_err)
88    }
89
90    #[pyo3(name = "load_currency")]
91    fn py_load_currency(&self, code: &str) -> PyResult<Option<Currency>> {
92        let result = get_runtime()
93            .block_on(async { DatabaseQueries::load_currency(&self.pool, code).await });
94        result.map_err(to_pyruntime_err)
95    }
96
97    #[pyo3(name = "load_currencies")]
98    fn py_load_currencies(&self) -> PyResult<Vec<Currency>> {
99        let result =
100            get_runtime().block_on(async { DatabaseQueries::load_currencies(&self.pool).await });
101        result.map_err(to_pyruntime_err)
102    }
103
104    #[pyo3(name = "load_instrument")]
105    fn py_load_instrument(
106        &self,
107        py: Python,
108        instrument_id: InstrumentId,
109    ) -> PyResult<Option<Py<PyAny>>> {
110        get_runtime().block_on(async {
111            let result = DatabaseQueries::load_instrument(&self.pool, &instrument_id)
112                .await
113                .map_err(to_pyruntime_err)?;
114
115            match result {
116                Some(instrument) => {
117                    let py_object = instrument_any_to_pyobject(py, instrument)?;
118                    Ok(Some(py_object))
119                }
120                None => Ok(None),
121            }
122        })
123    }
124
125    #[pyo3(name = "load_instruments")]
126    fn py_load_instruments(&self, py: Python) -> PyResult<Vec<Py<PyAny>>> {
127        get_runtime().block_on(async {
128            let result = DatabaseQueries::load_instruments(&self.pool)
129                .await
130                .map_err(to_pyruntime_err)?;
131            let mut instruments = Vec::new();
132
133            for instrument in result {
134                let py_object = instrument_any_to_pyobject(py, instrument)?;
135                instruments.push(py_object);
136            }
137            Ok(instruments)
138        })
139    }
140
141    #[pyo3(name = "load_order")]
142    fn py_load_order(
143        &self,
144        py: Python,
145        client_order_id: ClientOrderId,
146    ) -> PyResult<Option<Py<PyAny>>> {
147        get_runtime().block_on(async {
148            let result =
149                DatabaseQueries::load_order(&self.pool, &client_order_id, &self.trader_id())
150                    .await
151                    .map_err(to_pyruntime_err)?;
152
153            match result {
154                Some(order) => {
155                    let py_object = order_any_to_pyobject(py, order)?;
156                    Ok(Some(py_object))
157                }
158                None => Ok(None),
159            }
160        })
161    }
162
163    #[pyo3(name = "load_account")]
164    fn py_load_account(&self, py: Python, account_id: AccountId) -> PyResult<Option<Py<PyAny>>> {
165        get_runtime().block_on(async {
166            let result = DatabaseQueries::load_account(&self.pool, &account_id, &self.trader_id())
167                .await
168                .map_err(to_pyruntime_err)?;
169
170            match result {
171                Some(account) => {
172                    let py_object = account_any_to_pyobject(py, account)?;
173                    Ok(Some(py_object))
174                }
175                None => Ok(None),
176            }
177        })
178    }
179
180    #[pyo3(name = "load_quotes")]
181    fn py_load_quotes(&self, py: Python, instrument_id: InstrumentId) -> PyResult<Vec<Py<PyAny>>> {
182        get_runtime().block_on(async {
183            let result = DatabaseQueries::load_quotes(&self.pool, &instrument_id)
184                .await
185                .map_err(to_pyruntime_err)?;
186            let mut quotes = Vec::new();
187
188            for quote in result {
189                let py_object = quote.into_py_any(py)?;
190                quotes.push(py_object);
191            }
192            Ok(quotes)
193        })
194    }
195
196    #[pyo3(name = "load_trades")]
197    fn py_load_trades(&self, py: Python, instrument_id: InstrumentId) -> PyResult<Vec<Py<PyAny>>> {
198        get_runtime().block_on(async {
199            let result = DatabaseQueries::load_trades(&self.pool, &instrument_id)
200                .await
201                .map_err(to_pyruntime_err)?;
202            let mut trades = Vec::new();
203
204            for trade in result {
205                let py_object = trade.into_py_any(py)?;
206                trades.push(py_object);
207            }
208            Ok(trades)
209        })
210    }
211
212    #[pyo3(name = "load_bars")]
213    fn py_load_bars(&self, py: Python, instrument_id: InstrumentId) -> PyResult<Vec<Py<PyAny>>> {
214        get_runtime().block_on(async {
215            let result = DatabaseQueries::load_bars(&self.pool, &instrument_id)
216                .await
217                .map_err(to_pyruntime_err)?;
218            let mut bars = Vec::new();
219
220            for bar in result {
221                let py_object = bar.into_py_any(py)?;
222                bars.push(py_object);
223            }
224            Ok(bars)
225        })
226    }
227
228    #[pyo3(name = "load_signals")]
229    fn py_load_signals(&self, name: &str) -> PyResult<Vec<Signal>> {
230        get_runtime().block_on(async {
231            DatabaseQueries::load_signals(&self.pool, name)
232                .await
233                .map_err(to_pyruntime_err)
234        })
235    }
236
237    #[pyo3(name = "load_custom_data")]
238    #[expect(clippy::needless_pass_by_value)]
239    fn py_load_custom_data(&self, data_type: DataType) -> PyResult<Vec<CustomData>> {
240        get_runtime()
241            .block_on(async { DatabaseQueries::load_custom_data(&self.pool, &data_type).await })
242            .map_err(to_pyruntime_err)
243    }
244
245    #[pyo3(name = "load_order_snapshot")]
246    fn py_load_order_snapshot(
247        &self,
248        client_order_id: ClientOrderId,
249    ) -> PyResult<Option<OrderSnapshot>> {
250        get_runtime().block_on(async {
251            DatabaseQueries::load_order_snapshot(&self.pool, &client_order_id, &self.trader_id())
252                .await
253                .map_err(to_pyruntime_err)
254        })
255    }
256
257    #[pyo3(name = "load_position_snapshot")]
258    fn py_load_position_snapshot(
259        &self,
260        position_id: PositionId,
261    ) -> PyResult<Option<PositionSnapshot>> {
262        get_runtime().block_on(async {
263            DatabaseQueries::load_position_snapshot(&self.pool, &position_id, &self.trader_id())
264                .await
265                .map_err(to_pyruntime_err)
266        })
267    }
268
269    #[pyo3(name = "add")]
270    fn py_add(&self, key: String, value: Vec<u8>) -> PyResult<()> {
271        self.add(key, Bytes::from(value)).map_err(to_pyruntime_err)
272    }
273
274    #[pyo3(name = "add_currency")]
275    fn py_add_currency(&self, currency: Currency) -> PyResult<()> {
276        self.add_currency(&currency).map_err(to_pyruntime_err)
277    }
278
279    #[pyo3(name = "add_instrument")]
280    fn py_add_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
281        let instrument_any = pyobject_to_instrument_any(py, instrument)?;
282        self.add_instrument(&instrument_any)
283            .map_err(to_pyruntime_err)
284    }
285
286    #[pyo3(name = "add_order")]
287    #[pyo3(signature = (order, client_id=None))]
288    fn py_add_order(
289        &self,
290        py: Python,
291        order: Py<PyAny>,
292        client_id: Option<ClientId>,
293    ) -> PyResult<()> {
294        let order_any = pyobject_to_order_any(py, order)?;
295        self.add_order(&order_any, client_id)
296            .map_err(to_pyruntime_err)
297    }
298
299    #[pyo3(name = "add_order_snapshot")]
300    #[expect(clippy::needless_pass_by_value)]
301    fn py_add_order_snapshot(&self, snapshot: OrderSnapshot) -> PyResult<()> {
302        self.add_order_snapshot(&snapshot).map_err(to_pyruntime_err)
303    }
304
305    #[pyo3(name = "add_position_snapshot")]
306    #[expect(clippy::needless_pass_by_value)]
307    fn py_add_position_snapshot(&self, snapshot: PositionSnapshot) -> PyResult<()> {
308        self.add_position_snapshot(&snapshot)
309            .map_err(to_pyruntime_err)
310    }
311
312    #[pyo3(name = "add_account")]
313    fn py_add_account(&self, py: Python, account: Py<PyAny>) -> PyResult<()> {
314        let account_any = pyobject_to_account_any(py, account)?;
315        self.add_account(&account_any).map_err(to_pyruntime_err)
316    }
317
318    #[pyo3(name = "add_quote")]
319    fn py_add_quote(&self, quote: QuoteTick) -> PyResult<()> {
320        self.add_quote(&quote).map_err(to_pyruntime_err)
321    }
322
323    #[pyo3(name = "add_trade")]
324    fn py_add_trade(&self, trade: TradeTick) -> PyResult<()> {
325        self.add_trade(&trade).map_err(to_pyruntime_err)
326    }
327
328    #[pyo3(name = "add_bar")]
329    fn py_add_bar(&self, bar: Bar) -> PyResult<()> {
330        self.add_bar(&bar).map_err(to_pyruntime_err)
331    }
332
333    #[pyo3(name = "add_signal")]
334    #[expect(clippy::needless_pass_by_value)]
335    fn py_add_signal(&self, signal: Signal) -> PyResult<()> {
336        self.add_signal(&signal).map_err(to_pyruntime_err)
337    }
338
339    #[pyo3(name = "add_custom_data")]
340    #[expect(clippy::needless_pass_by_value)]
341    fn py_add_custom_data(&self, data: CustomData) -> PyResult<()> {
342        self.add_custom_data(&data).map_err(to_pyruntime_err)
343    }
344
345    #[pyo3(name = "update_order")]
346    fn py_update_order(&self, py: Python, order_event: Py<PyAny>) -> PyResult<()> {
347        let event = pyobject_to_order_event(py, order_event)?;
348        self.update_order(&event).map_err(to_pyruntime_err)
349    }
350
351    #[pyo3(name = "update_account")]
352    fn py_update_account(&self, py: Python, order: Py<PyAny>) -> PyResult<()> {
353        let order_any = pyobject_to_account_any(py, order)?;
354        self.update_account(&order_any).map_err(to_pyruntime_err)
355    }
356}
357
358#[pymethods]
359#[pyo3_stub_gen::derive::gen_stub_pymethods]
360impl PostgresCacheConfig {
361    /// Configuration for a Postgres-backed cache database.
362    ///
363    /// Missing fields are resolved from Postgres environment variables and then built-in defaults.
364    #[new]
365    #[pyo3(signature = (host=None, port=None, username=None, password=None, database=None))]
366    fn py_new(
367        host: Option<String>,
368        port: Option<u16>,
369        username: Option<String>,
370        password: Option<String>,
371        database: Option<String>,
372    ) -> Self {
373        Self {
374            host,
375            port,
376            username,
377            password,
378            database,
379        }
380    }
381
382    #[getter]
383    fn host(&self) -> Option<&str> {
384        self.host.as_deref()
385    }
386
387    #[getter]
388    const fn port(&self) -> Option<u16> {
389        self.port
390    }
391
392    #[getter]
393    fn username(&self) -> Option<&str> {
394        self.username.as_deref()
395    }
396
397    #[getter]
398    fn password(&self) -> Option<&str> {
399        self.password.as_deref()
400    }
401
402    #[getter]
403    fn database(&self) -> Option<&str> {
404        self.database.as_deref()
405    }
406}
407
408#[expect(clippy::needless_pass_by_value)]
409fn extract_postgres_cache_database_factory(
410    py: Python<'_>,
411    factory: Py<PyAny>,
412) -> PyResult<Box<dyn CacheDatabaseFactory>> {
413    Ok(Box::new(factory.extract::<PostgresCacheConfig>(py)?))
414}
415
416pub(in crate::python) fn register_postgres_cache_database_factory() -> PyResult<()> {
417    get_global_cache_database_factory_registry()
418        .register(
419            stringify!(PostgresCacheConfig).to_string(),
420            extract_postgres_cache_database_factory,
421        )
422        .map_err(to_pyruntime_err)
423}