use rust_decimal_macros::dec;
use std::sync::Arc;
use tracing::{debug, error, info};
use tracing_subscriber::FmtSubscriber;
use trader_bot::cli::Cli;
use trader_bot::domain::{DesiredAllocation, PlannedOrder, PlannedOrders};
use trader_bot::prelude::*;
use trader_bot::simulator::SimulatedExchange;
#[tokio::main]
async fn main() {
let cli = Cli::parse_with_lenv();
let subscriber = FmtSubscriber::builder()
.with_max_level(cli.get_log_level())
.finish();
tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber");
info!("Trader Bot v{}", trader_bot::VERSION);
debug!("CLI options: {:?}", cli);
match cli.load_config() {
Ok(Some(config)) => {
let config = cli.merge_with_config(config);
info!(
"Loaded configuration with {} account(s)",
config.all_accounts().count()
);
if cli.verbose {
debug!("Config: {:?}", config);
}
run_with_config(&cli, config).await;
}
Ok(None) => {
if cli.demo {
info!("Running demo mode...");
if cli.plan {
info!("PLAN MODE enabled - orders will be calculated but not executed");
}
demo_simulation(&cli).await;
} else {
info!("No configuration file found. Use --config to specify one, or --demo for demo mode.");
info!("Looking for: config.json, trader-bot.json, or .trader-bot.json");
}
}
Err(e) => {
error!("Failed to load configuration: {}", e);
std::process::exit(1);
}
}
}
#[allow(clippy::unused_async)]
async fn run_with_config(cli: &Cli, config: trader_bot::config::AppConfig) {
info!("Starting trading bot...");
if cli.plan {
info!("Running in PLAN MODE - orders will be calculated but not executed");
} else if cli.dry_run || config.settings.dry_run {
info!("Running in DRY-RUN mode - no actual trades will be executed");
}
let accounts: Vec<_> = match (&cli.account, &cli.user) {
(Some(account_id), _) => config
.all_accounts()
.filter(|a| a.id == *account_id)
.collect(),
(None, Some(user_id)) => config
.get_user(user_id)
.map(|u| u.accounts.iter().collect())
.unwrap_or_default(),
(None, None) => config.all_accounts().collect(),
};
if accounts.is_empty() {
error!("No accounts found matching criteria");
return;
}
info!("Processing {} account(s)", accounts.len());
for account in accounts {
info!("Account: {} ({})", account.name, account.id);
info!(" Exchange: {}", account.exchange);
info!(" Allocation mode: {:?}", account.allocation_mode);
if !account.desired_allocation.is_empty() {
info!(" Target allocation:");
for (symbol, pct) in &account.desired_allocation {
info!(" {}: {}%", symbol, pct);
}
}
if cli.plan {
info!(" Would calculate rebalancing plan (plan mode)...");
} else if cli.run_once {
info!(" Would execute single rebalance...");
} else {
info!(
" Would start continuous rebalancing (interval: {}s)",
account.balance_interval_secs
);
}
}
info!("Configuration loaded successfully. Exchange adapters are placeholders.");
info!("Use --demo for a working demonstration with simulated exchange.");
if cli.plan {
info!("Use --demo --plan to see plan mode in action with simulated data.");
}
}
async fn demo_simulation(cli: &Cli) {
info!("Running simulation demo...");
let exchange = SimulatedExchange::new("USD");
exchange.add_instrument("AAPL", "Apple Inc.", dec!(150), 1);
exchange.add_instrument("GOOGL", "Alphabet Inc.", dec!(2800), 1);
exchange.add_instrument("MSFT", "Microsoft Corp.", dec!(400), 1);
exchange.add_instrument("AMZN", "Amazon.com Inc.", dec!(180), 1);
let account_id = "demo_account";
exchange.create_account(account_id, dec!(100000));
info!("Created demo account with $100,000");
let mut target = DesiredAllocation::new();
target.set("AAPL", dec!(30));
target.set("GOOGL", dec!(25));
target.set("MSFT", dec!(25));
target.set("AMZN", dec!(20));
info!("Target allocation:");
info!(" AAPL: 30%");
info!(" GOOGL: 25%");
info!(" MSFT: 25%");
info!(" AMZN: 20%");
let exchange = Arc::new(exchange);
let order_delay = cli.order_delay.unwrap_or(100);
let effective_dry_run = cli.dry_run || cli.plan;
let config = BalancerConfig {
dry_run: effective_dry_run,
order_delay_ms: order_delay,
..BalancerConfig::default()
};
let mut engine = BalancerEngine::new(Arc::clone(&exchange), config);
if cli.plan {
info!("PLAN MODE enabled - calculating orders without executing");
} else if cli.dry_run {
info!("DRY-RUN mode enabled - simulating without actual orders");
}
if cli.plan {
info!("Creating rebalancing plan...");
match engine.create_plan(account_id, &target).await {
Ok(plan) => {
let mut planned_orders = PlannedOrders::new();
if plan.is_empty() {
info!("No rebalancing needed - portfolio is already balanced.");
if let Some(reason) = &plan.empty_reason {
info!("Reason: {}", reason);
}
} else {
for action in &plan.actions {
let order = PlannedOrder::from_rebalance_action(action, account_id);
planned_orders.add_order(order);
}
planned_orders.set_total_buy_value(plan.total_buy_value.clone());
planned_orders.set_total_sell_value(plan.total_sell_value.clone());
planned_orders.display();
info!("Plan summary: {}", plan.summary());
}
}
Err(e) => {
error!("Failed to create plan: {}", e);
}
}
} else {
info!("Running initial rebalance...");
match engine.rebalance(account_id, &target).await {
Ok(result) => {
info!("Rebalance complete!");
info!(" Trades executed: {}", result.executed.len());
if !result.executed.is_empty() {
for action in &result.executed {
info!(
" {} {} lots of {} @ {}",
if action.is_buy() { "BUY" } else { "SELL" },
action.lots,
action.symbol,
action.estimated_price
);
}
}
}
Err(e) => {
error!("Rebalance failed: {}", e);
}
}
}
match exchange.get_wallet(account_id).await {
Ok(wallet) => {
if cli.plan {
info!("Current portfolio (unchanged - plan mode):");
} else {
info!("Final portfolio:");
}
info!(" Cash: {}", wallet.cash());
if wallet.positions().is_empty() {
info!(" Positions: none");
} else {
info!(" Positions:");
for pos in wallet.positions() {
info!(
" {}: {} units @ {} = {}",
pos.symbol(),
pos.quantity(),
pos.current_price(),
pos.market_value()
);
}
}
info!(" Total value: {}", wallet.total_value());
let alloc = wallet.current_allocation();
if !alloc.is_empty() {
info!("Current allocation:");
for (symbol, pct) in alloc.iter() {
info!(" {}: {:.2}%", symbol, pct);
}
}
}
Err(e) => {
error!("Failed to get wallet: {}", e);
}
}
info!("Demo complete!");
}