backtest_server/
config.rs1use 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}
22
23#[derive(Debug, Deserialize)]
25pub struct ServerSection {
26 #[serde(default)]
28 pub endpoint: Option<ServiceEndpoint>,
29 #[serde(default)]
31 pub shm_name: String,
32 #[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#[derive(Debug, Deserialize)]
70pub struct DatabaseSection {
71 pub data_dir: String,
73}
74
75#[derive(Debug, Deserialize)]
77pub struct SymbolsSection {
78 pub registry_path: String,
79}
80
81#[derive(Debug, Deserialize)]
83pub struct InstrumentsSection {
84 #[serde(default)]
86 pub catalog_path: Option<String>,
87 #[serde(default)]
89 pub default_listing_venue: Option<String>,
90 #[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#[derive(Debug, Deserialize)]
107pub struct ProfilesSection {
108 pub profiles_path: String,
109}
110
111#[derive(Debug, Deserialize)]
113pub struct JobsSection {
114 #[serde(default = "default_job_retention_secs")]
116 pub retention_secs: u64,
117 #[serde(default = "default_job_cleanup_interval_secs")]
119 pub cleanup_interval_secs: u64,
120 #[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#[derive(Debug, Deserialize)]
137pub struct ArtifactsSection {
138 #[serde(default = "default_artifact_directory")]
140 pub directory: String,
141 #[serde(default = "default_artifact_inline_limit_bytes")]
143 pub inline_limit_bytes: usize,
144 #[serde(default = "default_artifact_chunk_size")]
146 pub chunk_size: usize,
147 #[serde(default = "default_artifact_retention_secs")]
149 pub retention_secs: u64,
150 #[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#[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
230pub 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}