From 426bc6914a028012b4df28d538e92529f3ca7584 Mon Sep 17 00:00:00 2001 From: zastian-dev Date: Tue, 10 Feb 2026 18:47:25 +0000 Subject: [PATCH] aparently backtests where not thesame as live --- src/bot.rs | 143 +++++++++++++++++++++++++++++++++++++++++++-------- src/paths.rs | 7 +++ 2 files changed, 128 insertions(+), 22 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index 48560be..54d62d7 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -9,9 +9,10 @@ use crate::alpaca::AlpacaClient; use crate::config::{ get_all_symbols, IndicatorParams, Timeframe, BOT_CHECK_INTERVAL_SECONDS, HOURS_PER_DAY, MAX_POSITION_SIZE, MIN_CASH_RESERVE, STOP_LOSS_PCT, TAKE_PROFIT_PCT, + TOP_MOMENTUM_COUNT, TRAILING_STOP_ACTIVATION, TRAILING_STOP_DISTANCE, }; use crate::indicators::{calculate_all_indicators, generate_signal}; -use crate::paths::{LIVE_EQUITY_FILE, LIVE_POSITIONS_FILE}; +use crate::paths::{LIVE_EQUITY_FILE, LIVE_HIGH_WATER_MARKS_FILE, LIVE_POSITIONS_FILE}; use crate::types::{EquitySnapshot, PositionInfo, Signal, TradeSignal}; /// Live trading bot for paper trading. @@ -20,6 +21,7 @@ pub struct TradingBot { params: IndicatorParams, timeframe: Timeframe, entry_prices: HashMap, + high_water_marks: HashMap, equity_history: Vec, } @@ -37,11 +39,13 @@ impl TradingBot { params: timeframe.params(), timeframe, entry_prices: HashMap::new(), + high_water_marks: HashMap::new(), equity_history: Vec::new(), }; // Load persisted state bot.load_entry_prices(); + bot.load_high_water_marks(); bot.load_equity_history(); // Log account info @@ -84,6 +88,38 @@ impl TradingBot { } } + /// Load high water marks from file. + fn load_high_water_marks(&mut self) { + if LIVE_HIGH_WATER_MARKS_FILE.exists() { + match std::fs::read_to_string(&*LIVE_HIGH_WATER_MARKS_FILE) { + Ok(content) => { + if !content.is_empty() { + match serde_json::from_str::>(&content) { + Ok(marks) => { + tracing::info!("Loaded high water marks for {} positions.", marks.len()); + self.high_water_marks = marks; + } + Err(e) => tracing::error!("Error parsing high water marks file: {}", e), + } + } + } + Err(e) => tracing::error!("Error loading high water marks file: {}", e), + } + } + } + + /// Save high water marks to file. + fn save_high_water_marks(&self) { + match serde_json::to_string_pretty(&self.high_water_marks) { + Ok(json) => { + if let Err(e) = std::fs::write(&*LIVE_HIGH_WATER_MARKS_FILE, json) { + tracing::error!("Error saving high water marks file: {}", e); + } + } + Err(e) => tracing::error!("Error serializing high water marks: {}", e), + } + } + /// Load equity history from file. fn load_equity_history(&mut self) { if LIVE_EQUITY_FILE.exists() { @@ -215,8 +251,8 @@ impl TradingBot { (position_value / price).floor() as u64 } - /// Check if stop-loss or take-profit should trigger. - fn check_stop_loss_take_profit(&self, symbol: &str, current_price: f64) -> Option { + /// Check if stop-loss, take-profit, or trailing stop should trigger. + fn check_stop_loss_take_profit(&mut self, symbol: &str, current_price: f64) -> Option { let entry_price = match self.entry_prices.get(symbol) { Some(&p) => p, None => return None, @@ -224,16 +260,40 @@ impl TradingBot { let pnl_pct = (current_price - entry_price) / entry_price; + // Update high water mark + if let Some(hwm) = self.high_water_marks.get_mut(symbol) { + if current_price > *hwm { + *hwm = current_price; + self.save_high_water_marks(); + } + } + + // Fixed stop loss if pnl_pct <= -STOP_LOSS_PCT { tracing::warn!("{}: Stop-loss triggered at {:.2}% loss", symbol, pnl_pct * 100.0); return Some(Signal::StrongSell); } + // Take profit if pnl_pct >= TAKE_PROFIT_PCT { tracing::info!("{}: Take-profit triggered at {:.2}% gain", symbol, pnl_pct * 100.0); return Some(Signal::Sell); } + // Trailing stop (only after activation threshold) + if pnl_pct >= TRAILING_STOP_ACTIVATION { + if let Some(&high_water) = self.high_water_marks.get(symbol) { + let trailing_stop_price = high_water * (1.0 - TRAILING_STOP_DISTANCE); + if current_price <= trailing_stop_price { + tracing::info!( + "{}: Trailing stop triggered at ${:.2} (peak: ${:.2}, stop: ${:.2})", + symbol, current_price, high_water, trailing_stop_price + ); + return Some(Signal::Sell); + } + } + } + None } @@ -260,7 +320,9 @@ impl TradingBot { { Ok(_order) => { self.entry_prices.insert(symbol.to_string(), signal.current_price); + self.high_water_marks.insert(symbol.to_string(), signal.current_price); self.save_entry_prices(); + self.save_high_water_marks(); tracing::info!( "BUY ORDER EXECUTED: {} - {} shares @ ~${:.2} \ @@ -303,6 +365,8 @@ impl TradingBot { tracing::info!("{}: Realized P&L: {:.2}%", symbol, pnl_pct * 100.0); self.save_entry_prices(); } + self.high_water_marks.remove(symbol); + self.save_high_water_marks(); tracing::info!( "SELL ORDER EXECUTED: {} - {} shares @ ~${:.2} \ @@ -323,7 +387,7 @@ impl TradingBot { } } - /// Analyze a symbol and generate trading signal. + /// Analyze a symbol and generate trading signal (without stop-loss check). async fn analyze_symbol(&self, symbol: &str) -> Option { let min_bars = self.params.min_bars(); @@ -364,14 +428,7 @@ impl TradingBot { let current = &indicators[indicators.len() - 1]; let previous = &indicators[indicators.len() - 2]; - let mut signal = generate_signal(symbol, current, previous); - - // Check stop-loss/take-profit - if let Some(sl_tp) = self.check_stop_loss_take_profit(symbol, signal.current_price) { - signal.signal = sl_tp; - } - - Some(signal) + Some(generate_signal(symbol, current, previous)) } /// Execute one complete trading cycle. @@ -382,7 +439,9 @@ impl TradingBot { let symbols = get_all_symbols(); - for symbol in symbols { + // Analyze all symbols first + let mut signals: Vec = Vec::new(); + for symbol in &symbols { tracing::info!("\nAnalyzing {}...", symbol); let signal = match self.analyze_symbol(symbol).await { @@ -396,7 +455,7 @@ impl TradingBot { tracing::info!( "{}: Signal={}, RSI={:.1}, MACD Hist={:.3}, Momentum={:.2}%, \ Price=${:.2}, Confidence={:.2}", - symbol, + signal.symbol, signal.signal.as_str(), signal.rsi, signal.macd_histogram, @@ -405,16 +464,56 @@ impl TradingBot { signal.confidence ); - if signal.signal.is_buy() { - self.execute_buy(symbol, &signal).await; - } else if signal.signal.is_sell() { - self.execute_sell(symbol, &signal).await; - } else { - tracing::info!("{}: Holding position (no action)", symbol); + signals.push(signal); + + // Small delay between symbols for rate limiting + sleep(TokioDuration::from_millis(500)).await; + } + + // Phase 1: Process all sells first (free up cash before buying) + for signal in &signals { + let mut effective_signal = signal.clone(); + + // Check stop-loss/take-profit/trailing stop + if let Some(sl_tp) = self.check_stop_loss_take_profit(&signal.symbol, signal.current_price) { + effective_signal.signal = sl_tp; } - // Small delay between symbols - sleep(TokioDuration::from_millis(500)).await; + if effective_signal.signal.is_sell() { + self.execute_sell(&signal.symbol, &effective_signal).await; + } + } + + // Phase 2: Momentum ranking - only buy top N momentum stocks + let mut ranked_signals: Vec<&TradeSignal> = signals + .iter() + .filter(|s| !s.momentum.is_nan() && s.momentum != 0.0) + .collect(); + ranked_signals.sort_by(|a, b| { + b.momentum.partial_cmp(&a.momentum).unwrap_or(std::cmp::Ordering::Equal) + }); + + let top_momentum_symbols: std::collections::HashSet = ranked_signals + .iter() + .take(TOP_MOMENTUM_COUNT) + .map(|s| s.symbol.clone()) + .collect(); + + tracing::info!( + "Top {} momentum stocks: {:?}", + TOP_MOMENTUM_COUNT, + top_momentum_symbols + ); + + // Phase 3: Process buys (only for top momentum stocks) + for signal in &signals { + if !top_momentum_symbols.contains(&signal.symbol) { + continue; + } + + if signal.signal.is_buy() { + self.execute_buy(&signal.symbol, signal).await; + } } // Save equity snapshot for dashboard diff --git a/src/paths.rs b/src/paths.rs index 6479875..9060746 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -30,6 +30,13 @@ lazy_static! { path }; + /// Path to the live high water marks JSON file. + pub static ref LIVE_HIGH_WATER_MARKS_FILE: PathBuf = { + let mut path = DATA_DIR.clone(); + path.push("live_high_water_marks.json"); + path + }; + /// Path to the trading log file. pub static ref LOG_FILE: PathBuf = { let mut path = DATA_DIR.clone();