Skip to main content

backtest_server/
config.rs

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