nautilus_backtest/modules/
mod.rs1pub mod cfd_swap;
19pub mod fx_rollover;
20
21use std::{
22 fmt::{Debug, Display},
23 rc::Rc,
24};
25
26use ahash::AHashMap;
27pub use cfd_swap::{CfdSwapModule, CfdSwapRate};
28pub use fx_rollover::FXRolloverInterestModule;
29use indexmap::IndexMap;
30use nautilus_common::cache::Cache;
31use nautilus_core::UnixNanos;
32use nautilus_execution::matching_engine::OrderMatchingEngine;
33use nautilus_model::{
34 data::Data,
35 identifiers::{InstrumentId, Venue},
36 instruments::InstrumentAny,
37 types::{Currency, Money},
38};
39
40#[derive(Debug)]
42pub struct ExchangeContext<'a> {
43 pub venue: Venue,
45 pub base_currency: Option<Currency>,
47 pub instruments: &'a AHashMap<InstrumentId, InstrumentAny>,
49 pub matching_engines: &'a IndexMap<InstrumentId, OrderMatchingEngine>,
51 pub cache: &'a Cache,
53}
54
55#[derive(Debug, Clone)]
56pub enum SimulationModuleAny {
57 CfdSwap(CfdSwapModule),
58 FXRolloverInterest(FXRolloverInterestModule),
59 #[cfg(feature = "python")]
60 Python(crate::python::modules::PythonSimulationModule),
61}
62
63impl SimulationModule for SimulationModuleAny {
64 fn pre_process(&self, data: &Data) -> anyhow::Result<()> {
65 match self {
66 Self::CfdSwap(module) => module.pre_process(data),
67 Self::FXRolloverInterest(module) => module.pre_process(data),
68 #[cfg(feature = "python")]
69 Self::Python(module) => module.pre_process(data),
70 }
71 }
72
73 fn process(
74 &self,
75 ts_now: UnixNanos,
76 ctx: &ExchangeContext,
77 ) -> anyhow::Result<SimulationModuleResult> {
78 match self {
79 Self::CfdSwap(module) => module.process(ts_now, ctx),
80 Self::FXRolloverInterest(module) => module.process(ts_now, ctx),
81 #[cfg(feature = "python")]
82 Self::Python(module) => module.process(ts_now, ctx),
83 }
84 }
85
86 fn acknowledge(&self, outcomes: &[AccountAdjustmentOutcome]) -> anyhow::Result<()> {
87 match self {
88 Self::CfdSwap(module) => module.acknowledge(outcomes),
89 Self::FXRolloverInterest(module) => module.acknowledge(outcomes),
90 #[cfg(feature = "python")]
91 Self::Python(module) => module.acknowledge(outcomes),
92 }
93 }
94
95 fn log_diagnostics(&self) -> anyhow::Result<()> {
96 match self {
97 Self::CfdSwap(module) => module.log_diagnostics(),
98 Self::FXRolloverInterest(module) => module.log_diagnostics(),
99 #[cfg(feature = "python")]
100 Self::Python(module) => module.log_diagnostics(),
101 }
102 }
103
104 fn reset(&self) -> anyhow::Result<()> {
105 match self {
106 Self::CfdSwap(module) => module.reset(),
107 Self::FXRolloverInterest(module) => module.reset(),
108 #[cfg(feature = "python")]
109 Self::Python(module) => module.reset(),
110 }
111 }
112}
113
114#[derive(Clone)]
119pub struct SimulationModuleHandle(Rc<dyn SimulationModule>);
120
121impl SimulationModuleHandle {
122 #[must_use]
124 pub fn new<T>(module: T) -> Self
125 where
126 T: SimulationModule + 'static,
127 {
128 Self(Rc::new(module))
129 }
130
131 #[must_use]
133 pub fn from_rc(module: Rc<dyn SimulationModule>) -> Self {
134 Self(module)
135 }
136}
137
138impl Debug for SimulationModuleHandle {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_tuple(stringify!(SimulationModuleHandle))
141 .field(&"<dyn SimulationModule>")
142 .finish()
143 }
144}
145
146impl SimulationModule for SimulationModuleHandle {
147 fn pre_process(&self, data: &Data) -> anyhow::Result<()> {
148 self.0.pre_process(data)
149 }
150
151 fn process(
152 &self,
153 ts_now: UnixNanos,
154 ctx: &ExchangeContext,
155 ) -> anyhow::Result<SimulationModuleResult> {
156 self.0.process(ts_now, ctx)
157 }
158
159 fn acknowledge(&self, outcomes: &[AccountAdjustmentOutcome]) -> anyhow::Result<()> {
160 self.0.acknowledge(outcomes)
161 }
162
163 fn log_diagnostics(&self) -> anyhow::Result<()> {
164 self.0.log_diagnostics()
165 }
166
167 fn reset(&self) -> anyhow::Result<()> {
168 self.0.reset()
169 }
170}
171
172impl From<SimulationModuleAny> for SimulationModuleHandle {
173 fn from(module: SimulationModuleAny) -> Self {
174 Self::new(module)
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum SimulationModuleResult {
181 NotReady,
183 Completed(Vec<Money>),
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum AccountAdjustmentError {
190 TotalOverflow(Currency),
192 FreeBalanceOverflow(Currency),
194 MissingBalance(Currency),
196 MissingAccount(Venue),
198 AccountStateGeneration(String),
200}
201
202impl Display for AccountAdjustmentError {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 match self {
205 Self::TotalOverflow(currency) => {
206 write!(
207 f,
208 "Cannot adjust account: {currency} total exceeds Money bounds"
209 )
210 }
211 Self::FreeBalanceOverflow(currency) => write!(
212 f,
213 "Cannot adjust account: {currency} free balance exceeds Money bounds"
214 ),
215 Self::MissingBalance(currency) => {
216 write!(
217 f,
218 "Cannot adjust account: no balance for currency {currency}"
219 )
220 }
221 Self::MissingAccount(venue) => {
222 write!(f, "Cannot adjust account: no account for venue {venue}")
223 }
224 Self::AccountStateGeneration(error) => {
225 write!(
226 f,
227 "Cannot adjust account: failed to generate account state: {error}"
228 )
229 }
230 }
231 }
232}
233
234impl std::error::Error for AccountAdjustmentError {}
235
236impl AccountAdjustmentError {
237 pub(crate) const fn is_retryable(&self) -> bool {
238 matches!(
239 self,
240 Self::TotalOverflow(_) | Self::FreeBalanceOverflow(_) | Self::AccountStateGeneration(_)
241 )
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum AccountAdjustmentOutcome {
248 Applied,
250 Failed(AccountAdjustmentError),
252}
253
254pub trait SimulationModule {
263 fn pre_process(&self, data: &Data) -> anyhow::Result<()>;
269
270 fn process(
279 &self,
280 ts_now: UnixNanos,
281 ctx: &ExchangeContext,
282 ) -> anyhow::Result<SimulationModuleResult>;
283
284 fn acknowledge(&self, outcomes: &[AccountAdjustmentOutcome]) -> anyhow::Result<()>;
295
296 fn log_diagnostics(&self) -> anyhow::Result<()>;
302
303 fn reset(&self) -> anyhow::Result<()>;
309}
310
311#[cfg(test)]
312mod tests {
313 use std::{cell::Cell, rc::Rc};
314
315 use rstest::rstest;
316
317 use super::*;
318
319 #[derive(Debug)]
320 struct CountingModule {
321 resets: Rc<Cell<u32>>,
322 }
323
324 impl SimulationModule for CountingModule {
325 fn pre_process(&self, _data: &Data) -> anyhow::Result<()> {
326 Ok(())
327 }
328
329 fn process(
330 &self,
331 _ts_now: UnixNanos,
332 _ctx: &ExchangeContext,
333 ) -> anyhow::Result<SimulationModuleResult> {
334 Ok(SimulationModuleResult::NotReady)
335 }
336
337 fn acknowledge(&self, _outcomes: &[AccountAdjustmentOutcome]) -> anyhow::Result<()> {
338 Ok(())
339 }
340
341 fn log_diagnostics(&self) -> anyhow::Result<()> {
342 Ok(())
343 }
344
345 fn reset(&self) -> anyhow::Result<()> {
346 self.resets.set(self.resets.get() + 1);
347 Ok(())
348 }
349 }
350
351 #[rstest]
352 fn simulation_module_handle_from_rc_clones_shared_module() {
353 let resets = Rc::new(Cell::new(0));
354 let module: Rc<dyn SimulationModule> = Rc::new(CountingModule {
355 resets: resets.clone(),
356 });
357 let handle = SimulationModuleHandle::from_rc(module);
358 let cloned = handle.clone();
359
360 handle.reset().unwrap();
361 cloned.reset().unwrap();
362
363 assert_eq!(resets.get(), 2);
364 assert_eq!(
365 format!("{handle:?}"),
366 "SimulationModuleHandle(\"<dyn SimulationModule>\")"
367 );
368 }
369}