Skip to main content

sandbox_quant/
config.rs

1use anyhow::{bail, Context, Result};
2use serde::Deserialize;
3use std::path::Path;
4
5#[derive(Debug, Clone, Deserialize)]
6pub struct Config {
7    pub binance: BinanceConfig,
8    pub strategy: StrategyConfig,
9    #[serde(default)]
10    pub risk: RiskConfig,
11    pub ui: UiConfig,
12    pub logging: LoggingConfig,
13}
14
15#[derive(Debug, Clone, Deserialize)]
16pub struct BinanceConfig {
17    pub rest_base_url: String,
18    pub ws_base_url: String,
19    #[serde(default = "default_futures_rest_base_url")]
20    pub futures_rest_base_url: String,
21    #[serde(default = "default_futures_ws_base_url")]
22    pub futures_ws_base_url: String,
23    pub symbol: String,
24    #[serde(default)]
25    pub symbols: Vec<String>,
26    #[serde(default)]
27    pub futures_symbols: Vec<String>,
28    pub recv_window: u64,
29    pub kline_interval: String,
30    #[serde(skip)]
31    pub api_key: String,
32    #[serde(skip)]
33    pub api_secret: String,
34    #[serde(skip)]
35    pub futures_api_key: String,
36    #[serde(skip)]
37    pub futures_api_secret: String,
38}
39
40#[derive(Debug, Clone, Deserialize)]
41pub struct StrategyConfig {
42    pub fast_period: usize,
43    pub slow_period: usize,
44    pub order_amount_usdt: f64,
45    pub min_ticks_between_signals: u64,
46}
47
48#[derive(Debug, Clone, Deserialize)]
49pub struct RiskConfig {
50    #[serde(default = "default_global_rate_limit_per_minute")]
51    pub global_rate_limit_per_minute: u32,
52    #[serde(default = "default_strategy_cooldown_ms")]
53    pub default_strategy_cooldown_ms: u64,
54    #[serde(default = "default_strategy_max_active_orders")]
55    pub default_strategy_max_active_orders: u32,
56    #[serde(default)]
57    pub strategy_limits: Vec<StrategyLimitConfig>,
58}
59
60#[derive(Debug, Clone, Deserialize)]
61pub struct StrategyLimitConfig {
62    pub source_tag: String,
63    pub cooldown_ms: Option<u64>,
64    pub max_active_orders: Option<u32>,
65}
66
67impl Default for RiskConfig {
68    fn default() -> Self {
69        Self {
70            global_rate_limit_per_minute: default_global_rate_limit_per_minute(),
71            default_strategy_cooldown_ms: default_strategy_cooldown_ms(),
72            default_strategy_max_active_orders: default_strategy_max_active_orders(),
73            strategy_limits: Vec::new(),
74        }
75    }
76}
77
78#[derive(Debug, Clone, Deserialize)]
79pub struct UiConfig {
80    pub refresh_rate_ms: u64,
81    pub price_history_len: usize,
82}
83
84#[derive(Debug, Clone, Deserialize)]
85pub struct LoggingConfig {
86    pub level: String,
87}
88
89fn default_futures_rest_base_url() -> String {
90    "https://demo-fapi.binance.com".to_string()
91}
92
93fn default_futures_ws_base_url() -> String {
94    "wss://fstream.binancefuture.com/ws".to_string()
95}
96
97fn default_global_rate_limit_per_minute() -> u32 {
98    600
99}
100
101fn default_strategy_cooldown_ms() -> u64 {
102    3_000
103}
104
105fn default_strategy_max_active_orders() -> u32 {
106    1
107}
108
109/// Parse a Binance kline interval string (e.g. "1s", "1m", "1h", "1d", "1w", "1M") into milliseconds.
110pub fn parse_interval_ms(s: &str) -> Result<u64> {
111    if s.len() < 2 {
112        bail!("invalid interval '{}': expected format like '1m'", s);
113    }
114
115    let (num_str, suffix) = s.split_at(s.len() - 1);
116    let n: u64 = num_str.parse().with_context(|| {
117        format!(
118            "invalid interval '{}': quantity must be a positive integer",
119            s
120        )
121    })?;
122    if n == 0 {
123        bail!("invalid interval '{}': quantity must be > 0", s);
124    }
125
126    let unit_ms = match suffix {
127        "s" => 1_000,
128        "m" => 60_000,
129        "h" => 3_600_000,
130        "d" => 86_400_000,
131        "w" => 7 * 86_400_000,
132        "M" => 30 * 86_400_000,
133        _ => bail!(
134            "invalid interval '{}': unsupported suffix '{}', expected one of s/m/h/d/w/M",
135            s,
136            suffix
137        ),
138    };
139
140    n.checked_mul(unit_ms)
141        .with_context(|| format!("invalid interval '{}': value is too large", s))
142}
143
144impl BinanceConfig {
145    pub fn kline_interval_ms(&self) -> Result<u64> {
146        parse_interval_ms(&self.kline_interval)
147    }
148
149    pub fn tradable_symbols(&self) -> Vec<String> {
150        let mut out = Vec::new();
151        if !self.symbol.trim().is_empty() {
152            out.push(self.symbol.trim().to_ascii_uppercase());
153        }
154        for sym in &self.symbols {
155            let s = sym.trim().to_ascii_uppercase();
156            if !s.is_empty() && !out.iter().any(|v| v == &s) {
157                out.push(s);
158            }
159        }
160        out
161    }
162
163    pub fn tradable_instruments(&self) -> Vec<String> {
164        let mut out = self.tradable_symbols();
165        for sym in &self.futures_symbols {
166            let s = sym.trim().to_ascii_uppercase();
167            if !s.is_empty() {
168                let label = format!("{} (FUT)", s);
169                if !out.iter().any(|v| v == &label) {
170                    out.push(label);
171                }
172            }
173        }
174        out
175    }
176}
177
178impl Config {
179    pub fn load() -> Result<Self> {
180        dotenvy::dotenv().ok();
181
182        let config_path = Path::new("config/default.toml");
183        let config_str = std::fs::read_to_string(config_path)
184            .with_context(|| format!("failed to read {}", config_path.display()))?;
185
186        let mut config: Config =
187            toml::from_str(&config_str).context("failed to parse config/default.toml")?;
188
189        config.binance.api_key = std::env::var("BINANCE_API_KEY")
190            .context("BINANCE_API_KEY not set in .env or environment")?;
191        config.binance.api_secret = std::env::var("BINANCE_API_SECRET")
192            .context("BINANCE_API_SECRET not set in .env or environment")?;
193        config.binance.futures_api_key = std::env::var("BINANCE_FUTURES_API_KEY")
194            .unwrap_or_else(|_| config.binance.api_key.clone());
195        config.binance.futures_api_secret = std::env::var("BINANCE_FUTURES_API_SECRET")
196            .unwrap_or_else(|_| config.binance.api_secret.clone());
197
198        config
199            .binance
200            .kline_interval_ms()
201            .context("binance.kline_interval is invalid")?;
202
203        Ok(config)
204    }
205}