nautilus_portfolio/
config.rs1use nautilus_common::config::{ConfigError, ConfigErrorCollector, ConfigResult};
17use nautilus_core::serialization::default_true;
18use serde::{Deserialize, Serialize};
19
20#[cfg_attr(
22 feature = "python",
23 pyo3::pyclass(module = "nautilus_trader.portfolio", from_py_object)
24)]
25#[cfg_attr(
26 feature = "python",
27 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.portfolio")
28)]
29#[cfg_attr(
30 feature = "python",
31 expect(
32 clippy::unsafe_derive_deserialize,
33 reason = "config deserializes plain fields; unsafe methods come from generated PyO3 integration"
34 )
35)]
36#[expect(
37 clippy::struct_excessive_bools,
38 reason = "config fields mirror the existing Python and serialization surface"
39)]
40#[derive(Debug, Clone, Copy, Serialize, Deserialize, bon::Builder)]
41#[builder(finish_fn(name = build_inner, vis = ""))]
42#[serde(deny_unknown_fields)]
43pub struct PortfolioConfig {
44 #[serde(default = "default_true")]
49 #[builder(default = true)]
50 pub use_mark_prices: bool,
51 #[serde(default)]
55 #[builder(default)]
56 pub use_mark_xrates: bool,
57 #[serde(default = "default_true")]
59 #[builder(default = true)]
60 pub bar_updates: bool,
61 #[serde(default = "default_true")]
64 #[builder(default = true)]
65 pub convert_to_account_base_currency: bool,
66 #[serde(default = "default_true")]
73 #[builder(default = true)]
74 pub equity_curve: bool,
75 #[serde(default)]
79 pub min_account_state_logging_interval_ms: Option<u64>,
80 #[serde(default)]
88 pub snapshot_interval_ms: Option<u64>,
89 #[serde(default)]
91 #[builder(default)]
92 pub debug: bool,
93}
94
95impl<S: portfolio_config_builder::IsComplete> PortfolioConfigBuilder<S> {
96 pub fn build(self) -> ConfigResult<PortfolioConfig> {
102 let config = self.build_inner();
103 config.validate()?;
104 Ok(config)
105 }
106}
107
108impl PortfolioConfig {
109 pub fn validate(&self) -> ConfigResult<()> {
116 let mut errors = ConfigErrorCollector::new();
117
118 for (field, value) in [
119 (
120 "min_account_state_logging_interval_ms",
121 self.min_account_state_logging_interval_ms,
122 ),
123 ("snapshot_interval_ms", self.snapshot_interval_ms),
124 ] {
125 if let Some(ms) = value {
126 errors.check(
127 ms > 0,
128 ConfigError::range(
129 field,
130 format!("must be a positive number of milliseconds, was {ms}"),
131 ),
132 );
133 }
134 }
135
136 errors.into_result()
137 }
138}
139
140impl Default for PortfolioConfig {
141 fn default() -> Self {
142 Self::builder()
143 .build()
144 .expect("default `PortfolioConfig` should be valid")
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use rstest::rstest;
151
152 use super::*;
153
154 #[rstest]
155 fn test_default_config_is_valid() {
156 assert!(PortfolioConfig::builder().build().is_ok());
157 }
158
159 #[rstest]
160 fn test_default_config_enables_daily_equity_curve_only() {
161 let config = PortfolioConfig::default();
162
163 assert!(config.equity_curve);
164 assert_eq!(config.snapshot_interval_ms, None);
165 }
166
167 #[rstest]
168 fn test_zero_min_account_state_logging_interval_rejected() {
169 let result = PortfolioConfig::builder()
170 .min_account_state_logging_interval_ms(0)
171 .build();
172 assert!(
173 matches!(result, Err(ConfigError::Range { field, .. }) if field == "min_account_state_logging_interval_ms")
174 );
175 }
176
177 #[rstest]
178 fn test_zero_snapshot_interval_rejected() {
179 let result = PortfolioConfig::builder().snapshot_interval_ms(0).build();
180 assert!(
181 matches!(result, Err(ConfigError::Range { field, .. }) if field == "snapshot_interval_ms")
182 );
183 }
184
185 #[rstest]
186 fn test_positive_intervals_accepted() {
187 let result = PortfolioConfig::builder()
188 .min_account_state_logging_interval_ms(1_000)
189 .snapshot_interval_ms(5_000)
190 .build();
191 assert!(result.is_ok());
192 }
193
194 #[rstest]
195 fn test_multiple_violations_collected() {
196 let result = PortfolioConfig::builder()
197 .min_account_state_logging_interval_ms(0)
198 .snapshot_interval_ms(0)
199 .build();
200 let ConfigError::Multiple { errors } = result.unwrap_err() else {
201 panic!("expected ConfigError::Multiple");
202 };
203 assert_eq!(errors.len(), 2);
204 }
205}