Skip to main content

trader_bot/
cli.rs

1//! Command-line interface for the trading bot.
2//!
3//! Uses clap for argument parsing with Links Notation configuration support.
4//! Configuration is loaded with the following priority:
5//!
6//! 1. CLI arguments (highest priority)
7//! 2. Environment variables
8//! 3. Configuration file (--config or TRADER_BOT_CONFIG)
9//! 4. Default values
10
11use crate::config::{AppConfig, ConfigError};
12use crate::lino_args::{auto_load_lenv, getenv, getenv_bool, getenv_u64};
13use clap::Parser;
14use std::path::PathBuf;
15use tracing::Level;
16
17/// Trader Bot - A configurable trading bot with multi-strategy support.
18#[derive(Parser, Debug)]
19#[command(
20    name = "trader-bot",
21    version,
22    about = "A configurable trading bot with multi-exchange support, portfolio balancing, scalping strategies, and multi-account management"
23)]
24#[allow(clippy::struct_excessive_bools)]
25pub struct Cli {
26    /// Path to configuration file (JSON format).
27    #[arg(short, long, env = "TRADER_BOT_CONFIG")]
28    pub config: Option<PathBuf>,
29
30    /// Path to lenv file for environment variables.
31    #[arg(short, long, env = "LENV_FILE")]
32    pub lenv: Option<PathBuf>,
33
34    /// Log level (trace, debug, info, warn, error).
35    #[arg(long, default_value = "info", env = "TRADER_BOT_LOG_LEVEL")]
36    pub log_level: String,
37
38    /// Enable verbose output.
39    #[arg(short, long, default_value = "false", env = "TRADER_BOT_VERBOSE")]
40    pub verbose: bool,
41
42    /// Run in dry-run mode (no actual trades).
43    #[arg(long, default_value = "false", env = "TRADER_BOT_DRY_RUN")]
44    pub dry_run: bool,
45
46    /// Run in plan mode - connect to real market, calculate orders but don't execute them.
47    /// Displays detailed information about what orders would be placed.
48    #[arg(long, default_value = "false", env = "TRADER_BOT_PLAN")]
49    pub plan: bool,
50
51    /// Account ID to use (if not specified, uses all configured accounts).
52    #[arg(short, long, env = "TRADER_BOT_ACCOUNT")]
53    pub account: Option<String>,
54
55    /// User ID to use (for multi-user setups).
56    #[arg(short, long, env = "TRADER_BOT_USER")]
57    pub user: Option<String>,
58
59    /// Rebalancing interval in seconds (overrides config).
60    #[arg(long, env = "TRADER_BOT_BALANCE_INTERVAL")]
61    pub balance_interval: Option<u64>,
62
63    /// Delay between orders in milliseconds.
64    #[arg(long, env = "TRADER_BOT_ORDER_DELAY")]
65    pub order_delay: Option<u64>,
66
67    /// Run once and exit (instead of continuous operation).
68    #[arg(long, default_value = "false", env = "TRADER_BOT_RUN_ONCE")]
69    pub run_once: bool,
70
71    /// Run demo mode with simulated exchange.
72    #[arg(long, default_value = "false")]
73    pub demo: bool,
74}
75
76impl Cli {
77    /// Parses CLI arguments with Links Notation configuration support.
78    ///
79    /// This function:
80    /// 1. Attempts to auto-load lenv file
81    /// 2. Parses CLI arguments (which also reads env vars via clap)
82    /// 3. Applies priority chain for configuration
83    pub fn parse_with_lenv() -> Self {
84        // Auto-load lenv file first (before clap parses env vars)
85        // This ensures lenv values are available as env vars for clap
86        let _ = auto_load_lenv();
87
88        // Parse CLI with clap (will also use env vars)
89        Self::parse()
90    }
91
92    /// Gets the effective log level.
93    #[must_use]
94    #[allow(clippy::match_same_arms)]
95    pub fn get_log_level(&self) -> Level {
96        match self.log_level.to_lowercase().as_str() {
97            "trace" => Level::TRACE,
98            "debug" => Level::DEBUG,
99            "info" => Level::INFO,
100            "warn" | "warning" => Level::WARN,
101            "error" => Level::ERROR,
102            _ => Level::INFO,
103        }
104    }
105
106    /// Loads configuration from file if specified.
107    pub fn load_config(&self) -> Result<Option<AppConfig>, ConfigError> {
108        if let Some(ref path) = self.config {
109            let config = AppConfig::from_file(path)?;
110            config.validate()?;
111            return Ok(Some(config));
112        }
113
114        // Try to find config from environment or common locations
115        let config_path = getenv("TRADER_BOT_CONFIG", "");
116        if !config_path.is_empty() && std::path::Path::new(&config_path).exists() {
117            let config = AppConfig::from_file(&config_path)?;
118            config.validate()?;
119            return Ok(Some(config));
120        }
121
122        // Check common config file locations
123        for path in &["config.json", "trader-bot.json", ".trader-bot.json"] {
124            if std::path::Path::new(path).exists() {
125                let config = AppConfig::from_file(path)?;
126                config.validate()?;
127                return Ok(Some(config));
128            }
129        }
130
131        Ok(None)
132    }
133
134    /// Creates a merged configuration from CLI args and file config.
135    ///
136    /// CLI arguments take priority over file config values.
137    pub fn merge_with_config(&self, mut config: AppConfig) -> AppConfig {
138        // Override global settings from CLI
139        if self.verbose {
140            config.settings.verbose = true;
141        }
142        if self.dry_run {
143            config.settings.dry_run = true;
144        }
145        config.settings.log_level.clone_from(&self.log_level);
146
147        // Override account settings if specified
148        if let Some(interval) = self.balance_interval {
149            for account in &mut config.accounts {
150                account.balance_interval_secs = interval;
151            }
152            for user in &mut config.users {
153                for account in &mut user.accounts {
154                    account.balance_interval_secs = interval;
155                }
156            }
157        }
158
159        if let Some(delay) = self.order_delay {
160            for account in &mut config.accounts {
161                account.order_delay_ms = delay;
162            }
163            for user in &mut config.users {
164                for account in &mut user.accounts {
165                    account.order_delay_ms = delay;
166                }
167            }
168        }
169
170        config
171    }
172}
173
174/// Runtime configuration combining CLI, environment, and file config.
175#[derive(Debug, Clone)]
176#[allow(clippy::struct_excessive_bools)]
177pub struct RuntimeConfig {
178    /// Path to configuration file.
179    pub config_path: Option<PathBuf>,
180    /// Path to lenv file.
181    pub lenv_path: Option<PathBuf>,
182    /// Log level.
183    pub log_level: Level,
184    /// Verbose output enabled.
185    pub verbose: bool,
186    /// Dry run mode enabled.
187    pub dry_run: bool,
188    /// Plan mode enabled - shows what orders would be placed without executing.
189    pub plan: bool,
190    /// Specific account to use.
191    pub account: Option<String>,
192    /// Specific user to use.
193    pub user: Option<String>,
194    /// Rebalancing interval override.
195    pub balance_interval: Option<u64>,
196    /// Order delay override.
197    pub order_delay: Option<u64>,
198    /// Run once mode.
199    pub run_once: bool,
200    /// Demo mode.
201    pub demo: bool,
202    /// Application config from file.
203    pub app_config: Option<AppConfig>,
204}
205
206impl RuntimeConfig {
207    /// Creates runtime config from CLI and loaded file config.
208    pub fn from_cli(cli: Cli, app_config: Option<AppConfig>) -> Self {
209        let log_level = cli.get_log_level();
210        Self {
211            config_path: cli.config,
212            lenv_path: cli.lenv,
213            log_level,
214            verbose: cli.verbose,
215            dry_run: cli.dry_run,
216            plan: cli.plan,
217            account: cli.account,
218            user: cli.user,
219            balance_interval: cli.balance_interval,
220            order_delay: cli.order_delay,
221            run_once: cli.run_once,
222            demo: cli.demo,
223            app_config,
224        }
225    }
226
227    /// Creates runtime config from environment variables only (no CLI parsing).
228    ///
229    /// Useful for library usage or testing.
230    #[must_use]
231    #[allow(clippy::match_same_arms)]
232    pub fn from_env() -> Self {
233        // Auto-load lenv
234        let _ = auto_load_lenv();
235
236        let log_level = match getenv("TRADER_BOT_LOG_LEVEL", "info")
237            .to_lowercase()
238            .as_str()
239        {
240            "trace" => Level::TRACE,
241            "debug" => Level::DEBUG,
242            "info" => Level::INFO,
243            "warn" | "warning" => Level::WARN,
244            "error" => Level::ERROR,
245            _ => Level::INFO,
246        };
247
248        Self {
249            config_path: std::env::var("TRADER_BOT_CONFIG").ok().map(PathBuf::from),
250            lenv_path: std::env::var("LENV_FILE").ok().map(PathBuf::from),
251            log_level,
252            verbose: getenv_bool("TRADER_BOT_VERBOSE", false),
253            dry_run: getenv_bool("TRADER_BOT_DRY_RUN", false),
254            plan: getenv_bool("TRADER_BOT_PLAN", false),
255            account: std::env::var("TRADER_BOT_ACCOUNT").ok(),
256            user: std::env::var("TRADER_BOT_USER").ok(),
257            balance_interval: {
258                let v = getenv_u64("TRADER_BOT_BALANCE_INTERVAL", 0);
259                if v > 0 {
260                    Some(v)
261                } else {
262                    None
263                }
264            },
265            order_delay: {
266                let v = getenv_u64("TRADER_BOT_ORDER_DELAY", 0);
267                if v > 0 {
268                    Some(v)
269                } else {
270                    None
271                }
272            },
273            run_once: getenv_bool("TRADER_BOT_RUN_ONCE", false),
274            demo: false,
275            app_config: None,
276        }
277    }
278}