nautilus-core 0.62.0

Core functionality for the Nautilus trading engine
Documentation
// -------------------------------------------------------------------------------------------------
//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
//  https://nautechsystems.io
//
//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
//  You may not use this file except in compliance with the License.
//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// -------------------------------------------------------------------------------------------------

#![expect(clippy::doc_markdown, reason = "Python docstrings")]

//! Python bindings and interoperability built using [`PyO3`](https://pyo3.rs).

#![allow(
    deprecated,
    reason = "pyo3-stub-gen currently relies on PyO3 initialization helpers marked as deprecated"
)]
#![expect(
    clippy::missing_errors_doc,
    reason = "errors documented on underlying Rust methods"
)]
//!
//! This sub-module groups together the Rust code that is *only* required when compiling the
//! `python` feature flag. It provides thin adapters so that NautilusTrader functionality can be
//! consumed from the `nautilus_trader` Python package without sacrificing type-safety or
//! performance.

/// Implements read-only Python getters for cloneable configuration fields.
#[macro_export]
macro_rules! impl_pyo3_config_getters {
    ($config:ty { $($field:ident: $field_type:ty),+ $(,)? }) => {
        #[pyo3_stub_gen::derive::gen_stub_pymethods]
        #[pyo3::pymethods]
        #[allow(
            clippy::clone_on_copy,
            reason = "one macro handles Copy and owned configuration fields"
        )]
        impl $config {
            $(
                #[getter]
                fn $field(&self) -> $field_type {
                    self.$field.clone()
                }
            )+
        }
    };
}

pub mod casing;
pub mod datetime;
pub mod enums;
pub mod params;
pub mod parsing;
pub mod serialization;
/// String manipulation utilities for Python.
pub mod string;
pub mod uuid;
pub mod version;

use std::{convert::Infallible, fmt::Display};

use pyo3::{
    BoundObject, Py,
    conversion::IntoPyObjectExt,
    exceptions::{
        PyException, PyKeyError, PyNotImplementedError, PyRuntimeError, PyTypeError, PyValueError,
    },
    prelude::*,
    types::{PyString, PyWeakrefMethods, PyWeakrefReference},
    wrap_pyfunction,
};

use crate::{
    UUID4,
    consts::{NAUTILUS_USER_AGENT, NAUTILUS_VERSION},
    correctness::CorrectnessError,
    datetime::{
        MILLISECONDS_IN_SECOND, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
        NANOSECONDS_IN_SECOND,
    },
};

/// Clones a Python object reference by attaching to the interpreter.
///
/// The result is a second strong reference to the same object, so this does not break a reference
/// cycle. When a Rust object holds a `Py<T>` whose Python object reaches back into Rust, cloning
/// adds another strong edge to that cycle rather than removing one.
///
/// Break such a back-reference with a Python weak reference (see [`upgrade_py_weakref`]) or an
/// explicit terminal release point that drops the strong reference during disposal.
#[must_use]
pub fn clone_py_object(obj: &Py<PyAny>) -> Py<PyAny> {
    Python::attach(|py| obj.clone_ref(py))
}

/// Upgrades the weak reference a Rust object keeps to its Python wrapper.
///
/// Returns `Ok(None)` when no wrapper was ever attached, which is the case for a purely Rust
/// construction. `owner` names the Rust object in the error message.
///
/// # Errors
///
/// Returns an error if a wrapper was attached but has since been collected. Callers propagate
/// this rather than skipping a required callback, because a live wrapper is an ownership
/// invariant of the caller rather than an optional extra.
pub fn upgrade_py_weakref(
    py_self: Option<&Py<PyWeakrefReference>>,
    owner: &dyn Display,
) -> PyResult<Option<Py<PyAny>>> {
    let Some(py_self) = py_self else {
        return Ok(None);
    };

    Python::attach(|py| {
        py_self
            .bind(py)
            .upgrade()
            .map(|wrapper| Some(wrapper.unbind()))
            .ok_or_else(|| {
                to_pyruntime_err(format!("Python wrapper for {owner} has been collected"))
            })
    })
}

/// Calls a Python callback with a single argument, logging any errors.
pub fn call_python(py: Python, callback: &Py<PyAny>, py_obj: Py<PyAny>) {
    if let Err(e) = callback.call1(py, (py_obj,)) {
        log::error!("Error calling Python: {e}");
    }
}

/// Schedules a Python callback on the event loop thread via `call_soon_threadsafe`.
///
/// This must be used instead of [`call_python`] when invoking Python callbacks
/// from Tokio worker threads, since Python callbacks that enter the kernel
/// (e.g. via `MessageBus.send`) must run on the asyncio event loop thread.
pub fn call_python_threadsafe(
    py: Python,
    call_soon: &Py<PyAny>,
    callback: &Py<PyAny>,
    py_obj: Py<PyAny>,
) {
    if let Err(e) = call_soon.call1(py, (callback, py_obj)) {
        log::error!("Error scheduling Python callback on event loop: {e}");
    }
}

/// Extends `IntoPyObjectExt` with an infallible conversion to `Py<PyAny>`.
pub trait IntoPyObjectNautilusExt<'py>: IntoPyObjectExt<'py> {
    /// Converts `self` into a [`Py<PyAny>`] when the underlying conversion is infallible.
    #[inline]
    fn into_py_any_unwrap(self, py: Python<'py>) -> Py<PyAny>
    where
        Self: IntoPyObject<'py, Error = Infallible>,
    {
        match self.into_pyobject(py) {
            Ok(obj) => obj.into_any().unbind(),
            Err(never) => match never {},
        }
    }
}

impl<'py, T> IntoPyObjectNautilusExt<'py> for T where T: IntoPyObjectExt<'py> {}

/// Gets the type name for the given Python `obj`.
///
/// # Errors
///
/// Returns a error if accessing the type name fails.
pub fn get_pytype_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyString>> {
    obj.get_type().name()
}

/// Converts any type that implements `Display` to a Python `ValueError`.
pub fn to_pyvalue_err(e: impl Display) -> PyErr {
    PyValueError::new_err(e.to_string())
}

/// Converts a correctness check failure to a Python `ValueError`.
#[must_use]
#[allow(
    clippy::needless_pass_by_value,
    reason = "Result::map_err passes owned errors to conversion functions"
)]
pub fn correctness_error_to_pyvalue_err(e: CorrectnessError) -> PyErr {
    PyValueError::new_err(e.to_string())
}

/// Converts any type that implements `Display` to a Python `TypeError`.
pub fn to_pytype_err(e: impl Display) -> PyErr {
    PyTypeError::new_err(e.to_string())
}

/// Converts any type that implements `Display` to a Python `RuntimeError`.
pub fn to_pyruntime_err(e: impl Display) -> PyErr {
    PyRuntimeError::new_err(e.to_string())
}

/// Converts any type that implements `Display` to a Python `KeyError`.
pub fn to_pykey_err(e: impl Display) -> PyErr {
    PyKeyError::new_err(e.to_string())
}

/// Converts any type that implements `Display` to a Python `Exception`.
pub fn to_pyexception(e: impl Display) -> PyErr {
    PyException::new_err(e.to_string())
}

/// Converts any type that implements `Display` to a Python `NotImplementedError`.
pub fn to_pynotimplemented_err(e: impl Display) -> PyErr {
    PyNotImplementedError::new_err(e.to_string())
}

/// Exposed through `nautilus_trader.core`.
///
/// # Errors
///
/// Returns a `PyErr` if registering any module components fails.
#[pymodule]
#[rustfmt::skip]
pub fn core(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add(stringify!(NAUTILUS_VERSION), NAUTILUS_VERSION)?;
    m.add(stringify!(NAUTILUS_USER_AGENT), NAUTILUS_USER_AGENT)?;
    m.add(stringify!(MILLISECONDS_IN_SECOND), MILLISECONDS_IN_SECOND)?;
    m.add(stringify!(NANOSECONDS_IN_SECOND), NANOSECONDS_IN_SECOND)?;
    m.add(stringify!(NANOSECONDS_IN_MILLISECOND), NANOSECONDS_IN_MILLISECOND)?;
    m.add(stringify!(NANOSECONDS_IN_MICROSECOND), NANOSECONDS_IN_MICROSECOND)?;
    m.add_class::<UUID4>()?;
    m.add_function(wrap_pyfunction!(casing::py_convert_to_snake_case, m)?)?;
    m.add_function(wrap_pyfunction!(string::py_mask_api_key, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_secs_to_nanos, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_secs_to_millis, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_millis_to_nanos, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_micros_to_nanos, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_nanos_to_secs, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_nanos_to_millis, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_nanos_to_micros, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_unix_nanos_to_iso8601, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_last_weekday_nanos, m)?)?;
    m.add_function(wrap_pyfunction!(datetime::py_is_within_last_24_hours, m)?)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::sync::Once;

    use pyo3::{Python, exceptions::PyValueError};
    use rstest::rstest;

    use super::*;

    fn ensure_python_initialized() {
        static INIT: Once = Once::new();
        INIT.call_once(|| {
            Python::initialize();
        });
    }

    #[rstest]
    fn test_correctness_error_to_pyvalue_err_preserves_display_text() {
        ensure_python_initialized();

        let error = CorrectnessError::EmptyString {
            param: "value".to_string(),
        };

        Python::attach(|py| {
            let py_err = correctness_error_to_pyvalue_err(error);

            assert!(py_err.is_instance_of::<PyValueError>(py));
            assert_eq!(
                py_err.value(py).to_string(),
                "invalid string for 'value', was empty"
            );
        });
    }
}