- Added HOURLY_ATR_STOP_MULTIPLIER (1.8x) vs daily (3.5x) - Added hourly-specific trail multipliers - Strategy now uses timeframe field to select appropriate stops - Tested multiple configurations on hourly: * 3.5x stops: -0.5% return, 45% max DD * 1.8x stops: -45% return, 53% max DD (worse) * Conservative regime (0.25x): -65% return, 67% max DD (terrible) - Conclusion: Hourly doesn't work with this strategy - Daily with relaxed regime remains best: +17.4% over 5yr, 24% max DD Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
249 lines
8.7 KiB
Rust
249 lines
8.7 KiB
Rust
//! Tech Giants Trading Bot - Algorithmic trading system using Alpaca API.
|
|
//!
|
|
//! A momentum + mean-reversion hybrid strategy for 50 stocks across multiple sectors.
|
|
//! Uses RSI, MACD, EMA, ADX, Bollinger Bands, and momentum indicators for trade signals.
|
|
//!
|
|
//! # Usage
|
|
//!
|
|
//! ```bash
|
|
//! # Live paper trading
|
|
//! invest-bot
|
|
//!
|
|
//! # Backtest with historical data
|
|
//! invest-bot --backtest --years 3
|
|
//! invest-bot --backtest --years 5 --capital 50000
|
|
//! ```
|
|
|
|
mod alpaca;
|
|
mod backtester;
|
|
mod bot;
|
|
mod config;
|
|
mod dashboard;
|
|
mod indicators;
|
|
mod paths;
|
|
mod strategy;
|
|
mod types;
|
|
|
|
use anyhow::{Context, Result};
|
|
use clap::Parser;
|
|
use std::env;
|
|
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
|
|
|
use crate::alpaca::AlpacaClient;
|
|
use crate::backtester::{save_backtest_results, Backtester};
|
|
use crate::bot::TradingBot;
|
|
use crate::config::{Timeframe, DEFAULT_INITIAL_CAPITAL};
|
|
|
|
/// Vibe Invest - Trade with Alpaca
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "vibe-invest")]
|
|
#[command(author = "Vibe Invest Team")]
|
|
#[command(version = "0.1.0")]
|
|
#[command(about = "Tech Giants Trading Bot - Algorithmic trading system using Alpaca API")]
|
|
#[command(
|
|
long_about = "A momentum + mean-reversion hybrid strategy for 50 stocks across multiple sectors.\n\
|
|
Uses RSI, MACD, EMA, ADX, Bollinger Bands, and momentum indicators for trade signals.\n\n\
|
|
Examples:\n \
|
|
Live trading: invest-bot\n \
|
|
Live daily bars: invest-bot --timeframe daily\n \
|
|
Backtest 3 years: invest-bot --backtest --years 3\n \
|
|
Backtest 6 months: invest-bot --backtest --months 6\n \
|
|
Backtest 1y 6m: invest-bot --backtest --years 1 --months 6\n \
|
|
Custom capital: invest-bot --backtest --years 5 --capital 50000\n \
|
|
Hourly backtest: invest-bot --backtest --years 1 --timeframe hourly\n \
|
|
Custom date range: invest-bot --backtest --start-date 2007-01-01 --end-date 2008-12-31"
|
|
)]
|
|
struct Args {
|
|
/// Run in backtest mode instead of live trading
|
|
#[arg(short, long)]
|
|
backtest: bool,
|
|
|
|
/// Number of years to backtest (default: 1 if --months not specified)
|
|
#[arg(short, long, default_value_t = 0.0)]
|
|
years: f64,
|
|
|
|
/// Number of months to backtest (can combine with --years)
|
|
#[arg(short, long, default_value_t = 0.0)]
|
|
months: f64,
|
|
|
|
/// Start date for backtest (YYYY-MM-DD). Overrides --years/--months if provided.
|
|
#[arg(long, value_name = "YYYY-MM-DD")]
|
|
start_date: Option<String>,
|
|
|
|
/// End date for backtest (YYYY-MM-DD). Defaults to now if not provided.
|
|
#[arg(long, value_name = "YYYY-MM-DD")]
|
|
end_date: Option<String>,
|
|
|
|
/// Initial capital for backtesting
|
|
#[arg(short, long, default_value_t = DEFAULT_INITIAL_CAPITAL)]
|
|
capital: f64,
|
|
|
|
/// Timeframe for bars
|
|
#[arg(short, long, value_enum, default_value_t = Timeframe::Daily)]
|
|
timeframe: Timeframe,
|
|
}
|
|
|
|
fn setup_logging(backtest_mode: bool) {
|
|
let filter = EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| EnvFilter::new("info"));
|
|
|
|
if backtest_mode {
|
|
// Console only for backtest
|
|
tracing_subscriber::registry()
|
|
.with(filter)
|
|
.with(fmt::layer())
|
|
.init();
|
|
} else {
|
|
// Console + file for live trading
|
|
let log_path = &paths::LOG_FILE;
|
|
let log_dir = log_path.parent().unwrap();
|
|
let log_file_name = log_path.file_name().unwrap();
|
|
|
|
let file_appender = tracing_appender::rolling::never(log_dir, log_file_name);
|
|
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
|
|
|
tracing_subscriber::registry()
|
|
.with(filter)
|
|
.with(fmt::layer())
|
|
.with(fmt::layer().with_writer(non_blocking).with_ansi(false))
|
|
.init();
|
|
}
|
|
}
|
|
|
|
fn print_banner(backtest: bool) {
|
|
if backtest {
|
|
println!(
|
|
r#"
|
|
+---------------------------------------------------------+
|
|
| TECH GIANTS TRADING BOT - BACKTEST MODE |
|
|
+---------------------------------------------------------+
|
|
| Symbols: AAPL, MSFT, GOOGL, AMZN, META, NVDA, TSLA |
|
|
| Strategy: Momentum + RSI Mean Reversion + MACD |
|
|
+---------------------------------------------------------+
|
|
"#
|
|
);
|
|
} else {
|
|
let dashboard_port = env::var("DASHBOARD_PORT").unwrap_or_else(|_| "5000".to_string());
|
|
println!(
|
|
r#"
|
|
+---------------------------------------------------------+
|
|
| TECH GIANTS TRADING BOT - PAPER TRADING |
|
|
+---------------------------------------------------------+
|
|
| Symbols: AAPL, MSFT, GOOGL, AMZN, META, NVDA, TSLA |
|
|
| Strategy: Momentum + RSI Mean Reversion + MACD |
|
|
| Mode: Alpaca Paper Trading |
|
|
| Dashboard: http://localhost:{:<29}|
|
|
+---------------------------------------------------------+
|
|
"#,
|
|
dashboard_port
|
|
);
|
|
}
|
|
}
|
|
|
|
fn get_credentials() -> Result<(String, String)> {
|
|
let api_key = env::var("ALPACA_API_KEY").context(
|
|
"Missing ALPACA_API_KEY. Please set it in your .env file or environment.",
|
|
)?;
|
|
let api_secret = env::var("ALPACA_SECRET_KEY").context(
|
|
"Missing ALPACA_SECRET_KEY. Please set it in your .env file or environment.",
|
|
)?;
|
|
Ok((api_key, api_secret))
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Load .env file if present
|
|
dotenvy::dotenv().ok();
|
|
|
|
let args = Args::parse();
|
|
|
|
setup_logging(args.backtest);
|
|
print_banner(args.backtest);
|
|
|
|
let (api_key, api_secret) = match get_credentials() {
|
|
Ok(creds) => creds,
|
|
Err(e) => {
|
|
eprintln!("\nConfiguration Error: {}", e);
|
|
eprintln!("\nPlease create a .env file with your Alpaca credentials:");
|
|
eprintln!("ALPACA_API_KEY=your_api_key");
|
|
eprintln!("ALPACA_SECRET_KEY=your_secret_key");
|
|
eprintln!("\nGet your free paper trading API keys at: https://alpaca.markets/");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
if args.backtest {
|
|
run_backtest(api_key, api_secret, args).await
|
|
} else {
|
|
run_live_trading(api_key, api_secret, args).await
|
|
}
|
|
}
|
|
|
|
async fn run_backtest(api_key: String, api_secret: String, args: Args) -> Result<()> {
|
|
use chrono::NaiveDate;
|
|
|
|
let client = AlpacaClient::new(api_key, api_secret)?;
|
|
let mut backtester = Backtester::new(args.capital, args.timeframe);
|
|
|
|
let result = if args.start_date.is_some() || args.end_date.is_some() {
|
|
// Custom date range mode
|
|
let start_date = if let Some(ref s) = args.start_date {
|
|
NaiveDate::parse_from_str(s, "%Y-%m-%d")
|
|
.context("Invalid start date format. Use YYYY-MM-DD (e.g., 2007-01-01)")?
|
|
} else {
|
|
// If no start date provided, default to 1 year before end date
|
|
let end = if let Some(ref e) = args.end_date {
|
|
NaiveDate::parse_from_str(e, "%Y-%m-%d")?
|
|
} else {
|
|
chrono::Utc::now().date_naive()
|
|
};
|
|
end - chrono::Duration::days(365)
|
|
};
|
|
|
|
let end_date = if let Some(ref e) = args.end_date {
|
|
NaiveDate::parse_from_str(e, "%Y-%m-%d")
|
|
.context("Invalid end date format. Use YYYY-MM-DD (e.g., 2008-12-31)")?
|
|
} else {
|
|
chrono::Utc::now().date_naive()
|
|
};
|
|
|
|
// Validate date range
|
|
if start_date >= end_date {
|
|
anyhow::bail!("Start date must be before end date");
|
|
}
|
|
|
|
backtester.run_with_dates(&client, start_date, end_date).await?
|
|
} else {
|
|
// Years/months mode (existing behavior)
|
|
let total_years = args.years + (args.months / 12.0);
|
|
let total_years = if total_years <= 0.0 { 1.0 } else { total_years };
|
|
backtester.run(&client, total_years).await?
|
|
};
|
|
|
|
// Save results to CSV
|
|
save_backtest_results(&result)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn run_live_trading(api_key: String, api_secret: String, args: Args) -> Result<()> {
|
|
let dashboard_port: u16 = env::var("DASHBOARD_PORT")
|
|
.unwrap_or_else(|_| "5000".to_string())
|
|
.parse()
|
|
.unwrap_or(5000);
|
|
|
|
// Create a separate client for the dashboard
|
|
let dashboard_client = AlpacaClient::new(api_key.clone(), api_secret.clone())?;
|
|
|
|
// Spawn dashboard in background
|
|
tokio::spawn(async move {
|
|
if let Err(e) = dashboard::start_dashboard(dashboard_client, dashboard_port).await {
|
|
tracing::error!("Dashboard error: {}", e);
|
|
}
|
|
});
|
|
|
|
// Run the trading bot
|
|
let mut bot = TradingBot::new(api_key, api_secret, args.timeframe).await?;
|
|
bot.run().await
|
|
}
|