use qs_instruments::PositiveDecimal;
use qs_service::ServiceEndpoint;
use serde::Deserialize;
use std::collections::BTreeMap;
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
pub server: ServerSection,
pub database: DatabaseSection,
pub symbols: SymbolsSection,
#[serde(default)]
pub instruments: InstrumentsSection,
pub profiles: ProfilesSection,
#[serde(default)]
pub jobs: JobsSection,
#[serde(default)]
pub artifacts: ArtifactsSection,
#[serde(default)]
pub logging: LoggingSection,
#[serde(default)]
pub strategies: StrategiesSection,
}
#[derive(Debug, Deserialize)]
pub struct ServerSection {
#[serde(default)]
pub endpoint: Option<ServiceEndpoint>,
#[serde(default)]
pub shm_name: String,
#[serde(default = "default_shm_buffer")]
pub shm_buffer_size: usize,
#[serde(default = "default_max_connections")]
pub max_connections: usize,
#[serde(default)]
pub allow_insecure_non_loopback: bool,
}
impl ServerSection {
pub fn resolved_endpoint(&self) -> crate::error::Result<ServiceEndpoint> {
let legacy = if self.shm_name.is_empty() {
None
} else {
Some(
ServiceEndpoint::shared_memory(self.shm_name.clone()).map_err(|error| {
crate::error::BacktestServerError::Config(error.to_string())
})?,
)
};
match (&self.endpoint, legacy) {
(Some(endpoint), Some(legacy)) if endpoint != &legacy => {
Err(crate::error::BacktestServerError::Config(format!(
"conflicting server.endpoint '{}' and deprecated server.shm_name '{}'",
endpoint, self.shm_name
)))
}
(Some(endpoint), _) => Ok(endpoint.clone()),
(None, Some(endpoint)) => Ok(endpoint),
(None, None) => Err(crate::error::BacktestServerError::Config(
"server.endpoint is required (or provide deprecated server.shm_name)".to_string(),
)),
}
}
}
#[derive(Debug, Deserialize)]
pub struct DatabaseSection {
pub data_dir: String,
}
#[derive(Debug, Deserialize)]
pub struct SymbolsSection {
pub registry_path: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstrumentsSection {
#[serde(default)]
pub catalog_path: Option<String>,
#[serde(default)]
pub default_listing_venue: Option<String>,
#[serde(default = "default_market_data_source")]
pub market_data_source: String,
#[serde(default)]
pub linear_instruments: Vec<LinearInstrumentConfig>,
#[serde(default)]
pub source_symbols: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LinearInstrumentConfig {
pub symbol: String,
pub contract_multiplier: PositiveDecimal,
pub price_step: PositiveDecimal,
pub display_scale: u8,
pub quantity_step: PositiveDecimal,
pub minimum: PositiveDecimal,
pub maximum: PositiveDecimal,
}
impl Default for InstrumentsSection {
fn default() -> Self {
Self {
catalog_path: None,
default_listing_venue: None,
market_data_source: default_market_data_source(),
linear_instruments: Vec::new(),
source_symbols: BTreeMap::new(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct ProfilesSection {
pub profiles_path: String,
}
#[derive(Debug, Deserialize)]
pub struct JobsSection {
#[serde(default = "default_job_retention_secs")]
pub retention_secs: u64,
#[serde(default = "default_job_cleanup_interval_secs")]
pub cleanup_interval_secs: u64,
#[serde(default = "default_max_retained_jobs")]
pub max_retained_jobs: usize,
}
impl Default for JobsSection {
fn default() -> Self {
Self {
retention_secs: default_job_retention_secs(),
cleanup_interval_secs: default_job_cleanup_interval_secs(),
max_retained_jobs: default_max_retained_jobs(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct StrategiesSection {
#[serde(default = "default_max_document_bytes")]
pub max_document_bytes: usize,
#[serde(default = "default_max_retained_bars")]
pub max_retained_bars: usize,
#[serde(default = "default_max_search_workers")]
pub max_search_workers: usize,
#[serde(default = "default_max_search_generation_points")]
pub max_search_generation_points: usize,
#[serde(default = "default_max_search_runs")]
pub max_search_runs: usize,
#[serde(default = "default_max_portfolio_instances")]
pub max_portfolio_instances: usize,
#[serde(default = "default_max_calendar_sessions")]
pub max_calendar_sessions: usize,
#[serde(default = "default_max_calendar_intervals")]
pub max_calendar_intervals: usize,
#[serde(default = "default_max_calendar_exceptions")]
pub max_calendar_exceptions: usize,
#[serde(default = "default_max_calendar_history")]
pub max_calendar_history: usize,
#[serde(default = "default_max_calendar_children")]
pub max_calendar_children: usize,
#[serde(default = "default_max_calendar_bytes")]
pub max_calendar_bytes: usize,
}
impl Default for StrategiesSection {
fn default() -> Self {
Self {
max_document_bytes: default_max_document_bytes(),
max_retained_bars: default_max_retained_bars(),
max_search_workers: default_max_search_workers(),
max_search_generation_points: default_max_search_generation_points(),
max_search_runs: default_max_search_runs(),
max_portfolio_instances: default_max_portfolio_instances(),
max_calendar_sessions: default_max_calendar_sessions(),
max_calendar_intervals: default_max_calendar_intervals(),
max_calendar_exceptions: default_max_calendar_exceptions(),
max_calendar_history: default_max_calendar_history(),
max_calendar_children: default_max_calendar_children(),
max_calendar_bytes: default_max_calendar_bytes(),
}
}
}
fn default_max_document_bytes() -> usize {
256 * 1024
}
fn default_max_retained_bars() -> usize {
200_000
}
fn default_max_search_workers() -> usize {
4
}
fn default_max_search_generation_points() -> usize {
100_000
}
fn default_max_search_runs() -> usize {
20_000
}
fn default_max_portfolio_instances() -> usize {
16
}
fn default_max_calendar_sessions() -> usize {
32
}
fn default_max_calendar_intervals() -> usize {
64
}
fn default_max_calendar_exceptions() -> usize {
366
}
fn default_max_calendar_history() -> usize {
512
}
fn default_max_calendar_children() -> usize {
1_000_000
}
fn default_max_calendar_bytes() -> usize {
64 * 1024 * 1024
}
#[derive(Debug, Deserialize)]
pub struct ArtifactsSection {
#[serde(default = "default_artifact_directory")]
pub directory: String,
#[serde(default = "default_artifact_inline_limit_bytes")]
pub inline_limit_bytes: usize,
#[serde(default = "default_artifact_chunk_size")]
pub chunk_size: usize,
#[serde(default = "default_artifact_retention_secs")]
pub retention_secs: u64,
#[serde(default = "default_artifact_max_total_bytes")]
pub max_total_bytes: u64,
}
impl Default for ArtifactsSection {
fn default() -> Self {
Self {
directory: default_artifact_directory(),
inline_limit_bytes: default_artifact_inline_limit_bytes(),
chunk_size: default_artifact_chunk_size(),
retention_secs: default_artifact_retention_secs(),
max_total_bytes: default_artifact_max_total_bytes(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct LoggingSection {
#[serde(default = "default_log_level")]
pub level: String,
}
impl Default for LoggingSection {
fn default() -> Self {
Self {
level: default_log_level(),
}
}
}
fn default_market_data_source() -> String {
"local-parquet".into()
}
fn default_shm_buffer() -> usize {
16 * 1024 * 1024
}
fn default_max_connections() -> usize {
256
}
fn default_job_retention_secs() -> u64 {
3_600
}
fn default_job_cleanup_interval_secs() -> u64 {
60
}
fn default_max_retained_jobs() -> usize {
1_000
}
fn default_artifact_directory() -> String {
"backtest-artifacts".into()
}
fn default_artifact_inline_limit_bytes() -> usize {
12 * 1024 * 1024
}
fn default_artifact_chunk_size() -> usize {
1024 * 1024
}
fn default_artifact_retention_secs() -> u64 {
3_600
}
fn default_artifact_max_total_bytes() -> u64 {
1024 * 1024 * 1024
}
fn default_log_level() -> String {
"info".into()
}
pub fn load_config(path: &str) -> crate::error::Result<ServerConfig> {
let content = std::fs::read_to_string(path)
.map_err(|e| crate::error::BacktestServerError::Config(format!("{path}: {e}")))?;
toml::from_str(&content)
.map_err(|e| crate::error::BacktestServerError::Config(format!("{path}: {e}")))
}