nautilus_model/instruments/
synthetic.rs1use std::{
17 collections::HashMap,
18 hash::{Hash, Hasher},
19};
20
21use derive_builder::Builder;
22use evalexpr::{ContextWithMutableVariables, DefaultNumericTypes, HashMapContext, Node, Value};
23use nautilus_core::{correctness::FAILED, UnixNanos};
24
25use crate::{
26 identifiers::{InstrumentId, Symbol, Venue},
27 types::Price,
28};
29
30#[derive(Clone, Debug, Builder)]
35#[cfg_attr(
36 feature = "python",
37 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
38)]
39pub struct SyntheticInstrument {
40 pub id: InstrumentId,
42 pub price_precision: u8,
44 pub price_increment: Price,
46 pub components: Vec<InstrumentId>,
48 pub formula: String,
50 pub ts_event: UnixNanos,
52 pub ts_init: UnixNanos,
54 context: HashMapContext,
55 variables: Vec<String>,
56 operator_tree: Node,
57}
58
59impl SyntheticInstrument {
60 pub fn new_checked(
66 symbol: Symbol,
67 price_precision: u8,
68 components: Vec<InstrumentId>,
69 formula: String,
70 ts_event: UnixNanos,
71 ts_init: UnixNanos,
72 ) -> anyhow::Result<Self> {
73 let price_increment = Price::new(10f64.powi(-i32::from(price_precision)), price_precision);
74
75 let variables: Vec<String> = components
77 .iter()
78 .map(std::string::ToString::to_string)
79 .collect();
80
81 let operator_tree = evalexpr::build_operator_tree(&formula)?;
82
83 Ok(Self {
84 id: InstrumentId::new(symbol, Venue::synthetic()),
85 price_precision,
86 price_increment,
87 components,
88 formula,
89 context: HashMapContext::new(),
90 variables,
91 operator_tree,
92 ts_event,
93 ts_init,
94 })
95 }
96
97 pub fn new(
99 symbol: Symbol,
100 price_precision: u8,
101 components: Vec<InstrumentId>,
102 formula: String,
103 ts_event: UnixNanos,
104 ts_init: UnixNanos,
105 ) -> Self {
106 Self::new_checked(
107 symbol,
108 price_precision,
109 components,
110 formula,
111 ts_event,
112 ts_init,
113 )
114 .expect(FAILED)
115 }
116
117 #[must_use]
118 pub fn is_valid_formula(&self, formula: &str) -> bool {
119 evalexpr::build_operator_tree::<DefaultNumericTypes>(formula).is_ok()
120 }
121
122 pub fn change_formula(&mut self, formula: String) -> anyhow::Result<()> {
123 let operator_tree = evalexpr::build_operator_tree::<DefaultNumericTypes>(&formula)?;
124 self.formula = formula;
125 self.operator_tree = operator_tree;
126 Ok(())
127 }
128
129 #[allow(dead_code)]
132 pub fn calculate_from_map(&mut self, inputs: &HashMap<String, f64>) -> anyhow::Result<Price> {
133 let mut input_values = Vec::new();
134
135 for variable in &self.variables {
136 if let Some(&value) = inputs.get(variable) {
137 input_values.push(value);
138 self.context
139 .set_value(variable.clone(), Value::Float(value))
140 .expect("TODO: Unable to set value");
141 } else {
142 panic!("Missing price for component: {variable}");
143 }
144 }
145
146 self.calculate(&input_values)
147 }
148
149 pub fn calculate(&mut self, inputs: &[f64]) -> anyhow::Result<Price> {
152 if inputs.len() != self.variables.len() {
153 return Err(anyhow::anyhow!("Invalid number of input values"));
154 }
155
156 for (variable, input) in self.variables.iter().zip(inputs) {
157 self.context
158 .set_value(variable.clone(), Value::Float(*input))?;
159 }
160
161 let result: Value = self.operator_tree.eval_with_context(&self.context)?;
162
163 match result {
164 Value::Float(price) => Ok(Price::new(price, self.price_precision)),
165 _ => Err(anyhow::anyhow!(
166 "Failed to evaluate formula to a floating point number"
167 )),
168 }
169 }
170}
171
172impl PartialEq<Self> for SyntheticInstrument {
173 fn eq(&self, other: &Self) -> bool {
174 self.id == other.id
175 }
176}
177
178impl Eq for SyntheticInstrument {}
179
180impl Hash for SyntheticInstrument {
181 fn hash<H: Hasher>(&self, state: &mut H) {
182 self.id.hash(state);
183 }
184}
185
186#[cfg(test)]
190mod tests {
191 use rstest::rstest;
192
193 use super::*;
194
195 #[rstest]
196 fn test_calculate_from_map() {
197 let mut synth = SyntheticInstrument::default();
198 let mut inputs = HashMap::new();
199 inputs.insert("BTC.BINANCE".to_string(), 100.0);
200 inputs.insert("LTC.BINANCE".to_string(), 200.0);
201 let price = synth.calculate_from_map(&inputs).unwrap();
202
203 assert_eq!(price.as_f64(), 150.0);
204 assert_eq!(
205 synth.formula,
206 "(BTC.BINANCE + LTC.BINANCE) / 2.0".to_string()
207 );
208 }
209
210 #[rstest]
211 fn test_calculate() {
212 let mut synth = SyntheticInstrument::default();
213 let inputs = vec![100.0, 200.0];
214 let price = synth.calculate(&inputs).unwrap();
215 assert_eq!(price.as_f64(), 150.0);
216 }
217
218 #[rstest]
219 fn test_change_formula() {
220 let mut synth = SyntheticInstrument::default();
221 let new_formula = "(BTC.BINANCE + LTC.BINANCE) / 4".to_string();
222 synth.change_formula(new_formula.clone()).unwrap();
223
224 let mut inputs = HashMap::new();
225 inputs.insert("BTC.BINANCE".to_string(), 100.0);
226 inputs.insert("LTC.BINANCE".to_string(), 200.0);
227 let price = synth.calculate_from_map(&inputs).unwrap();
228
229 assert_eq!(price.as_f64(), 75.0);
230 assert_eq!(synth.formula, new_formula);
231 }
232}