sandbox_quant/app/
bootstrap.rs1use 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 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}