Skip to main content

sandbox_quant/app/
bootstrap.rs

1use std::env;
2use std::fs::File;
3use std::path::Path;
4use std::sync::Arc;
5
6use crate::error::exchange_error::ExchangeError;
7use crate::exchange::binance::auth::BinanceAuth;
8use crate::exchange::binance::client::{BinanceExchange, BinanceHttpTransport, BinanceTransport};
9use crate::exchange::binance::demo::BinanceDemoHttpTransport;
10use crate::exchange::facade::ExchangeFacade;
11use crate::execution::service::ExecutionService;
12use crate::market_data::price_store::PriceStore;
13use crate::market_data::service::MarketDataService;
14use crate::portfolio::store::PortfolioStateStore;
15use crate::portfolio::sync::PortfolioSyncService;
16use crate::record::coordination::RecorderCoordination;
17use crate::storage::event_log::EventLog;
18use crate::strategy::store::StrategyStore;
19
20#[derive(Debug)]
21pub struct AppBootstrap<E: ExchangeFacade> {
22    pub exchange: E,
23    pub mode: BinanceMode,
24    pub portfolio_store: PortfolioStateStore,
25    pub price_store: PriceStore,
26    pub event_log: EventLog,
27    pub execution: ExecutionService,
28    pub portfolio_sync: PortfolioSyncService,
29    pub market_data: MarketDataService,
30    pub recorder_coordination: RecorderCoordination,
31    pub strategy_store: StrategyStore,
32}
33
34impl<E: ExchangeFacade> AppBootstrap<E> {
35    pub fn new(exchange: E, portfolio_store: PortfolioStateStore) -> Self {
36        Self {
37            exchange,
38            mode: BinanceMode::Demo,
39            portfolio_store,
40            price_store: PriceStore::default(),
41            event_log: EventLog::default(),
42            execution: ExecutionService::default(),
43            portfolio_sync: PortfolioSyncService,
44            market_data: MarketDataService,
45            recorder_coordination: RecorderCoordination::default(),
46            strategy_store: StrategyStore::default(),
47        }
48    }
49}
50
51impl AppBootstrap<BinanceExchange> {
52    /// Builds the real Binance-backed app bootstrap from environment variables.
53    ///
54    /// Required:
55    /// - `BINANCE_DEMO_API_KEY` and `BINANCE_DEMO_SECRET_KEY` when `BINANCE_MODE=demo`
56    /// - `BINANCE_REAL_API_KEY` and `BINANCE_REAL_SECRET_KEY` when `BINANCE_MODE=real`
57    ///
58    /// Optional:
59    /// - `BINANCE_API_KEY`
60    /// - `BINANCE_SECRET_KEY`
61    /// - `BINANCE_SPOT_BASE_URL`
62    /// - `BINANCE_FUTURES_BASE_URL`
63    /// - `BINANCE_OPTIONS_BASE_URL`
64    /// - `BINANCE_MODE`
65    pub fn from_env(portfolio_store: PortfolioStateStore) -> Result<Self, ExchangeError> {
66        let config = BinanceEnvConfig::from_env()?;
67        let mut app = Self::new(
68            BinanceExchange::new(config.build_transport()),
69            portfolio_store,
70        );
71        app.mode = config.mode;
72        Ok(app)
73    }
74
75    pub fn switch_mode(&mut self, mode: BinanceMode) -> Result<(), ExchangeError> {
76        let mut config = BinanceEnvConfig::from_mode(mode)?;
77        config.spot_base_url = None;
78        config.futures_base_url = None;
79        config.options_base_url = None;
80        self.exchange = BinanceExchange::new(config.build_transport());
81        self.mode = mode;
82        Ok(())
83    }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
87pub enum BinanceMode {
88    Real,
89    Demo,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct BinanceEnvConfig {
94    pub api_key: String,
95    pub secret_key: String,
96    pub mode: BinanceMode,
97    pub spot_base_url: Option<String>,
98    pub futures_base_url: Option<String>,
99    pub options_base_url: Option<String>,
100}
101
102impl BinanceEnvConfig {
103    pub fn from_env() -> Result<Self, ExchangeError> {
104        Self::from_mode(Self::mode_from_env())
105    }
106
107    pub fn from_mode(mode: BinanceMode) -> Result<Self, ExchangeError> {
108        let (api_key_var, secret_key_var) = mode.credentials_env_names();
109        let api_key = Self::read_required_env(api_key_var, "BINANCE_API_KEY")?;
110        let secret_key = Self::read_required_env(secret_key_var, "BINANCE_SECRET_KEY")?;
111        Ok(Self {
112            api_key,
113            secret_key,
114            mode,
115            spot_base_url: Self::read_env_value("BINANCE_SPOT_BASE_URL"),
116            futures_base_url: Self::read_env_value("BINANCE_FUTURES_BASE_URL"),
117            options_base_url: Self::read_env_value("BINANCE_OPTIONS_BASE_URL"),
118        })
119    }
120
121    fn mode_from_env() -> BinanceMode {
122        let mode = Self::read_env_value("BINANCE_MODE").unwrap_or_else(|| "demo".to_string());
123        match mode.to_ascii_lowercase().as_str() {
124            "demo" => BinanceMode::Demo,
125            _ => BinanceMode::Real,
126        }
127    }
128
129    fn read_required_env(
130        primary: &'static str,
131        fallback: &'static str,
132    ) -> Result<String, ExchangeError> {
133        Self::read_env_value(primary)
134            .or_else(|| Self::read_env_value(fallback))
135            .ok_or(ExchangeError::MissingConfiguration(primary))
136    }
137
138    fn read_env_value(key: &'static str) -> Option<String> {
139        env::var(key).ok().or_else(|| Self::read_dotenv_value(key))
140    }
141
142    fn read_dotenv_value(key: &'static str) -> Option<String> {
143        if env::var_os("SANDBOX_QUANT_DISABLE_DOTENV").is_some() {
144            return None;
145        }
146
147        Self::find_in_dotenv_iter(dotenvy::from_filename_iter(".env").ok(), key).or_else(|| {
148            let manifest_dotenv = Path::new(env!("CARGO_MANIFEST_DIR")).join(".env");
149            Self::find_in_dotenv_iter(dotenvy::from_path_iter(&manifest_dotenv).ok(), key)
150        })
151    }
152
153    fn find_in_dotenv_iter(iter: Option<dotenvy::Iter<File>>, key: &'static str) -> Option<String> {
154        iter.and_then(|entries| {
155            entries
156                .filter_map(Result::ok)
157                .find_map(|(entry_key, entry_value)| {
158                    if entry_key == key {
159                        Some(entry_value)
160                    } else {
161                        None
162                    }
163                })
164        })
165    }
166
167    pub fn build_transport(&self) -> Arc<dyn BinanceTransport> {
168        let auth = BinanceAuth::new(self.api_key.clone(), self.secret_key.clone());
169        match (
170            &self.spot_base_url,
171            &self.futures_base_url,
172            &self.options_base_url,
173        ) {
174            (Some(spot), Some(futures), Some(options)) => {
175                Arc::new(BinanceHttpTransport::with_base_urls(
176                    auth,
177                    spot.clone(),
178                    futures.clone(),
179                    options.clone(),
180                ))
181            }
182            _ => match self.mode {
183                BinanceMode::Real => Arc::new(BinanceHttpTransport::new(auth)),
184                BinanceMode::Demo => Arc::new(BinanceDemoHttpTransport::new(auth)),
185            },
186        }
187    }
188}
189
190impl BinanceMode {
191    pub fn as_str(self) -> &'static str {
192        match self {
193            Self::Real => "real",
194            Self::Demo => "demo",
195        }
196    }
197
198    fn credentials_env_names(self) -> (&'static str, &'static str) {
199        match self {
200            Self::Real => ("BINANCE_REAL_API_KEY", "BINANCE_REAL_SECRET_KEY"),
201            Self::Demo => ("BINANCE_DEMO_API_KEY", "BINANCE_DEMO_SECRET_KEY"),
202        }
203    }
204}