1use qs_instruments::PositiveDecimal;
4use qs_service::ServiceEndpoint;
5use serde::Deserialize;
6use std::collections::BTreeMap;
7
8#[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#[derive(Debug, Deserialize)]
29pub struct ServerSection {
30 #[serde(default)]
32 pub endpoint: Option<ServiceEndpoint>,
33 #[serde(default)]
35 pub shm_name: String,
36 #[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#[derive(Debug, Deserialize)]
74pub struct DatabaseSection {
75 pub data_dir: String,
77}
78
79#[derive(Debug, Deserialize)]
81pub struct SymbolsSection {
82 pub registry_path: String,
83}
84
85#[derive(Debug, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct InstrumentsSection {
89 #[serde(default)]
91 pub catalog_path: Option<String>,
92 #[serde(default)]
94 pub default_listing_venue: Option<String>,
95 #[serde(default = "default_market_data_source")]
97 pub market_data_source: String,
98 #[serde(default)]
100 pub linear_instruments: Vec<LinearInstrumentConfig>,
101 #[serde(default)]
103 pub source_symbols: BTreeMap<String, String>,
104}
105
106#[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#[derive(Debug, Deserialize)]
133pub struct ProfilesSection {
134 pub profiles_path: String,
135}
136
137#[derive(Debug, Deserialize)]
139pub struct JobsSection {
140 #[serde(default = "default_job_retention_secs")]
142 pub retention_secs: u64,
143 #[serde(default = "default_job_cleanup_interval_secs")]
145 pub cleanup_interval_secs: u64,
146 #[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#[derive(Debug, Clone, Deserialize)]
163pub struct StrategiesSection {
164 #[serde(default = "default_max_document_bytes")]
166 pub max_document_bytes: usize,
167 #[serde(default = "default_max_retained_bars")]
169 pub max_retained_bars: usize,
170 #[serde(default = "default_max_search_workers")]
172 pub max_search_workers: usize,
173 #[serde(default = "default_max_search_generation_points")]
175 pub max_search_generation_points: usize,
176 #[serde(default = "default_max_search_runs")]
178 pub max_search_runs: usize,
179 #[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#[derive(Debug, Deserialize)]
265pub struct ArtifactsSection {
266 #[serde(default = "default_artifact_directory")]
268 pub directory: String,
269 #[serde(default = "default_artifact_inline_limit_bytes")]
271 pub inline_limit_bytes: usize,
272 #[serde(default = "default_artifact_chunk_size")]
274 pub chunk_size: usize,
275 #[serde(default = "default_artifact_retention_secs")]
277 pub retention_secs: u64,
278 #[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#[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
358pub 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}