Skip to main content

nautilus_model/ffi/instruments/
synthetic.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 std::ffi::c_char;
17
18use nautilus_core::{
19    UnixNanos,
20    ffi::{
21        abort_on_panic,
22        cvec::CVec,
23        parsing::{bytes_to_string_vec, string_vec_to_bytes},
24        string::{cstr_as_str, str_to_cstr},
25    },
26};
27
28use crate::{
29    identifiers::{InstrumentId, Symbol},
30    instruments::synthetic::SyntheticInstrument,
31    types::{ERROR_PRICE, Price},
32};
33
34/// Creates a new [`SyntheticInstrument`] from the given components and formula.
35///
36/// # Panics
37///
38/// Panics if the formula is invalid for the given components.
39///
40/// # Safety
41///
42/// This function assumes:
43/// - `components_ptr` is a valid C string pointer of a JSON format list of strings.
44/// - `formula_ptr` is a valid C string pointer.
45///
46/// Returns an owning pointer to the heap-allocated `SyntheticInstrument` which the
47/// caller must eventually pass to [`synthetic_instrument_drop`].
48#[unsafe(no_mangle)]
49pub unsafe extern "C" fn synthetic_instrument_new(
50    symbol: Symbol,
51    price_precision: u8,
52    components_ptr: *const c_char,
53    formula_ptr: *const c_char,
54    ts_event: u64,
55    ts_init: u64,
56) -> *mut SyntheticInstrument {
57    // TODO: There is absolutely no error handling here yet
58    let components = unsafe { bytes_to_string_vec(components_ptr) }
59        .into_iter()
60        .map(InstrumentId::from)
61        .collect::<Vec<InstrumentId>>();
62    let formula = unsafe { cstr_as_str(formula_ptr) };
63    let synth = SyntheticInstrument::builder()
64        .symbol(symbol)
65        .price_precision(price_precision)
66        .components(components)
67        .formula(formula)
68        .ts_event(ts_event.into())
69        .ts_init(ts_init.into())
70        .build()
71        .unwrap();
72
73    Box::into_raw(Box::new(synth))
74}
75
76/// # Safety
77///
78/// `synth` must be a live owning pointer returned by [`synthetic_instrument_new`],
79/// and must not be used after this call.
80///
81/// # Panics
82///
83/// Panics if `synth` is null.
84#[unsafe(no_mangle)]
85pub unsafe extern "C" fn synthetic_instrument_drop(synth: *mut SyntheticInstrument) {
86    abort_on_panic(|| {
87        assert!(!synth.is_null(), "`synth` was NULL");
88        // SAFETY: Caller guarantees `synth` was allocated by `synthetic_instrument_new`
89        drop(unsafe { Box::from_raw(synth) }); // Memory freed here
90    });
91}
92
93#[unsafe(no_mangle)]
94pub extern "C" fn synthetic_instrument_id(synth: &SyntheticInstrument) -> InstrumentId {
95    synth.id
96}
97
98#[unsafe(no_mangle)]
99pub extern "C" fn synthetic_instrument_price_precision(synth: &SyntheticInstrument) -> u8 {
100    synth.price_precision
101}
102
103#[unsafe(no_mangle)]
104#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
105pub extern "C" fn synthetic_instrument_price_increment(synth: &SyntheticInstrument) -> Price {
106    synth.price_increment
107}
108
109#[unsafe(no_mangle)]
110pub extern "C" fn synthetic_instrument_formula_to_cstr(
111    synth: &SyntheticInstrument,
112) -> *const c_char {
113    str_to_cstr(&synth.formula)
114}
115
116#[unsafe(no_mangle)]
117pub extern "C" fn synthetic_instrument_components_to_cstr(
118    synth: &SyntheticInstrument,
119) -> *const c_char {
120    let components_vec = synth
121        .components
122        .iter()
123        .map(ToString::to_string)
124        .collect::<Vec<String>>();
125
126    string_vec_to_bytes(&components_vec)
127}
128
129#[unsafe(no_mangle)]
130pub extern "C" fn synthetic_instrument_components_count(synth: &SyntheticInstrument) -> usize {
131    synth.components.len()
132}
133
134#[unsafe(no_mangle)]
135pub extern "C" fn synthetic_instrument_ts_event(synth: &SyntheticInstrument) -> UnixNanos {
136    synth.ts_event
137}
138
139#[unsafe(no_mangle)]
140pub extern "C" fn synthetic_instrument_ts_init(synth: &SyntheticInstrument) -> UnixNanos {
141    synth.ts_init
142}
143
144/// # Safety
145///
146/// Assumes `formula_ptr` is a valid C string pointer.
147#[unsafe(no_mangle)]
148pub unsafe extern "C" fn synthetic_instrument_is_valid_formula(
149    formula_ptr: *const c_char,
150    components_ptr: *const c_char,
151) -> u8 {
152    if formula_ptr.is_null() || components_ptr.is_null() {
153        return 0;
154    }
155
156    let components = unsafe { bytes_to_string_vec(components_ptr) }
157        .into_iter()
158        .map(InstrumentId::from)
159        .collect::<Vec<InstrumentId>>();
160
161    let formula = unsafe { cstr_as_str(formula_ptr) };
162
163    u8::from(SyntheticInstrument::is_valid_formula_for_components(
164        formula,
165        &components,
166    ))
167}
168
169/// # Safety
170///
171/// Assumes `formula_ptr` is a valid C string pointer.
172///
173/// # Panics
174///
175/// Panics if changing the formula fails (i.e., `unwrap()` in `change_formula`).
176#[unsafe(no_mangle)]
177pub unsafe extern "C" fn synthetic_instrument_change_formula(
178    synth: &mut SyntheticInstrument,
179    formula_ptr: *const c_char,
180) {
181    let formula = unsafe { cstr_as_str(formula_ptr) };
182    synth.change_formula(formula).unwrap();
183}
184
185#[unsafe(no_mangle)]
186#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
187/// # Safety
188///
189/// `inputs_ptr` must describe initialized `f64` values that remain valid and immutable for the
190/// duration of this call.
191pub unsafe extern "C" fn synthetic_instrument_calculate(
192    synth: &mut SyntheticInstrument,
193    inputs_ptr: &CVec,
194) -> Price {
195    let inputs = unsafe { inputs_ptr.as_slice::<f64>() };
196
197    match synth.calculate(inputs) {
198        Ok(price) => price,
199        Err(_) => ERROR_PRICE,
200    }
201}
202
203#[cfg(test)]
204mod cvec_tests {
205    use rstest::rstest;
206
207    use super::*;
208
209    #[rstest]
210    fn test_synthetic_calculate_borrows_inputs() {
211        let mut synth = SyntheticInstrument::default();
212        let mut inputs = vec![100.0, 200.0];
213        let cvec = CVec {
214            ptr: inputs.as_mut_ptr().cast(),
215            len: inputs.len(),
216            cap: inputs.capacity(),
217        };
218
219        let price = unsafe { synthetic_instrument_calculate(&mut synth, &cvec) };
220
221        assert_eq!(price, Price::from("150.0"));
222        assert_eq!(inputs, [100.0, 200.0]);
223    }
224}