1use 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#[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 #[arg(short, long, env = "TRADER_BOT_CONFIG")]
28 pub config: Option<PathBuf>,
29
30 #[arg(short, long, env = "LENV_FILE")]
32 pub lenv: Option<PathBuf>,
33
34 #[arg(long, default_value = "info", env = "TRADER_BOT_LOG_LEVEL")]
36 pub log_level: String,
37
38 #[arg(short, long, default_value = "false", env = "TRADER_BOT_VERBOSE")]
40 pub verbose: bool,
41
42 #[arg(long, default_value = "false", env = "TRADER_BOT_DRY_RUN")]
44 pub dry_run: bool,
45
46 #[arg(long, default_value = "false", env = "TRADER_BOT_PLAN")]
49 pub plan: bool,
50
51 #[arg(short, long, env = "TRADER_BOT_ACCOUNT")]
53 pub account: Option<String>,
54
55 #[arg(short, long, env = "TRADER_BOT_USER")]
57 pub user: Option<String>,
58
59 #[arg(long, env = "TRADER_BOT_BALANCE_INTERVAL")]
61 pub balance_interval: Option<u64>,
62
63 #[arg(long, env = "TRADER_BOT_ORDER_DELAY")]
65 pub order_delay: Option<u64>,
66
67 #[arg(long, default_value = "false", env = "TRADER_BOT_RUN_ONCE")]
69 pub run_once: bool,
70
71 #[arg(long, default_value = "false")]
73 pub demo: bool,
74}
75
76impl Cli {
77 pub fn parse_with_lenv() -> Self {
84 let _ = auto_load_lenv();
87
88 Self::parse()
90 }
91
92 #[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 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 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 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 pub fn merge_with_config(&self, mut config: AppConfig) -> AppConfig {
138 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 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#[derive(Debug, Clone)]
176#[allow(clippy::struct_excessive_bools)]
177pub struct RuntimeConfig {
178 pub config_path: Option<PathBuf>,
180 pub lenv_path: Option<PathBuf>,
182 pub log_level: Level,
184 pub verbose: bool,
186 pub dry_run: bool,
188 pub plan: bool,
190 pub account: Option<String>,
192 pub user: Option<String>,
194 pub balance_interval: Option<u64>,
196 pub order_delay: Option<u64>,
198 pub run_once: bool,
200 pub demo: bool,
202 pub app_config: Option<AppConfig>,
204}
205
206impl RuntimeConfig {
207 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 #[must_use]
231 #[allow(clippy::match_same_arms)]
232 pub fn from_env() -> Self {
233 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}