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    #[serde(default)]
12    pub ev: EvConfig,
13    #[serde(default)]
14    pub exit: ExitConfig,
15    pub ui: UiConfig,
16    pub logging: LoggingConfig,
17}
18
19#[derive(Debug, Clone, Deserialize)]
20pub struct BinanceConfig {
21    pub rest_base_url: String,
22    pub ws_base_url: String,
23    #[serde(default = "default_futures_rest_base_url")]
24    pub futures_rest_base_url: String,
25    #[serde(default = "default_futures_ws_base_url")]
26    pub futures_ws_base_url: String,
27    pub symbol: String,
28    #[serde(default)]
29    pub symbols: Vec<String>,
30    #[serde(default)]
31    pub futures_symbols: Vec<String>,
32    pub recv_window: u64,
33    pub kline_interval: String,
34    #[serde(skip)]
35    pub api_key: String,
36    #[serde(skip)]
37    pub api_secret: String,
38    #[serde(skip)]
39    pub futures_api_key: String,
40    #[serde(skip)]
41    pub futures_api_secret: String,
42}
43
44#[derive(Debug, Clone, Deserialize)]
45pub struct StrategyConfig {
46    pub fast_period: usize,
47    pub slow_period: usize,
48    pub order_amount_usdt: f64,
49    pub min_ticks_between_signals: u64,
50}
51
52#[derive(Debug, Clone, Deserialize)]
53pub struct RiskConfig {
54    #[serde(default = "default_global_rate_limit_per_minute")]
55    pub global_rate_limit_per_minute: u32,
56    #[serde(default = "default_strategy_cooldown_ms")]
57    pub default_strategy_cooldown_ms: u64,
58    #[serde(default = "default_strategy_max_active_orders")]
59    pub default_strategy_max_active_orders: u32,
60    #[serde(default = "default_symbol_max_exposure_usdt")]
61    pub default_symbol_max_exposure_usdt: f64,
62    #[serde(default)]
63    pub strategy_limits: Vec<StrategyLimitConfig>,
64    #[serde(default)]
65    pub symbol_exposure_limits: Vec<SymbolExposureLimitConfig>,
66    #[serde(default)]
67    pub endpoint_rate_limits: EndpointRateLimitConfig,
68}
69
70#[derive(Debug, Clone, Deserialize)]
71pub struct StrategyLimitConfig {
72    pub source_tag: String,
73    pub cooldown_ms: Option<u64>,
74    pub max_active_orders: Option<u32>,
75}
76
77#[derive(Debug, Clone, Deserialize)]
78pub struct SymbolExposureLimitConfig {
79    pub symbol: String,
80    pub market: Option<String>,
81    pub max_exposure_usdt: f64,
82}
83
84#[derive(Debug, Clone, Deserialize)]
85pub struct EndpointRateLimitConfig {
86    #[serde(default = "default_endpoint_orders_limit_per_minute")]
87    pub orders_per_minute: u32,
88    #[serde(default = "default_endpoint_account_limit_per_minute")]
89    pub account_per_minute: u32,
90    #[serde(default = "default_endpoint_market_data_limit_per_minute")]
91    pub market_data_per_minute: u32,
92}
93
94#[derive(Debug, Clone, Deserialize)]
95pub struct EvConfig {
96    #[serde(default = "default_ev_enabled")]
97    pub enabled: bool,
98    #[serde(default = "default_ev_mode")]
99    pub mode: String,
100    #[serde(default = "default_ev_lookback_trades")]
101    pub lookback_trades: usize,
102    #[serde(default = "default_ev_prior_a")]
103    pub prior_a: f64,
104    #[serde(default = "default_ev_prior_b")]
105    pub prior_b: f64,
106    #[serde(default = "default_ev_tail_prior_a")]
107    pub tail_prior_a: f64,
108    #[serde(default = "default_ev_tail_prior_b")]
109    pub tail_prior_b: f64,
110    #[serde(default = "default_ev_recency_lambda")]
111    pub recency_lambda: f64,
112    #[serde(default = "default_ev_shrink_k")]
113    pub shrink_k: f64,
114    #[serde(default = "default_ev_loss_threshold_usdt")]
115    pub loss_threshold_usdt: f64,
116    #[serde(default = "default_ev_gamma_tail_penalty")]
117    pub gamma_tail_penalty: f64,
118    #[serde(default = "default_ev_fee_slippage_penalty_usdt")]
119    pub fee_slippage_penalty_usdt: f64,
120    #[serde(default = "default_ev_entry_gate_min_ev_usdt")]
121    pub entry_gate_min_ev_usdt: f64,
122    #[serde(default = "default_ev_forward_p_win")]
123    pub forward_p_win: f64,
124    #[serde(default = "default_ev_forward_target_rr")]
125    pub forward_target_rr: f64,
126    #[serde(default = "default_ev_y_mu")]
127    pub y_mu: f64,
128    #[serde(default = "default_ev_y_sigma_spot")]
129    pub y_sigma_spot: f64,
130    #[serde(default = "default_ev_y_sigma_futures")]
131    pub y_sigma_futures: f64,
132    #[serde(default = "default_ev_futures_multiplier")]
133    pub futures_multiplier: f64,
134    #[serde(default = "default_ev_y_ewma_alpha_mean")]
135    pub y_ewma_alpha_mean: f64,
136    #[serde(default = "default_ev_y_ewma_alpha_var")]
137    pub y_ewma_alpha_var: f64,
138    #[serde(default = "default_ev_y_min_sigma")]
139    pub y_min_sigma: f64,
140}
141
142impl Default for EvConfig {
143    fn default() -> Self {
144        Self {
145            enabled: default_ev_enabled(),
146            mode: default_ev_mode(),
147            lookback_trades: default_ev_lookback_trades(),
148            prior_a: default_ev_prior_a(),
149            prior_b: default_ev_prior_b(),
150            tail_prior_a: default_ev_tail_prior_a(),
151            tail_prior_b: default_ev_tail_prior_b(),
152            recency_lambda: default_ev_recency_lambda(),
153            shrink_k: default_ev_shrink_k(),
154            loss_threshold_usdt: default_ev_loss_threshold_usdt(),
155            gamma_tail_penalty: default_ev_gamma_tail_penalty(),
156            fee_slippage_penalty_usdt: default_ev_fee_slippage_penalty_usdt(),
157            entry_gate_min_ev_usdt: default_ev_entry_gate_min_ev_usdt(),
158            forward_p_win: default_ev_forward_p_win(),
159            forward_target_rr: default_ev_forward_target_rr(),
160            y_mu: default_ev_y_mu(),
161            y_sigma_spot: default_ev_y_sigma_spot(),
162            y_sigma_futures: default_ev_y_sigma_futures(),
163            futures_multiplier: default_ev_futures_multiplier(),
164            y_ewma_alpha_mean: default_ev_y_ewma_alpha_mean(),
165            y_ewma_alpha_var: default_ev_y_ewma_alpha_var(),
166            y_min_sigma: default_ev_y_min_sigma(),
167        }
168    }
169}
170
171#[derive(Debug, Clone, Deserialize)]
172pub struct ExitConfig {
173    #[serde(default = "default_exit_max_holding_ms")]
174    pub max_holding_ms: u64,
175    #[serde(default = "default_exit_stop_loss_pct")]
176    pub stop_loss_pct: f64,
177    #[serde(default = "default_exit_enforce_protective_stop")]
178    pub enforce_protective_stop: bool,
179}
180
181impl Default for ExitConfig {
182    fn default() -> Self {
183        Self {
184            max_holding_ms: default_exit_max_holding_ms(),
185            stop_loss_pct: default_exit_stop_loss_pct(),
186            enforce_protective_stop: default_exit_enforce_protective_stop(),
187        }
188    }
189}
190
191impl Default for EndpointRateLimitConfig {
192    fn default() -> Self {
193        Self {
194            orders_per_minute: default_endpoint_orders_limit_per_minute(),
195            account_per_minute: default_endpoint_account_limit_per_minute(),
196            market_data_per_minute: default_endpoint_market_data_limit_per_minute(),
197        }
198    }
199}
200
201impl Default for RiskConfig {
202    fn default() -> Self {
203        Self {
204            global_rate_limit_per_minute: default_global_rate_limit_per_minute(),
205            default_strategy_cooldown_ms: default_strategy_cooldown_ms(),
206            default_strategy_max_active_orders: default_strategy_max_active_orders(),
207            default_symbol_max_exposure_usdt: default_symbol_max_exposure_usdt(),
208            strategy_limits: Vec::new(),
209            symbol_exposure_limits: Vec::new(),
210            endpoint_rate_limits: EndpointRateLimitConfig::default(),
211        }
212    }
213}
214
215#[derive(Debug, Clone, Deserialize)]
216pub struct UiConfig {
217    pub refresh_rate_ms: u64,
218    pub price_history_len: usize,
219}
220
221#[derive(Debug, Clone, Deserialize)]
222pub struct LoggingConfig {
223    pub level: String,
224}
225
226fn default_futures_rest_base_url() -> String {
227    "https://demo-fapi.binance.com".to_string()
228}
229
230fn default_futures_ws_base_url() -> String {
231    "wss://fstream.binancefuture.com/ws".to_string()
232}
233
234fn default_global_rate_limit_per_minute() -> u32 {
235    600
236}
237
238fn default_strategy_cooldown_ms() -> u64 {
239    3_000
240}
241
242fn default_strategy_max_active_orders() -> u32 {
243    1
244}
245
246fn default_symbol_max_exposure_usdt() -> f64 {
247    200.0
248}
249
250fn default_endpoint_orders_limit_per_minute() -> u32 {
251    240
252}
253
254fn default_endpoint_account_limit_per_minute() -> u32 {
255    180
256}
257
258fn default_endpoint_market_data_limit_per_minute() -> u32 {
259    360
260}
261
262fn default_ev_enabled() -> bool {
263    true
264}
265
266fn default_ev_mode() -> String {
267    "shadow".to_string()
268}
269
270fn default_ev_lookback_trades() -> usize {
271    200
272}
273
274fn default_ev_prior_a() -> f64 {
275    6.0
276}
277
278fn default_ev_prior_b() -> f64 {
279    6.0
280}
281
282fn default_ev_tail_prior_a() -> f64 {
283    3.0
284}
285
286fn default_ev_tail_prior_b() -> f64 {
287    7.0
288}
289
290fn default_ev_recency_lambda() -> f64 {
291    0.08
292}
293
294fn default_ev_shrink_k() -> f64 {
295    40.0
296}
297
298fn default_ev_loss_threshold_usdt() -> f64 {
299    15.0
300}
301
302fn default_ev_gamma_tail_penalty() -> f64 {
303    0.8
304}
305
306fn default_ev_fee_slippage_penalty_usdt() -> f64 {
307    0.0
308}
309
310fn default_ev_entry_gate_min_ev_usdt() -> f64 {
311    0.0
312}
313
314fn default_ev_forward_p_win() -> f64 {
315    0.5
316}
317
318fn default_ev_forward_target_rr() -> f64 {
319    1.5
320}
321
322fn default_ev_y_mu() -> f64 {
323    0.0
324}
325
326fn default_ev_y_sigma_spot() -> f64 {
327    0.01
328}
329
330fn default_ev_y_sigma_futures() -> f64 {
331    0.015
332}
333
334fn default_ev_futures_multiplier() -> f64 {
335    1.0
336}
337
338fn default_ev_y_ewma_alpha_mean() -> f64 {
339    0.08
340}
341
342fn default_ev_y_ewma_alpha_var() -> f64 {
343    0.08
344}
345
346fn default_ev_y_min_sigma() -> f64 {
347    0.001
348}
349
350fn default_exit_max_holding_ms() -> u64 {
351    1_800_000
352}
353
354fn default_exit_stop_loss_pct() -> f64 {
355    0.015
356}
357
358fn default_exit_enforce_protective_stop() -> bool {
359    true
360}
361
362/// Parse a Binance kline interval string (e.g. "1s", "1m", "1h", "1d", "1w", "1M") into milliseconds.
363pub fn parse_interval_ms(s: &str) -> Result<u64> {
364    if s.len() < 2 {
365        bail!("invalid interval '{}': expected format like '1m'", s);
366    }
367
368    let (num_str, suffix) = s.split_at(s.len() - 1);
369    let n: u64 = num_str.parse().with_context(|| {
370        format!(
371            "invalid interval '{}': quantity must be a positive integer",
372            s
373        )
374    })?;
375    if n == 0 {
376        bail!("invalid interval '{}': quantity must be > 0", s);
377    }
378
379    let unit_ms = match suffix {
380        "s" => 1_000,
381        "m" => 60_000,
382        "h" => 3_600_000,
383        "d" => 86_400_000,
384        "w" => 7 * 86_400_000,
385        "M" => 30 * 86_400_000,
386        _ => bail!(
387            "invalid interval '{}': unsupported suffix '{}', expected one of s/m/h/d/w/M",
388            s,
389            suffix
390        ),
391    };
392
393    n.checked_mul(unit_ms)
394        .with_context(|| format!("invalid interval '{}': value is too large", s))
395}
396
397impl BinanceConfig {
398    pub fn kline_interval_ms(&self) -> Result<u64> {
399        parse_interval_ms(&self.kline_interval)
400    }
401
402    pub fn tradable_symbols(&self) -> Vec<String> {
403        let mut out = Vec::new();
404        if !self.symbol.trim().is_empty() {
405            out.push(self.symbol.trim().to_ascii_uppercase());
406        }
407        for sym in &self.symbols {
408            let s = sym.trim().to_ascii_uppercase();
409            if !s.is_empty() && !out.iter().any(|v| v == &s) {
410                out.push(s);
411            }
412        }
413        out
414    }
415
416    pub fn tradable_instruments(&self) -> Vec<String> {
417        let mut out = self.tradable_symbols();
418        for sym in &self.futures_symbols {
419            let s = sym.trim().to_ascii_uppercase();
420            if !s.is_empty() {
421                let label = format!("{} (FUT)", s);
422                if !out.iter().any(|v| v == &label) {
423                    out.push(label);
424                }
425            }
426        }
427        out
428    }
429}
430
431impl Config {
432    pub fn load() -> Result<Self> {
433        dotenvy::dotenv().ok();
434
435        let config_path = Path::new("config/default.toml");
436        let config_str = std::fs::read_to_string(config_path)
437            .with_context(|| format!("failed to read {}", config_path.display()))?;
438
439        let mut config: Config =
440            toml::from_str(&config_str).context("failed to parse config/default.toml")?;
441
442        config.binance.api_key = std::env::var("BINANCE_API_KEY")
443            .context("BINANCE_API_KEY not set in .env or environment")?;
444        config.binance.api_secret = std::env::var("BINANCE_API_SECRET")
445            .context("BINANCE_API_SECRET not set in .env or environment")?;
446        config.binance.futures_api_key = std::env::var("BINANCE_FUTURES_API_KEY")
447            .unwrap_or_else(|_| config.binance.api_key.clone());
448        config.binance.futures_api_secret = std::env::var("BINANCE_FUTURES_API_SECRET")
449            .unwrap_or_else(|_| config.binance.api_secret.clone());
450
451        config
452            .binance
453            .kline_interval_ms()
454            .context("binance.kline_interval is invalid")?;
455
456        Ok(config)
457    }
458}