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(
24 module = "nautilus_trader.core.nautilus_pyo3.portfolio",
25 from_py_object
26 )
27)]
28#[cfg_attr(
29 feature = "python",
30 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.portfolio")
31)]
32#[cfg_attr(
33 feature = "python",
34 expect(
35 clippy::unsafe_derive_deserialize,
36 reason = "config deserializes plain fields; unsafe methods come from generated PyO3 integration"
37 )
38)]
39#[expect(
40 clippy::struct_excessive_bools,
41 reason = "config fields mirror the existing Python and serialization surface"
42)]
43#[derive(Debug, Clone, Copy, Serialize, Deserialize, bon::Builder)]
44#[builder(finish_fn(name = build_inner, vis = ""))]
45#[serde(deny_unknown_fields)]
46pub struct PortfolioConfig {
47 #[serde(default)]
52 #[builder(default)]
53 pub use_mark_prices: bool,
54 #[serde(default)]
58 #[builder(default)]
59 pub use_mark_xrates: bool,
60 #[serde(default = "default_true")]
62 #[builder(default = true)]
63 pub bar_updates: bool,
64 #[serde(default = "default_true")]
67 #[builder(default = true)]
68 pub convert_to_account_base_currency: bool,
69 #[serde(default)]
73 pub min_account_state_logging_interval_ms: Option<u64>,
74 #[serde(default)]
82 pub snapshot_interval_ms: Option<u64>,
83 #[serde(default)]
85 #[builder(default)]
86 pub debug: bool,
87}
88
89impl<S: portfolio_config_builder::IsComplete> PortfolioConfigBuilder<S> {
90 pub fn build(self) -> ConfigResult<PortfolioConfig> {
97 let config = self.build_inner();
98 config.validate()?;
99 Ok(config)
100 }
101}
102
103impl PortfolioConfig {
104 pub fn validate(&self) -> ConfigResult<()> {
111 let mut errors = ConfigErrorCollector::new();
112
113 for (field, value) in [
114 (
115 "min_account_state_logging_interval_ms",
116 self.min_account_state_logging_interval_ms,
117 ),
118 ("snapshot_interval_ms", self.snapshot_interval_ms),
119 ] {
120 if let Some(ms) = value {
121 errors.check(
122 ms > 0,
123 ConfigError::range(
124 field,
125 format!("must be a positive number of milliseconds, was {ms}"),
126 ),
127 );
128 }
129 }
130
131 errors.into_result()
132 }
133}
134
135impl Default for PortfolioConfig {
136 fn default() -> Self {
137 Self::builder()
138 .build()
139 .expect("default `PortfolioConfig` should be valid")
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use rstest::rstest;
146
147 use super::*;
148
149 #[rstest]
150 fn test_default_config_is_valid() {
151 assert!(PortfolioConfig::builder().build().is_ok());
152 }
153
154 #[rstest]
155 fn test_zero_min_account_state_logging_interval_rejected() {
156 let result = PortfolioConfig::builder()
157 .min_account_state_logging_interval_ms(0)
158 .build();
159 assert!(
160 matches!(result, Err(ConfigError::Range { field, .. }) if field == "min_account_state_logging_interval_ms")
161 );
162 }
163
164 #[rstest]
165 fn test_zero_snapshot_interval_rejected() {
166 let result = PortfolioConfig::builder().snapshot_interval_ms(0).build();
167 assert!(
168 matches!(result, Err(ConfigError::Range { field, .. }) if field == "snapshot_interval_ms")
169 );
170 }
171
172 #[rstest]
173 fn test_positive_intervals_accepted() {
174 let result = PortfolioConfig::builder()
175 .min_account_state_logging_interval_ms(1_000)
176 .snapshot_interval_ms(5_000)
177 .build();
178 assert!(result.is_ok());
179 }
180
181 #[rstest]
182 fn test_multiple_violations_collected() {
183 let result = PortfolioConfig::builder()
184 .min_account_state_logging_interval_ms(0)
185 .snapshot_interval_ms(0)
186 .build();
187 let ConfigError::Multiple { errors } = result.unwrap_err() else {
188 panic!("expected ConfigError::Multiple");
189 };
190 assert_eq!(errors.len(), 2);
191 }
192}