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}
22
23/// Service listener and transport settings.
24#[derive(Debug, Deserialize)]
25pub struct ServerSection {
26    /// Provider-neutral listener endpoint. Defaults to the legacy `shm_name`.
27    #[serde(default)]
28    pub endpoint: Option<ServiceEndpoint>,
29    /// Deprecated compatibility name for the shared-memory endpoint.
30    #[serde(default)]
31    pub shm_name: String,
32    /// Per-client buffer or maximum frame size. Default: 16 MB.
33    #[serde(default = "default_shm_buffer")]
34    pub shm_buffer_size: usize,
35    #[serde(default = "default_max_connections")]
36    pub max_connections: usize,
37    #[serde(default)]
38    pub allow_insecure_non_loopback: bool,
39}
40
41impl ServerSection {
42    pub fn resolved_endpoint(&self) -> crate::error::Result<ServiceEndpoint> {
43        let legacy = if self.shm_name.is_empty() {
44            None
45        } else {
46            Some(
47                ServiceEndpoint::shared_memory(self.shm_name.clone()).map_err(|error| {
48                    crate::error::BacktestServerError::Config(error.to_string())
49                })?,
50            )
51        };
52        match (&self.endpoint, legacy) {
53            (Some(endpoint), Some(legacy)) if endpoint != &legacy => {
54                Err(crate::error::BacktestServerError::Config(format!(
55                    "conflicting server.endpoint '{}' and deprecated server.shm_name '{}'",
56                    endpoint, self.shm_name
57                )))
58            }
59            (Some(endpoint), _) => Ok(endpoint.clone()),
60            (None, Some(endpoint)) => Ok(endpoint),
61            (None, None) => Err(crate::error::BacktestServerError::Config(
62                "server.endpoint is required (or provide deprecated server.shm_name)".to_string(),
63            )),
64        }
65    }
66}
67
68/// Path to the Parquet data store root directory.
69#[derive(Debug, Deserialize)]
70pub struct DatabaseSection {
71    /// Root directory for Parquet-partitioned market data.
72    pub data_dir: String,
73}
74
75/// Path to the symbol registry TOML.
76#[derive(Debug, Deserialize)]
77pub struct SymbolsSection {
78    pub registry_path: String,
79}
80
81/// Instrument catalog and stored-series identity settings.
82#[derive(Debug, Deserialize)]
83pub struct InstrumentsSection {
84    /// Optional strict instrument catalog document. The symbol registry is adapted when omitted.
85    #[serde(default)]
86    pub catalog_path: Option<String>,
87    /// Optional broker-, exchange-, or repository-owned alias-resolution default.
88    #[serde(default)]
89    pub default_listing_venue: Option<String>,
90    /// Identity of the historical market-data source bound to current partitions.
91    #[serde(default = "default_market_data_source")]
92    pub market_data_source: String,
93}
94
95impl Default for InstrumentsSection {
96    fn default() -> Self {
97        Self {
98            catalog_path: None,
99            default_listing_venue: None,
100            market_data_source: default_market_data_source(),
101        }
102    }
103}
104
105/// Path to the management profiles TOML.
106#[derive(Debug, Deserialize)]
107pub struct ProfilesSection {
108    pub profiles_path: String,
109}
110
111/// Async job retention and cleanup settings.
112#[derive(Debug, Deserialize)]
113pub struct JobsSection {
114    /// Retain completed, failed, and cancelled jobs for this many seconds.
115    #[serde(default = "default_job_retention_secs")]
116    pub retention_secs: u64,
117    /// Run terminal-job cleanup at this interval.
118    #[serde(default = "default_job_cleanup_interval_secs")]
119    pub cleanup_interval_secs: u64,
120    /// Maximum queued, running, and retained terminal jobs.
121    #[serde(default = "default_max_retained_jobs")]
122    pub max_retained_jobs: usize,
123}
124
125impl Default for JobsSection {
126    fn default() -> Self {
127        Self {
128            retention_secs: default_job_retention_secs(),
129            cleanup_interval_secs: default_job_cleanup_interval_secs(),
130            max_retained_jobs: default_max_retained_jobs(),
131        }
132    }
133}
134
135/// Large-result artifact storage settings.
136#[derive(Debug, Deserialize)]
137pub struct ArtifactsSection {
138    /// Directory containing result artifact JSON files.
139    #[serde(default = "default_artifact_directory")]
140    pub directory: String,
141    /// Largest result JSON payload that can be returned inline.
142    #[serde(default = "default_artifact_inline_limit_bytes")]
143    pub inline_limit_bytes: usize,
144    /// Raw bytes returned by each artifact chunk RPC.
145    #[serde(default = "default_artifact_chunk_size")]
146    pub chunk_size: usize,
147    /// Retain artifacts for this many seconds.
148    #[serde(default = "default_artifact_retention_secs")]
149    pub retention_secs: u64,
150    /// Maximum total bytes retained by the artifact store.
151    #[serde(default = "default_artifact_max_total_bytes")]
152    pub max_total_bytes: u64,
153}
154
155impl Default for ArtifactsSection {
156    fn default() -> Self {
157        Self {
158            directory: default_artifact_directory(),
159            inline_limit_bytes: default_artifact_inline_limit_bytes(),
160            chunk_size: default_artifact_chunk_size(),
161            retention_secs: default_artifact_retention_secs(),
162            max_total_bytes: default_artifact_max_total_bytes(),
163        }
164    }
165}
166
167/// Logging configuration with sensible defaults.
168#[derive(Debug, Deserialize)]
169pub struct LoggingSection {
170    #[serde(default = "default_log_level")]
171    pub level: String,
172}
173
174impl Default for LoggingSection {
175    fn default() -> Self {
176        Self {
177            level: default_log_level(),
178        }
179    }
180}
181
182fn default_market_data_source() -> String {
183    "local-parquet".into()
184}
185
186fn default_shm_buffer() -> usize {
187    16 * 1024 * 1024
188}
189
190fn default_max_connections() -> usize {
191    256
192}
193
194fn default_job_retention_secs() -> u64 {
195    3_600
196}
197
198fn default_job_cleanup_interval_secs() -> u64 {
199    60
200}
201
202fn default_max_retained_jobs() -> usize {
203    1_000
204}
205
206fn default_artifact_directory() -> String {
207    "backtest-artifacts".into()
208}
209
210fn default_artifact_inline_limit_bytes() -> usize {
211    12 * 1024 * 1024
212}
213
214fn default_artifact_chunk_size() -> usize {
215    1024 * 1024
216}
217
218fn default_artifact_retention_secs() -> u64 {
219    3_600
220}
221
222fn default_artifact_max_total_bytes() -> u64 {
223    1024 * 1024 * 1024
224}
225
226fn default_log_level() -> String {
227    "info".into()
228}
229
230/// Load and parse a TOML config file into `ServerConfig`.
231pub fn load_config(path: &str) -> crate::error::Result<ServerConfig> {
232    let content = std::fs::read_to_string(path)
233        .map_err(|e| crate::error::BacktestServerError::Config(format!("{path}: {e}")))?;
234    toml::from_str(&content)
235        .map_err(|e| crate::error::BacktestServerError::Config(format!("{path}: {e}")))
236}