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}