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, alias = "ev")]
12 pub alpha: AlphaConfig,
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 AlphaConfig {
96 #[serde(default = "default_alpha_enabled")]
97 pub enabled: bool,
98 #[serde(default = "default_alpha_mode")]
99 pub mode: String,
100 #[serde(default = "default_alpha_lookback_trades")]
101 pub lookback_trades: usize,
102 #[serde(default = "default_alpha_prior_a")]
103 pub prior_a: f64,
104 #[serde(default = "default_alpha_prior_b")]
105 pub prior_b: f64,
106 #[serde(default = "default_alpha_tail_prior_a")]
107 pub tail_prior_a: f64,
108 #[serde(default = "default_alpha_tail_prior_b")]
109 pub tail_prior_b: f64,
110 #[serde(default = "default_alpha_recency_lambda")]
111 pub recency_lambda: f64,
112 #[serde(default = "default_alpha_shrink_k")]
113 pub shrink_k: f64,
114 #[serde(default = "default_alpha_loss_threshold_usdt")]
115 pub loss_threshold_usdt: f64,
116 #[serde(default = "default_alpha_gamma_tail_penalty")]
117 pub gamma_tail_penalty: f64,
118 #[serde(default = "default_alpha_fee_slippage_penalty_usdt")]
119 pub fee_slippage_penalty_usdt: f64,
120 #[serde(
121 default = "default_alpha_entry_gate_min_alpha_usdt",
122 alias = "entry_gate_min_ev_usdt"
123 )]
124 pub entry_gate_min_alpha_usdt: f64,
125 #[serde(default = "default_alpha_forward_p_win")]
126 pub forward_p_win: f64,
127 #[serde(default = "default_alpha_forward_target_rr")]
128 pub forward_target_rr: f64,
129 #[serde(default = "default_alpha_predictor_mu", alias = "y_mu")]
130 pub predictor_mu: f64,
131 #[serde(default = "default_alpha_predictor_sigma_spot", alias = "y_sigma_spot")]
132 pub predictor_sigma_spot: f64,
133 #[serde(
134 default = "default_alpha_predictor_sigma_futures",
135 alias = "y_sigma_futures"
136 )]
137 pub predictor_sigma_futures: f64,
138 #[serde(default = "default_alpha_futures_multiplier")]
139 pub futures_multiplier: f64,
140 #[serde(
141 default = "default_alpha_predictor_ewma_alpha_mean",
142 alias = "y_ewma_alpha_mean"
143 )]
144 pub predictor_ewma_alpha_mean: f64,
145 #[serde(
146 default = "default_alpha_predictor_ewma_alpha_var",
147 alias = "y_ewma_alpha_var"
148 )]
149 pub predictor_ewma_alpha_var: f64,
150 #[serde(default = "default_alpha_predictor_min_sigma", alias = "y_min_sigma")]
151 pub predictor_min_sigma: f64,
152 #[serde(default = "default_alpha_regime_gate_enabled")]
153 pub regime_gate_enabled: bool,
154 #[serde(default = "default_alpha_regime_confidence_min")]
155 pub regime_confidence_min: f64,
156 #[serde(default = "default_alpha_regime_entry_multiplier_up")]
157 pub regime_entry_multiplier_up: f64,
158 #[serde(default = "default_alpha_regime_entry_multiplier_range")]
159 pub regime_entry_multiplier_range: f64,
160 #[serde(default = "default_alpha_regime_entry_multiplier_down")]
161 pub regime_entry_multiplier_down: f64,
162 #[serde(default = "default_alpha_regime_entry_multiplier_unknown")]
163 pub regime_entry_multiplier_unknown: f64,
164 #[serde(default = "default_alpha_regime_hold_multiplier_up")]
165 pub regime_hold_multiplier_up: f64,
166 #[serde(default = "default_alpha_regime_hold_multiplier_range")]
167 pub regime_hold_multiplier_range: f64,
168 #[serde(default = "default_alpha_regime_hold_multiplier_down")]
169 pub regime_hold_multiplier_down: f64,
170 #[serde(default = "default_alpha_regime_hold_multiplier_unknown")]
171 pub regime_hold_multiplier_unknown: f64,
172}
173
174impl Default for AlphaConfig {
175 fn default() -> Self {
176 Self {
177 enabled: default_alpha_enabled(),
178 mode: default_alpha_mode(),
179 lookback_trades: default_alpha_lookback_trades(),
180 prior_a: default_alpha_prior_a(),
181 prior_b: default_alpha_prior_b(),
182 tail_prior_a: default_alpha_tail_prior_a(),
183 tail_prior_b: default_alpha_tail_prior_b(),
184 recency_lambda: default_alpha_recency_lambda(),
185 shrink_k: default_alpha_shrink_k(),
186 loss_threshold_usdt: default_alpha_loss_threshold_usdt(),
187 gamma_tail_penalty: default_alpha_gamma_tail_penalty(),
188 fee_slippage_penalty_usdt: default_alpha_fee_slippage_penalty_usdt(),
189 entry_gate_min_alpha_usdt: default_alpha_entry_gate_min_alpha_usdt(),
190 forward_p_win: default_alpha_forward_p_win(),
191 forward_target_rr: default_alpha_forward_target_rr(),
192 predictor_mu: default_alpha_predictor_mu(),
193 predictor_sigma_spot: default_alpha_predictor_sigma_spot(),
194 predictor_sigma_futures: default_alpha_predictor_sigma_futures(),
195 futures_multiplier: default_alpha_futures_multiplier(),
196 predictor_ewma_alpha_mean: default_alpha_predictor_ewma_alpha_mean(),
197 predictor_ewma_alpha_var: default_alpha_predictor_ewma_alpha_var(),
198 predictor_min_sigma: default_alpha_predictor_min_sigma(),
199 regime_gate_enabled: default_alpha_regime_gate_enabled(),
200 regime_confidence_min: default_alpha_regime_confidence_min(),
201 regime_entry_multiplier_up: default_alpha_regime_entry_multiplier_up(),
202 regime_entry_multiplier_range: default_alpha_regime_entry_multiplier_range(),
203 regime_entry_multiplier_down: default_alpha_regime_entry_multiplier_down(),
204 regime_entry_multiplier_unknown: default_alpha_regime_entry_multiplier_unknown(),
205 regime_hold_multiplier_up: default_alpha_regime_hold_multiplier_up(),
206 regime_hold_multiplier_range: default_alpha_regime_hold_multiplier_range(),
207 regime_hold_multiplier_down: default_alpha_regime_hold_multiplier_down(),
208 regime_hold_multiplier_unknown: default_alpha_regime_hold_multiplier_unknown(),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Deserialize)]
214pub struct ExitConfig {
215 #[serde(default = "default_exit_max_holding_ms")]
216 pub max_holding_ms: u64,
217 #[serde(default = "default_exit_stop_loss_pct")]
218 pub stop_loss_pct: f64,
219 #[serde(default = "default_exit_enforce_protective_stop")]
220 pub enforce_protective_stop: bool,
221}
222
223impl Default for ExitConfig {
224 fn default() -> Self {
225 Self {
226 max_holding_ms: default_exit_max_holding_ms(),
227 stop_loss_pct: default_exit_stop_loss_pct(),
228 enforce_protective_stop: default_exit_enforce_protective_stop(),
229 }
230 }
231}
232
233impl Default for EndpointRateLimitConfig {
234 fn default() -> Self {
235 Self {
236 orders_per_minute: default_endpoint_orders_limit_per_minute(),
237 account_per_minute: default_endpoint_account_limit_per_minute(),
238 market_data_per_minute: default_endpoint_market_data_limit_per_minute(),
239 }
240 }
241}
242
243impl Default for RiskConfig {
244 fn default() -> Self {
245 Self {
246 global_rate_limit_per_minute: default_global_rate_limit_per_minute(),
247 default_strategy_cooldown_ms: default_strategy_cooldown_ms(),
248 default_strategy_max_active_orders: default_strategy_max_active_orders(),
249 default_symbol_max_exposure_usdt: default_symbol_max_exposure_usdt(),
250 strategy_limits: Vec::new(),
251 symbol_exposure_limits: Vec::new(),
252 endpoint_rate_limits: EndpointRateLimitConfig::default(),
253 }
254 }
255}
256
257#[derive(Debug, Clone, Deserialize)]
258pub struct UiConfig {
259 pub refresh_rate_ms: u64,
260 pub price_history_len: usize,
261}
262
263#[derive(Debug, Clone, Deserialize)]
264pub struct LoggingConfig {
265 pub level: String,
266}
267
268fn default_futures_rest_base_url() -> String {
269 "https://demo-fapi.binance.com".to_string()
270}
271
272fn default_futures_ws_base_url() -> String {
273 "wss://fstream.binancefuture.com/ws".to_string()
274}
275
276fn default_global_rate_limit_per_minute() -> u32 {
277 600
278}
279
280fn default_strategy_cooldown_ms() -> u64 {
281 3_000
282}
283
284fn default_strategy_max_active_orders() -> u32 {
285 1
286}
287
288fn default_symbol_max_exposure_usdt() -> f64 {
289 200.0
290}
291
292fn default_endpoint_orders_limit_per_minute() -> u32 {
293 240
294}
295
296fn default_endpoint_account_limit_per_minute() -> u32 {
297 180
298}
299
300fn default_endpoint_market_data_limit_per_minute() -> u32 {
301 360
302}
303
304fn default_alpha_enabled() -> bool {
305 true
306}
307
308fn default_alpha_mode() -> String {
309 "shadow".to_string()
310}
311
312fn default_alpha_lookback_trades() -> usize {
313 200
314}
315
316fn default_alpha_prior_a() -> f64 {
317 6.0
318}
319
320fn default_alpha_prior_b() -> f64 {
321 6.0
322}
323
324fn default_alpha_tail_prior_a() -> f64 {
325 3.0
326}
327
328fn default_alpha_tail_prior_b() -> f64 {
329 7.0
330}
331
332fn default_alpha_recency_lambda() -> f64 {
333 0.08
334}
335
336fn default_alpha_shrink_k() -> f64 {
337 40.0
338}
339
340fn default_alpha_loss_threshold_usdt() -> f64 {
341 15.0
342}
343
344fn default_alpha_gamma_tail_penalty() -> f64 {
345 0.8
346}
347
348fn default_alpha_fee_slippage_penalty_usdt() -> f64 {
349 0.0
350}
351
352fn default_alpha_entry_gate_min_alpha_usdt() -> f64 {
353 0.0
354}
355
356fn default_alpha_forward_p_win() -> f64 {
357 0.5
358}
359
360fn default_alpha_forward_target_rr() -> f64 {
361 1.5
362}
363
364fn default_alpha_predictor_mu() -> f64 {
365 0.0
366}
367
368fn default_alpha_predictor_sigma_spot() -> f64 {
369 0.01
370}
371
372fn default_alpha_predictor_sigma_futures() -> f64 {
373 0.015
374}
375
376fn default_alpha_futures_multiplier() -> f64 {
377 1.0
378}
379
380fn default_alpha_predictor_ewma_alpha_mean() -> f64 {
381 0.08
382}
383
384fn default_alpha_predictor_ewma_alpha_var() -> f64 {
385 0.08
386}
387
388fn default_alpha_predictor_min_sigma() -> f64 {
389 0.001
390}
391
392fn default_alpha_regime_gate_enabled() -> bool {
393 true
394}
395
396fn default_alpha_regime_confidence_min() -> f64 {
397 0.0
398}
399
400fn default_alpha_regime_entry_multiplier_up() -> f64 {
401 1.0
402}
403
404fn default_alpha_regime_entry_multiplier_range() -> f64 {
405 0.5
406}
407
408fn default_alpha_regime_entry_multiplier_down() -> f64 {
409 0.0
410}
411
412fn default_alpha_regime_entry_multiplier_unknown() -> f64 {
413 0.0
414}
415
416fn default_alpha_regime_hold_multiplier_up() -> f64 {
417 1.0
418}
419
420fn default_alpha_regime_hold_multiplier_range() -> f64 {
421 0.5
422}
423
424fn default_alpha_regime_hold_multiplier_down() -> f64 {
425 0.75
426}
427
428fn default_alpha_regime_hold_multiplier_unknown() -> f64 {
429 0.75
430}
431
432fn default_exit_max_holding_ms() -> u64 {
433 1_800_000
434}
435
436fn default_exit_stop_loss_pct() -> f64 {
437 0.015
438}
439
440fn default_exit_enforce_protective_stop() -> bool {
441 true
442}
443
444pub fn parse_interval_ms(s: &str) -> Result<u64> {
446 if s.len() < 2 {
447 bail!("invalid interval '{}': expected format like '1m'", s);
448 }
449
450 let (num_str, suffix) = s.split_at(s.len() - 1);
451 let n: u64 = num_str.parse().with_context(|| {
452 format!(
453 "invalid interval '{}': quantity must be a positive integer",
454 s
455 )
456 })?;
457 if n == 0 {
458 bail!("invalid interval '{}': quantity must be > 0", s);
459 }
460
461 let unit_ms = match suffix {
462 "s" => 1_000,
463 "m" => 60_000,
464 "h" => 3_600_000,
465 "d" => 86_400_000,
466 "w" => 7 * 86_400_000,
467 "M" => 30 * 86_400_000,
468 _ => bail!(
469 "invalid interval '{}': unsupported suffix '{}', expected one of s/m/h/d/w/M",
470 s,
471 suffix
472 ),
473 };
474
475 n.checked_mul(unit_ms)
476 .with_context(|| format!("invalid interval '{}': value is too large", s))
477}
478
479impl BinanceConfig {
480 pub fn kline_interval_ms(&self) -> Result<u64> {
481 parse_interval_ms(&self.kline_interval)
482 }
483
484 pub fn tradable_symbols(&self) -> Vec<String> {
485 let mut out = Vec::new();
486 if !self.symbol.trim().is_empty() {
487 out.push(self.symbol.trim().to_ascii_uppercase());
488 }
489 for sym in &self.symbols {
490 let s = sym.trim().to_ascii_uppercase();
491 if !s.is_empty() && !out.iter().any(|v| v == &s) {
492 out.push(s);
493 }
494 }
495 out
496 }
497
498 pub fn tradable_instruments(&self) -> Vec<String> {
499 let mut out = self.tradable_symbols();
500 for sym in &self.futures_symbols {
501 let s = sym.trim().to_ascii_uppercase();
502 if !s.is_empty() {
503 let label = format!("{} (FUT)", s);
504 if !out.iter().any(|v| v == &label) {
505 out.push(label);
506 }
507 }
508 }
509 out
510 }
511}
512
513impl Config {
514 pub fn load() -> Result<Self> {
515 dotenvy::dotenv().ok();
516
517 let config_path = Path::new("config/default.toml");
518 let config_str = std::fs::read_to_string(config_path)
519 .with_context(|| format!("failed to read {}", config_path.display()))?;
520
521 let mut config: Config =
522 toml::from_str(&config_str).context("failed to parse config/default.toml")?;
523
524 config.binance.api_key = load_required_credential("BINANCE_API_KEY")?;
525 config.binance.api_secret = load_required_credential("BINANCE_API_SECRET")?;
526 config.binance.futures_api_key = load_optional_credential("BINANCE_FUTURES_API_KEY")
527 .unwrap_or_else(|| config.binance.api_key.clone());
528 config.binance.futures_api_secret = load_optional_credential("BINANCE_FUTURES_API_SECRET")
529 .unwrap_or_else(|| config.binance.api_secret.clone());
530
531 config
532 .binance
533 .kline_interval_ms()
534 .context("binance.kline_interval is invalid")?;
535
536 Ok(config)
537 }
538}
539
540fn sanitize_credential(raw: &str) -> String {
541 let trimmed = raw.trim();
542 let unquoted = if trimmed.len() >= 2 {
543 let starts = trimmed.as_bytes()[0];
544 let ends = trimmed.as_bytes()[trimmed.len() - 1];
545 if (starts == b'"' && ends == b'"') || (starts == b'\'' && ends == b'\'') {
546 &trimmed[1..trimmed.len() - 1]
547 } else {
548 trimmed
549 }
550 } else {
551 trimmed
552 };
553 unquoted.trim().to_string()
554}
555
556fn load_required_credential(name: &str) -> Result<String> {
557 let raw =
558 std::env::var(name).with_context(|| format!("{} not set in .env or environment", name))?;
559 let sanitized = sanitize_credential(&raw);
560 if sanitized.is_empty() {
561 bail!("{} is empty after trimming whitespace/quotes", name);
562 }
563 Ok(sanitized)
564}
565
566fn load_optional_credential(name: &str) -> Option<String> {
567 let raw = std::env::var(name).ok()?;
568 let sanitized = sanitize_credential(&raw);
569 if sanitized.is_empty() {
570 None
571 } else {
572 Some(sanitized)
573 }
574}