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