Skip to main content

backtest_server/
config.rs

1//! Server configuration loaded from TOML.
2
3use qs_service::ServiceEndpoint;
4use serde::Deserialize;
5
6/// Top-level server configuration.
7#[derive(Debug, Deserialize)]
8pub struct ServerConfig {
9    pub server: ServerSection,
10    pub database: DatabaseSection,
11    pub symbols: SymbolsSection,
12    #[serde(default)]
13    pub instruments: InstrumentsSection,
14    pub profiles: ProfilesSection,
15    #[serde(default)]
16    pub jobs: JobsSection,
17    #[serde(default)]
18    pub artifacts: ArtifactsSection,
19    #[serde(default)]
20    pub logging: LoggingSection,
21    #[serde(default)]
22    pub strategies: StrategiesSection,
23}
24
25/// Service listener and transport settings.
26#[derive(Debug, Deserialize)]
27pub struct ServerSection {
28    /// Provider-neutral listener endpoint. Defaults to the legacy `shm_name`.
29    #[serde(default)]
30    pub endpoint: Option<ServiceEndpoint>,
31    /// Deprecated compatibility name for the shared-memory endpoint.
32    #[serde(default)]
33    pub shm_name: String,
34    /// Per-client buffer or maximum frame size. Default: 16 MB.
35    #[serde(default = "default_shm_buffer")]
36    pub shm_buffer_size: usize,
37    #[serde(default = "default_max_connections")]
38    pub max_connections: usize,
39    #[serde(default)]
40    pub allow_insecure_non_loopback: bool,
41}
42
43impl ServerSection {
44    pub fn resolved_endpoint(&self) -> crate::error::Result<ServiceEndpoint> {
45        let legacy = if self.shm_name.is_empty() {
46            None
47        } else {
48            Some(
49                ServiceEndpoint::shared_memory(self.shm_name.clone()).map_err(|error| {
50                    crate::error::BacktestServerError::Config(error.to_string())
51                })?,
52            )
53        };
54        match (&self.endpoint, legacy) {
55            (Some(endpoint), Some(legacy)) if endpoint != &legacy => {
56                Err(crate::error::BacktestServerError::Config(format!(
57                    "conflicting server.endpoint '{}' and deprecated server.shm_name '{}'",
58                    endpoint, self.shm_name
59                )))
60            }
61            (Some(endpoint), _) => Ok(endpoint.clone()),
62            (None, Some(endpoint)) => Ok(endpoint),
63            (None, None) => Err(crate::error::BacktestServerError::Config(
64                "server.endpoint is required (or provide deprecated server.shm_name)".to_string(),
65            )),
66        }
67    }
68}
69
70/// Path to the Parquet data store root directory.
71#[derive(Debug, Deserialize)]
72pub struct DatabaseSection {
73    /// Root directory for Parquet-partitioned market data.
74    pub data_dir: String,
75}
76
77/// Path to the symbol registry TOML.
78#[derive(Debug, Deserialize)]
79pub struct SymbolsSection {
80    pub registry_path: String,
81}
82
83/// Instrument catalog and stored-series identity settings.
84#[derive(Debug, Deserialize)]
85pub struct InstrumentsSection {
86    /// Optional strict instrument catalog document. The symbol registry is adapted when omitted.
87    #[serde(default)]
88    pub catalog_path: Option<String>,
89    /// Optional broker-, exchange-, or repository-owned alias-resolution default.
90    #[serde(default)]
91    pub default_listing_venue: Option<String>,
92    /// Identity of the historical market-data source bound to current partitions.
93    #[serde(default = "default_market_data_source")]
94    pub market_data_source: String,
95}
96
97impl Default for InstrumentsSection {
98    fn default() -> Self {
99        Self {
100            catalog_path: None,
101            default_listing_venue: None,
102            market_data_source: default_market_data_source(),
103        }
104    }
105}
106
107/// Path to the management profiles TOML.
108#[derive(Debug, Deserialize)]
109pub struct ProfilesSection {
110    pub profiles_path: String,
111}
112
113/// Async job retention and cleanup settings.
114#[derive(Debug, Deserialize)]
115pub struct JobsSection {
116    /// Retain completed, failed, and cancelled jobs for this many seconds.
117    #[serde(default = "default_job_retention_secs")]
118    pub retention_secs: u64,
119    /// Run terminal-job cleanup at this interval.
120    #[serde(default = "default_job_cleanup_interval_secs")]
121    pub cleanup_interval_secs: u64,
122    /// Maximum queued, running, and retained terminal jobs.
123    #[serde(default = "default_max_retained_jobs")]
124    pub max_retained_jobs: usize,
125}
126
127impl Default for JobsSection {
128    fn default() -> Self {
129        Self {
130            retention_secs: default_job_retention_secs(),
131            cleanup_interval_secs: default_job_cleanup_interval_secs(),
132            max_retained_jobs: default_max_retained_jobs(),
133        }
134    }
135}
136
137/// Limits for configured strategy runs and server-side parameter searches.
138#[derive(Debug, Clone, Deserialize)]
139pub struct StrategiesSection {
140    /// Largest accepted strategy, template, or space document, measured as its JSON encoding in bytes.
141    #[serde(default = "default_max_document_bytes")]
142    pub max_document_bytes: usize,
143    /// Largest total retained history one configured run may bind across all of its sources, in bars.
144    #[serde(default = "default_max_retained_bars")]
145    pub max_retained_bars: usize,
146    /// Most worker threads one search may use; a request asking for more is capped here.
147    #[serde(default = "default_max_search_workers")]
148    pub max_search_workers: usize,
149    /// Most pre-constraint parameter combinations one search may enumerate.
150    #[serde(default = "default_max_search_generation_points")]
151    pub max_search_generation_points: usize,
152    /// Most runs, counted as points times symbols times windows, one search may schedule.
153    #[serde(default = "default_max_search_runs")]
154    pub max_search_runs: usize,
155    /// Most configured strategy instances one portfolio run may replay together; `max_retained_bars` bounds their combined retained history.
156    #[serde(default = "default_max_portfolio_instances")]
157    pub max_portfolio_instances: usize,
158    #[serde(default = "default_max_calendar_sessions")]
159    pub max_calendar_sessions: usize,
160    #[serde(default = "default_max_calendar_intervals")]
161    pub max_calendar_intervals: usize,
162    #[serde(default = "default_max_calendar_exceptions")]
163    pub max_calendar_exceptions: usize,
164    #[serde(default = "default_max_calendar_history")]
165    pub max_calendar_history: usize,
166    #[serde(default = "default_max_calendar_children")]
167    pub max_calendar_children: usize,
168    #[serde(default = "default_max_calendar_bytes")]
169    pub max_calendar_bytes: usize,
170}
171
172impl Default for StrategiesSection {
173    fn default() -> Self {
174        Self {
175            max_document_bytes: default_max_document_bytes(),
176            max_retained_bars: default_max_retained_bars(),
177            max_search_workers: default_max_search_workers(),
178            max_search_generation_points: default_max_search_generation_points(),
179            max_search_runs: default_max_search_runs(),
180            max_portfolio_instances: default_max_portfolio_instances(),
181            max_calendar_sessions: default_max_calendar_sessions(),
182            max_calendar_intervals: default_max_calendar_intervals(),
183            max_calendar_exceptions: default_max_calendar_exceptions(),
184            max_calendar_history: default_max_calendar_history(),
185            max_calendar_children: default_max_calendar_children(),
186            max_calendar_bytes: default_max_calendar_bytes(),
187        }
188    }
189}
190
191fn default_max_document_bytes() -> usize {
192    256 * 1024
193}
194
195fn default_max_retained_bars() -> usize {
196    200_000
197}
198
199fn default_max_search_workers() -> usize {
200    4
201}
202
203fn default_max_search_generation_points() -> usize {
204    100_000
205}
206
207fn default_max_search_runs() -> usize {
208    20_000
209}
210
211fn default_max_portfolio_instances() -> usize {
212    16
213}
214
215fn default_max_calendar_sessions() -> usize {
216    32
217}
218
219fn default_max_calendar_intervals() -> usize {
220    64
221}
222
223fn default_max_calendar_exceptions() -> usize {
224    366
225}
226
227fn default_max_calendar_history() -> usize {
228    512
229}
230
231fn default_max_calendar_children() -> usize {
232    1_000_000
233}
234
235fn default_max_calendar_bytes() -> usize {
236    64 * 1024 * 1024
237}
238
239/// Large-result artifact storage settings.
240#[derive(Debug, Deserialize)]
241pub struct ArtifactsSection {
242    /// Directory containing result artifact JSON files.
243    #[serde(default = "default_artifact_directory")]
244    pub directory: String,
245    /// Largest result JSON payload that can be returned inline.
246    #[serde(default = "default_artifact_inline_limit_bytes")]
247    pub inline_limit_bytes: usize,
248    /// Raw bytes returned by each artifact chunk RPC.
249    #[serde(default = "default_artifact_chunk_size")]
250    pub chunk_size: usize,
251    /// Retain artifacts for this many seconds.
252    #[serde(default = "default_artifact_retention_secs")]
253    pub retention_secs: u64,
254    /// Maximum total bytes retained by the artifact store.
255    #[serde(default = "default_artifact_max_total_bytes")]
256    pub max_total_bytes: u64,
257}
258
259impl Default for ArtifactsSection {
260    fn default() -> Self {
261        Self {
262            directory: default_artifact_directory(),
263            inline_limit_bytes: default_artifact_inline_limit_bytes(),
264            chunk_size: default_artifact_chunk_size(),
265            retention_secs: default_artifact_retention_secs(),
266            max_total_bytes: default_artifact_max_total_bytes(),
267        }
268    }
269}
270
271/// Logging configuration with sensible defaults.
272#[derive(Debug, Deserialize)]
273pub struct LoggingSection {
274    #[serde(default = "default_log_level")]
275    pub level: String,
276}
277
278impl Default for LoggingSection {
279    fn default() -> Self {
280        Self {
281            level: default_log_level(),
282        }
283    }
284}
285
286fn default_market_data_source() -> String {
287    "local-parquet".into()
288}
289
290fn default_shm_buffer() -> usize {
291    16 * 1024 * 1024
292}
293
294fn default_max_connections() -> usize {
295    256
296}
297
298fn default_job_retention_secs() -> u64 {
299    3_600
300}
301
302fn default_job_cleanup_interval_secs() -> u64 {
303    60
304}
305
306fn default_max_retained_jobs() -> usize {
307    1_000
308}
309
310fn default_artifact_directory() -> String {
311    "backtest-artifacts".into()
312}
313
314fn default_artifact_inline_limit_bytes() -> usize {
315    12 * 1024 * 1024
316}
317
318fn default_artifact_chunk_size() -> usize {
319    1024 * 1024
320}
321
322fn default_artifact_retention_secs() -> u64 {
323    3_600
324}
325
326fn default_artifact_max_total_bytes() -> u64 {
327    1024 * 1024 * 1024
328}
329
330fn default_log_level() -> String {
331    "info".into()
332}
333
334/// Load and parse a TOML config file into `ServerConfig`.
335pub fn load_config(path: &str) -> crate::error::Result<ServerConfig> {
336    let content = std::fs::read_to_string(path)
337        .map_err(|e| crate::error::BacktestServerError::Config(format!("{path}: {e}")))?;
338    toml::from_str(&content)
339        .map_err(|e| crate::error::BacktestServerError::Config(format!("{path}: {e}")))
340}