1use qs_service::ServiceEndpoint;
4use serde::Deserialize;
5
6#[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#[derive(Debug, Deserialize)]
27pub struct ServerSection {
28 #[serde(default)]
30 pub endpoint: Option<ServiceEndpoint>,
31 #[serde(default)]
33 pub shm_name: String,
34 #[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#[derive(Debug, Deserialize)]
72pub struct DatabaseSection {
73 pub data_dir: String,
75}
76
77#[derive(Debug, Deserialize)]
79pub struct SymbolsSection {
80 pub registry_path: String,
81}
82
83#[derive(Debug, Deserialize)]
85pub struct InstrumentsSection {
86 #[serde(default)]
88 pub catalog_path: Option<String>,
89 #[serde(default)]
91 pub default_listing_venue: Option<String>,
92 #[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#[derive(Debug, Deserialize)]
109pub struct ProfilesSection {
110 pub profiles_path: String,
111}
112
113#[derive(Debug, Deserialize)]
115pub struct JobsSection {
116 #[serde(default = "default_job_retention_secs")]
118 pub retention_secs: u64,
119 #[serde(default = "default_job_cleanup_interval_secs")]
121 pub cleanup_interval_secs: u64,
122 #[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#[derive(Debug, Clone, Deserialize)]
139pub struct StrategiesSection {
140 #[serde(default = "default_max_document_bytes")]
142 pub max_document_bytes: usize,
143 #[serde(default = "default_max_retained_bars")]
145 pub max_retained_bars: usize,
146 #[serde(default = "default_max_search_workers")]
148 pub max_search_workers: usize,
149 #[serde(default = "default_max_search_generation_points")]
151 pub max_search_generation_points: usize,
152 #[serde(default = "default_max_search_runs")]
154 pub max_search_runs: usize,
155 #[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#[derive(Debug, Deserialize)]
241pub struct ArtifactsSection {
242 #[serde(default = "default_artifact_directory")]
244 pub directory: String,
245 #[serde(default = "default_artifact_inline_limit_bytes")]
247 pub inline_limit_bytes: usize,
248 #[serde(default = "default_artifact_chunk_size")]
250 pub chunk_size: usize,
251 #[serde(default = "default_artifact_retention_secs")]
253 pub retention_secs: u64,
254 #[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#[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
334pub 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}