1use super::Allocation;
42use rust_decimal::Decimal;
43use serde::{Deserialize, Serialize};
44use std::collections::HashMap;
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
48#[serde(rename_all = "snake_case")]
49pub enum BrokerType {
50 #[default]
52 TBank,
53 Binance,
55 InteractiveBrokers,
57 Simulator,
59 Custom(String),
61}
62
63impl BrokerType {
64 #[must_use]
66 pub fn from_name(name: &str) -> Self {
67 match name.to_lowercase().as_str() {
68 "tbank" | "t-bank" | "tinkoff" => Self::TBank,
69 "binance" => Self::Binance,
70 "ib" | "ibkr" | "interactive_brokers" | "interactivebrokers" => {
71 Self::InteractiveBrokers
72 }
73 "sim" | "simulator" | "simulated" | "demo" => Self::Simulator,
74 _ => Self::Custom(name.to_string()),
75 }
76 }
77
78 #[must_use]
80 pub fn as_str(&self) -> &str {
81 match self {
82 Self::TBank => "tbank",
83 Self::Binance => "binance",
84 Self::InteractiveBrokers => "interactive_brokers",
85 Self::Simulator => "simulator",
86 Self::Custom(name) => name,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Account {
94 pub id: String,
96
97 #[serde(default)]
99 pub name: String,
100
101 #[serde(default)]
103 pub exchange_account_id: String,
104
105 pub allocation: Allocation,
107
108 #[serde(default = "default_balance_interval")]
110 pub balance_interval_secs: u64,
111
112 #[serde(default = "default_order_delay")]
114 pub order_delay_ms: u64,
115
116 #[serde(default = "default_min_trade_percent")]
118 pub min_trade_percent: Decimal,
119
120 #[serde(default = "default_tolerance")]
122 pub tolerance_percent: Decimal,
123}
124
125fn default_balance_interval() -> u64 {
126 3600 }
128
129fn default_order_delay() -> u64 {
130 1000 }
132
133fn default_min_trade_percent() -> Decimal {
134 Decimal::new(1, 1) }
136
137fn default_tolerance() -> Decimal {
138 Decimal::new(5, 1) }
140
141impl Account {
142 #[must_use]
144 pub fn new(id: impl Into<String>, allocation: Allocation) -> Self {
145 let id = id.into();
146 Self {
147 name: id.clone(),
148 id,
149 exchange_account_id: String::new(),
150 allocation,
151 balance_interval_secs: default_balance_interval(),
152 order_delay_ms: default_order_delay(),
153 min_trade_percent: default_min_trade_percent(),
154 tolerance_percent: default_tolerance(),
155 }
156 }
157
158 #[must_use]
160 pub fn with_name(mut self, name: impl Into<String>) -> Self {
161 self.name = name.into();
162 self
163 }
164
165 #[must_use]
167 pub fn with_exchange_id(mut self, id: impl Into<String>) -> Self {
168 self.exchange_account_id = id.into();
169 self
170 }
171
172 #[must_use]
174 pub fn with_balance_interval(mut self, secs: u64) -> Self {
175 self.balance_interval_secs = secs;
176 self
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct User {
183 #[serde(default)]
185 pub id: String,
186
187 #[serde(default)]
189 pub name: String,
190
191 pub broker: BrokerType,
193
194 pub token_env_var: String,
196
197 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
199 pub credentials: HashMap<String, String>,
200
201 #[serde(default)]
203 pub accounts: Vec<Account>,
204
205 #[serde(default = "default_true")]
207 pub enabled: bool,
208}
209
210fn default_true() -> bool {
211 true
212}
213
214impl User {
215 #[must_use]
217 pub fn new(broker: BrokerType, token_env_var: impl Into<String>) -> Self {
218 Self {
219 id: uuid::Uuid::new_v4().to_string(),
220 name: String::new(),
221 broker,
222 token_env_var: token_env_var.into(),
223 credentials: HashMap::new(),
224 accounts: Vec::new(),
225 enabled: true,
226 }
227 }
228
229 #[must_use]
231 pub fn with_id(mut self, id: impl Into<String>) -> Self {
232 self.id = id.into();
233 self
234 }
235
236 #[must_use]
238 pub fn with_name(mut self, name: impl Into<String>) -> Self {
239 self.name = name.into();
240 self
241 }
242
243 #[must_use]
245 pub fn with_credential(mut self, key: impl Into<String>, env_var: impl Into<String>) -> Self {
246 self.credentials.insert(key.into(), env_var.into());
247 self
248 }
249
250 pub fn add_account(&mut self, account: Account) {
252 self.accounts.push(account);
253 }
254
255 #[must_use]
257 pub fn get_account(&self, id: &str) -> Option<&Account> {
258 self.accounts.iter().find(|a| a.id == id)
259 }
260
261 pub fn get_account_mut(&mut self, id: &str) -> Option<&mut Account> {
263 self.accounts.iter_mut().find(|a| a.id == id)
264 }
265
266 pub fn active_accounts(&self) -> impl Iterator<Item = &Account> {
268 self.accounts.iter()
269 }
270
271 #[must_use]
273 pub fn all_tickers(&self) -> Vec<String> {
274 let mut tickers = Vec::new();
275 for account in &self.accounts {
276 for ticker in account.allocation.all_tickers() {
277 if !tickers.contains(&ticker) {
278 tickers.push(ticker);
279 }
280 }
281 }
282 tickers
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, Default)]
288pub struct Settings {
289 #[serde(default = "default_log_level")]
291 pub log_level: String,
292
293 #[serde(default)]
295 pub verbose: bool,
296
297 #[serde(default)]
299 pub dry_run: bool,
300
301 #[serde(default)]
303 pub plan: bool,
304}
305
306fn default_log_level() -> String {
307 "info".to_string()
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, Default)]
312pub struct TradingConfig {
313 #[serde(default)]
315 pub settings: Settings,
316
317 #[serde(default)]
319 pub users: Vec<User>,
320}
321
322impl TradingConfig {
323 #[must_use]
325 pub fn new() -> Self {
326 Self::default()
327 }
328
329 pub fn add_user(&mut self, user: User) {
331 self.users.push(user);
332 }
333
334 #[must_use]
336 pub fn get_user(&self, id: &str) -> Option<&User> {
337 self.users.iter().find(|u| u.id == id)
338 }
339
340 pub fn active_users(&self) -> impl Iterator<Item = &User> {
342 self.users.iter().filter(|u| u.enabled)
343 }
344
345 pub fn all_accounts(&self) -> impl Iterator<Item = (&User, &Account)> {
347 self.users
348 .iter()
349 .flat_map(|u| u.accounts.iter().map(move |a| (u, a)))
350 }
351
352 pub fn validate(&self) -> Result<(), ConfigValidationError> {
354 if self.users.is_empty() {
355 return Err(ConfigValidationError::NoUsers);
356 }
357
358 for user in &self.users {
359 if user.token_env_var.is_empty() {
360 return Err(ConfigValidationError::MissingToken(user.id.clone()));
361 }
362
363 if user.accounts.is_empty() {
364 return Err(ConfigValidationError::NoAccounts(user.id.clone()));
365 }
366
367 for account in &user.accounts {
368 if !account.allocation.is_valid() {
369 return Err(ConfigValidationError::InvalidAllocation(
370 user.id.clone(),
371 account.id.clone(),
372 ));
373 }
374 }
375 }
376
377 Ok(())
378 }
379}
380
381#[derive(Debug, Clone)]
383pub enum ConfigValidationError {
384 NoUsers,
386 MissingToken(String),
388 NoAccounts(String),
390 InvalidAllocation(String, String),
392}
393
394impl std::fmt::Display for ConfigValidationError {
395 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396 match self {
397 Self::NoUsers => write!(f, "No users configured"),
398 Self::MissingToken(user) => write!(f, "Missing API token for user '{user}'"),
399 Self::NoAccounts(user) => write!(f, "No accounts configured for user '{user}'"),
400 Self::InvalidAllocation(user, account) => {
401 write!(
402 f,
403 "Invalid allocation for account '{account}' of user '{user}'"
404 )
405 }
406 }
407 }
408}
409
410impl std::error::Error for ConfigValidationError {}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use rust_decimal_macros::dec;
416
417 #[test]
418 fn test_broker_type_from_name() {
419 assert_eq!(BrokerType::from_name("tbank"), BrokerType::TBank);
420 assert_eq!(BrokerType::from_name("BINANCE"), BrokerType::Binance);
421 assert_eq!(BrokerType::from_name("ib"), BrokerType::InteractiveBrokers);
422 assert_eq!(BrokerType::from_name("simulator"), BrokerType::Simulator);
423 assert_eq!(
424 BrokerType::from_name("custom"),
425 BrokerType::Custom("custom".to_string())
426 );
427 }
428
429 #[test]
430 fn test_create_user() {
431 let user = User::new(BrokerType::TBank, "TBANK_TOKEN")
432 .with_name("Test User")
433 .with_credential("secret", "TBANK_SECRET");
434
435 assert_eq!(user.broker, BrokerType::TBank);
436 assert_eq!(user.token_env_var, "TBANK_TOKEN");
437 assert_eq!(user.name, "Test User");
438 assert_eq!(
439 user.credentials.get("secret"),
440 Some(&"TBANK_SECRET".to_string())
441 );
442 }
443
444 #[test]
445 fn test_create_account() {
446 let alloc = Allocation::from_simple(&[
447 ("SBER".to_string(), dec!(50)),
448 ("LKOH".to_string(), dec!(50)),
449 ]);
450
451 let account = Account::new("main", alloc)
452 .with_name("Main Account")
453 .with_exchange_id("12345");
454
455 assert_eq!(account.id, "main");
456 assert_eq!(account.name, "Main Account");
457 assert_eq!(account.exchange_account_id, "12345");
458 }
459
460 #[test]
461 fn test_config_validation() {
462 let mut config = TradingConfig::new();
463
464 assert!(config.validate().is_err());
466
467 let user = User::new(BrokerType::TBank, "TOKEN");
469 config.add_user(user);
470 assert!(config.validate().is_err());
471
472 let alloc =
474 Allocation::from_simple(&[("A".to_string(), dec!(50)), ("B".to_string(), dec!(50))]);
475 config.users[0].add_account(Account::new("main", alloc));
476
477 assert!(config.validate().is_ok());
479 }
480
481 #[test]
482 fn test_all_tickers() {
483 let mut user = User::new(BrokerType::TBank, "TOKEN");
484
485 let alloc1 = Allocation::from_simple(&[
486 ("SBER".to_string(), dec!(50)),
487 ("LKOH".to_string(), dec!(50)),
488 ]);
489 user.add_account(Account::new("acc1", alloc1));
490
491 let alloc2 = Allocation::from_simple(&[("GAZP".to_string(), dec!(100))]);
492 user.add_account(Account::new("acc2", alloc2));
493
494 let tickers = user.all_tickers();
495 assert!(tickers.contains(&"SBER".to_string()));
496 assert!(tickers.contains(&"LKOH".to_string()));
497 assert!(tickers.contains(&"GAZP".to_string()));
498 }
499}