Skip to main content

nautilus_backtest/modules/
mod.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
16//! Simulation module trait for extending backtesting with custom venue behaviors.
17
18pub mod fx_rollover;
19
20use std::fmt::Display;
21
22use ahash::AHashMap;
23pub use fx_rollover::FXRolloverInterestModule;
24use indexmap::IndexMap;
25use nautilus_common::cache::Cache;
26use nautilus_core::UnixNanos;
27use nautilus_execution::matching_engine::OrderMatchingEngine;
28use nautilus_model::{
29    data::Data,
30    identifiers::{InstrumentId, Venue},
31    instruments::InstrumentAny,
32    types::{Currency, Money},
33};
34
35/// Read-only view of exchange state passed to simulation modules during processing.
36#[derive(Debug)]
37pub struct ExchangeContext<'a> {
38    /// The venue identifier.
39    pub venue: Venue,
40    /// The optional base currency for single-currency accounts.
41    pub base_currency: Option<Currency>,
42    /// All instruments registered on the exchange.
43    pub instruments: &'a AHashMap<InstrumentId, InstrumentAny>,
44    /// All matching engines, providing order book access.
45    pub matching_engines: &'a IndexMap<InstrumentId, OrderMatchingEngine>,
46    /// Read-only cache access for querying positions and other state.
47    pub cache: &'a Cache,
48}
49
50#[derive(Debug, Clone)]
51pub enum SimulationModuleAny {
52    FXRolloverInterest(FXRolloverInterestModule),
53}
54
55impl SimulationModule for SimulationModuleAny {
56    fn pre_process(&self, data: &Data) {
57        match self {
58            Self::FXRolloverInterest(module) => module.pre_process(data),
59        }
60    }
61
62    fn process(&self, ts_now: UnixNanos, ctx: &ExchangeContext) -> SimulationModuleResult {
63        match self {
64            Self::FXRolloverInterest(module) => module.process(ts_now, ctx),
65        }
66    }
67
68    fn acknowledge(&self, outcomes: &[AccountAdjustmentOutcome]) {
69        match self {
70            Self::FXRolloverInterest(module) => module.acknowledge(outcomes),
71        }
72    }
73
74    fn log_diagnostics(&self) {
75        match self {
76            Self::FXRolloverInterest(module) => module.log_diagnostics(),
77        }
78    }
79
80    fn reset(&self) {
81        match self {
82            Self::FXRolloverInterest(module) => module.reset(),
83        }
84    }
85}
86
87impl From<SimulationModuleAny> for Box<dyn SimulationModule> {
88    fn from(value: SimulationModuleAny) -> Self {
89        match value {
90            SimulationModuleAny::FXRolloverInterest(module) => Box::new(module),
91        }
92    }
93}
94
95/// Result of processing a simulation module.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum SimulationModuleResult {
98    /// The module does not yet have a complete batch of adjustments.
99    NotReady,
100    /// The module produced a complete batch, which may be empty.
101    Completed(Vec<Money>),
102}
103
104/// Failure applying an account adjustment produced by a simulation module.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum AccountAdjustmentError {
107    /// The adjusted total balance would exceed [`Money`] bounds.
108    TotalOverflow(Currency),
109    /// The adjusted free balance would exceed [`Money`] bounds.
110    FreeBalanceOverflow(Currency),
111    /// The account has no balance for the adjustment currency.
112    MissingBalance(Currency),
113    /// The exchange has no account for the venue.
114    MissingAccount(Venue),
115    /// Generating the updated account state failed.
116    AccountStateGeneration(String),
117}
118
119impl Display for AccountAdjustmentError {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            Self::TotalOverflow(currency) => {
123                write!(
124                    f,
125                    "Cannot adjust account: {currency} total exceeds Money bounds"
126                )
127            }
128            Self::FreeBalanceOverflow(currency) => write!(
129                f,
130                "Cannot adjust account: {currency} free balance exceeds Money bounds"
131            ),
132            Self::MissingBalance(currency) => {
133                write!(
134                    f,
135                    "Cannot adjust account: no balance for currency {currency}"
136                )
137            }
138            Self::MissingAccount(venue) => {
139                write!(f, "Cannot adjust account: no account for venue {venue}")
140            }
141            Self::AccountStateGeneration(error) => {
142                write!(
143                    f,
144                    "Cannot adjust account: failed to generate account state: {error}"
145                )
146            }
147        }
148    }
149}
150
151impl std::error::Error for AccountAdjustmentError {}
152
153/// Outcome of applying an account adjustment produced by a simulation module.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum AccountAdjustmentOutcome {
156    /// The adjustment was applied successfully.
157    Applied,
158    /// The adjustment could not be applied.
159    Failed(AccountAdjustmentError),
160}
161
162/// Trait for custom simulation modules that extend backtesting functionality.
163///
164/// Implementations can add specialized behavior such as rollover interest,
165/// market makers, price impact models, or other venue-specific simulation
166/// logic that runs alongside the core backtesting engine.
167///
168/// Modules use interior mutability (`Cell`/`RefCell`) for state since they
169/// are stored inside `SimulatedExchange` and invoked through shared references.
170pub trait SimulationModule {
171    /// Pre-processes market data before matching engine processing.
172    fn pre_process(&self, data: &Data);
173
174    /// Processes simulation logic at the given timestamp.
175    ///
176    /// Returns a complete batch of account balance adjustments, or indicates
177    /// that the module is not ready.
178    fn process(&self, ts_now: UnixNanos, ctx: &ExchangeContext) -> SimulationModuleResult;
179
180    /// Acknowledges the ordered application outcomes for a completed batch.
181    ///
182    /// This is called exactly once for every [`SimulationModuleResult::Completed`],
183    /// including an empty batch.
184    ///
185    /// # Panics
186    ///
187    /// Implementations may panic if the outcome count does not match the
188    /// completed batch or if no completed batch is awaiting acknowledgement.
189    fn acknowledge(&self, outcomes: &[AccountAdjustmentOutcome]);
190
191    /// Logs diagnostic information about the module's state.
192    fn log_diagnostics(&self);
193
194    /// Resets the module to its initial state.
195    fn reset(&self);
196}