nautilus-model 0.62.0

Domain model 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.
// -------------------------------------------------------------------------------------------------

use std::ffi::c_char;

use nautilus_core::{
    UnixNanos,
    ffi::{
        abort_on_panic,
        cvec::CVec,
        parsing::{bytes_to_string_vec, string_vec_to_bytes},
        string::{cstr_as_str, str_to_cstr},
    },
};

use crate::{
    identifiers::{InstrumentId, Symbol},
    instruments::synthetic::SyntheticInstrument,
    types::{ERROR_PRICE, Price},
};

/// Creates a new [`SyntheticInstrument`] from the given components and formula.
///
/// # Panics
///
/// Panics if the formula is invalid for the given components.
///
/// # Safety
///
/// This function assumes:
/// - `components_ptr` is a valid C string pointer of a JSON format list of strings.
/// - `formula_ptr` is a valid C string pointer.
///
/// Returns an owning pointer to the heap-allocated `SyntheticInstrument` which the
/// caller must eventually pass to [`synthetic_instrument_drop`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn synthetic_instrument_new(
    symbol: Symbol,
    price_precision: u8,
    components_ptr: *const c_char,
    formula_ptr: *const c_char,
    ts_event: u64,
    ts_init: u64,
) -> *mut SyntheticInstrument {
    // TODO: There is absolutely no error handling here yet
    let components = unsafe { bytes_to_string_vec(components_ptr) }
        .into_iter()
        .map(InstrumentId::from)
        .collect::<Vec<InstrumentId>>();
    let formula = unsafe { cstr_as_str(formula_ptr) };
    let synth = SyntheticInstrument::new(
        symbol,
        price_precision,
        components,
        formula,
        ts_event.into(),
        ts_init.into(),
    );

    Box::into_raw(Box::new(synth))
}

/// # Safety
///
/// `synth` must be a live owning pointer returned by [`synthetic_instrument_new`],
/// and must not be used after this call.
///
/// # Panics
///
/// Panics if `synth` is null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn synthetic_instrument_drop(synth: *mut SyntheticInstrument) {
    abort_on_panic(|| {
        assert!(!synth.is_null(), "`synth` was NULL");
        // SAFETY: Caller guarantees `synth` was allocated by `synthetic_instrument_new`
        drop(unsafe { Box::from_raw(synth) }); // Memory freed here
    });
}

#[unsafe(no_mangle)]
pub extern "C" fn synthetic_instrument_id(synth: &SyntheticInstrument) -> InstrumentId {
    synth.id
}

#[unsafe(no_mangle)]
pub extern "C" fn synthetic_instrument_price_precision(synth: &SyntheticInstrument) -> u8 {
    synth.price_precision
}

#[unsafe(no_mangle)]
#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
pub extern "C" fn synthetic_instrument_price_increment(synth: &SyntheticInstrument) -> Price {
    synth.price_increment
}

#[unsafe(no_mangle)]
pub extern "C" fn synthetic_instrument_formula_to_cstr(
    synth: &SyntheticInstrument,
) -> *const c_char {
    str_to_cstr(&synth.formula)
}

#[unsafe(no_mangle)]
pub extern "C" fn synthetic_instrument_components_to_cstr(
    synth: &SyntheticInstrument,
) -> *const c_char {
    let components_vec = synth
        .components
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<String>>();

    string_vec_to_bytes(&components_vec)
}

#[unsafe(no_mangle)]
pub extern "C" fn synthetic_instrument_components_count(synth: &SyntheticInstrument) -> usize {
    synth.components.len()
}

#[unsafe(no_mangle)]
pub extern "C" fn synthetic_instrument_ts_event(synth: &SyntheticInstrument) -> UnixNanos {
    synth.ts_event
}

#[unsafe(no_mangle)]
pub extern "C" fn synthetic_instrument_ts_init(synth: &SyntheticInstrument) -> UnixNanos {
    synth.ts_init
}

/// # Safety
///
/// Assumes `formula_ptr` is a valid C string pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn synthetic_instrument_is_valid_formula(
    formula_ptr: *const c_char,
    components_ptr: *const c_char,
) -> u8 {
    if formula_ptr.is_null() || components_ptr.is_null() {
        return 0;
    }

    let components = unsafe { bytes_to_string_vec(components_ptr) }
        .into_iter()
        .map(InstrumentId::from)
        .collect::<Vec<InstrumentId>>();

    let formula = unsafe { cstr_as_str(formula_ptr) };

    u8::from(SyntheticInstrument::is_valid_formula_for_components(
        formula,
        &components,
    ))
}

/// # Safety
///
/// Assumes `formula_ptr` is a valid C string pointer.
///
/// # Panics
///
/// Panics if changing the formula fails (i.e., `unwrap()` in `change_formula`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn synthetic_instrument_change_formula(
    synth: &mut SyntheticInstrument,
    formula_ptr: *const c_char,
) {
    let formula = unsafe { cstr_as_str(formula_ptr) };
    synth.change_formula(formula).unwrap();
}

#[unsafe(no_mangle)]
#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
/// # Safety
///
/// `inputs_ptr` must describe initialized `f64` values that remain valid and immutable for the
/// duration of this call.
pub unsafe extern "C" fn synthetic_instrument_calculate(
    synth: &mut SyntheticInstrument,
    inputs_ptr: &CVec,
) -> Price {
    let inputs = unsafe { inputs_ptr.as_slice::<f64>() };

    match synth.calculate(inputs) {
        Ok(price) => price,
        Err(_) => ERROR_PRICE,
    }
}

#[cfg(test)]
mod cvec_tests {
    use rstest::rstest;

    use super::*;

    #[rstest]
    fn test_synthetic_calculate_borrows_inputs() {
        let mut synth = SyntheticInstrument::default();
        let mut inputs = vec![100.0, 200.0];
        let cvec = CVec {
            ptr: inputs.as_mut_ptr().cast(),
            len: inputs.len(),
            cap: inputs.capacity(),
        };

        let price = unsafe { synthetic_instrument_calculate(&mut synth, &cvec) };

        assert_eq!(price, Price::from("150.0"));
        assert_eq!(inputs, [100.0, 200.0]);
    }
}