aparently backtests where not thesame as live
This commit is contained in:
143
src/bot.rs
143
src/bot.rs
@@ -9,9 +9,10 @@ use crate::alpaca::AlpacaClient;
|
|||||||
use crate::config::{
|
use crate::config::{
|
||||||
get_all_symbols, IndicatorParams, Timeframe, BOT_CHECK_INTERVAL_SECONDS, HOURS_PER_DAY,
|
get_all_symbols, IndicatorParams, Timeframe, BOT_CHECK_INTERVAL_SECONDS, HOURS_PER_DAY,
|
||||||
MAX_POSITION_SIZE, MIN_CASH_RESERVE, STOP_LOSS_PCT, TAKE_PROFIT_PCT,
|
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::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};
|
use crate::types::{EquitySnapshot, PositionInfo, Signal, TradeSignal};
|
||||||
|
|
||||||
/// Live trading bot for paper trading.
|
/// Live trading bot for paper trading.
|
||||||
@@ -20,6 +21,7 @@ pub struct TradingBot {
|
|||||||
params: IndicatorParams,
|
params: IndicatorParams,
|
||||||
timeframe: Timeframe,
|
timeframe: Timeframe,
|
||||||
entry_prices: HashMap<String, f64>,
|
entry_prices: HashMap<String, f64>,
|
||||||
|
high_water_marks: HashMap<String, f64>,
|
||||||
equity_history: Vec<EquitySnapshot>,
|
equity_history: Vec<EquitySnapshot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,11 +39,13 @@ impl TradingBot {
|
|||||||
params: timeframe.params(),
|
params: timeframe.params(),
|
||||||
timeframe,
|
timeframe,
|
||||||
entry_prices: HashMap::new(),
|
entry_prices: HashMap::new(),
|
||||||
|
high_water_marks: HashMap::new(),
|
||||||
equity_history: Vec::new(),
|
equity_history: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load persisted state
|
// Load persisted state
|
||||||
bot.load_entry_prices();
|
bot.load_entry_prices();
|
||||||
|
bot.load_high_water_marks();
|
||||||
bot.load_equity_history();
|
bot.load_equity_history();
|
||||||
|
|
||||||
// Log account info
|
// 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::<HashMap<String, f64>>(&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.
|
/// Load equity history from file.
|
||||||
fn load_equity_history(&mut self) {
|
fn load_equity_history(&mut self) {
|
||||||
if LIVE_EQUITY_FILE.exists() {
|
if LIVE_EQUITY_FILE.exists() {
|
||||||
@@ -215,8 +251,8 @@ impl TradingBot {
|
|||||||
(position_value / price).floor() as u64
|
(position_value / price).floor() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if stop-loss or take-profit should trigger.
|
/// Check if stop-loss, take-profit, or trailing stop should trigger.
|
||||||
fn check_stop_loss_take_profit(&self, symbol: &str, current_price: f64) -> Option<Signal> {
|
fn check_stop_loss_take_profit(&mut self, symbol: &str, current_price: f64) -> Option<Signal> {
|
||||||
let entry_price = match self.entry_prices.get(symbol) {
|
let entry_price = match self.entry_prices.get(symbol) {
|
||||||
Some(&p) => p,
|
Some(&p) => p,
|
||||||
None => return None,
|
None => return None,
|
||||||
@@ -224,16 +260,40 @@ impl TradingBot {
|
|||||||
|
|
||||||
let pnl_pct = (current_price - entry_price) / entry_price;
|
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 {
|
if pnl_pct <= -STOP_LOSS_PCT {
|
||||||
tracing::warn!("{}: Stop-loss triggered at {:.2}% loss", symbol, pnl_pct * 100.0);
|
tracing::warn!("{}: Stop-loss triggered at {:.2}% loss", symbol, pnl_pct * 100.0);
|
||||||
return Some(Signal::StrongSell);
|
return Some(Signal::StrongSell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take profit
|
||||||
if pnl_pct >= TAKE_PROFIT_PCT {
|
if pnl_pct >= TAKE_PROFIT_PCT {
|
||||||
tracing::info!("{}: Take-profit triggered at {:.2}% gain", symbol, pnl_pct * 100.0);
|
tracing::info!("{}: Take-profit triggered at {:.2}% gain", symbol, pnl_pct * 100.0);
|
||||||
return Some(Signal::Sell);
|
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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +320,9 @@ impl TradingBot {
|
|||||||
{
|
{
|
||||||
Ok(_order) => {
|
Ok(_order) => {
|
||||||
self.entry_prices.insert(symbol.to_string(), signal.current_price);
|
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_entry_prices();
|
||||||
|
self.save_high_water_marks();
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"BUY ORDER EXECUTED: {} - {} shares @ ~${:.2} \
|
"BUY ORDER EXECUTED: {} - {} shares @ ~${:.2} \
|
||||||
@@ -303,6 +365,8 @@ impl TradingBot {
|
|||||||
tracing::info!("{}: Realized P&L: {:.2}%", symbol, pnl_pct * 100.0);
|
tracing::info!("{}: Realized P&L: {:.2}%", symbol, pnl_pct * 100.0);
|
||||||
self.save_entry_prices();
|
self.save_entry_prices();
|
||||||
}
|
}
|
||||||
|
self.high_water_marks.remove(symbol);
|
||||||
|
self.save_high_water_marks();
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"SELL ORDER EXECUTED: {} - {} shares @ ~${:.2} \
|
"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<TradeSignal> {
|
async fn analyze_symbol(&self, symbol: &str) -> Option<TradeSignal> {
|
||||||
let min_bars = self.params.min_bars();
|
let min_bars = self.params.min_bars();
|
||||||
|
|
||||||
@@ -364,14 +428,7 @@ impl TradingBot {
|
|||||||
let current = &indicators[indicators.len() - 1];
|
let current = &indicators[indicators.len() - 1];
|
||||||
let previous = &indicators[indicators.len() - 2];
|
let previous = &indicators[indicators.len() - 2];
|
||||||
|
|
||||||
let mut signal = generate_signal(symbol, current, previous);
|
Some(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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute one complete trading cycle.
|
/// Execute one complete trading cycle.
|
||||||
@@ -382,7 +439,9 @@ impl TradingBot {
|
|||||||
|
|
||||||
let symbols = get_all_symbols();
|
let symbols = get_all_symbols();
|
||||||
|
|
||||||
for symbol in symbols {
|
// Analyze all symbols first
|
||||||
|
let mut signals: Vec<TradeSignal> = Vec::new();
|
||||||
|
for symbol in &symbols {
|
||||||
tracing::info!("\nAnalyzing {}...", symbol);
|
tracing::info!("\nAnalyzing {}...", symbol);
|
||||||
|
|
||||||
let signal = match self.analyze_symbol(symbol).await {
|
let signal = match self.analyze_symbol(symbol).await {
|
||||||
@@ -396,7 +455,7 @@ impl TradingBot {
|
|||||||
tracing::info!(
|
tracing::info!(
|
||||||
"{}: Signal={}, RSI={:.1}, MACD Hist={:.3}, Momentum={:.2}%, \
|
"{}: Signal={}, RSI={:.1}, MACD Hist={:.3}, Momentum={:.2}%, \
|
||||||
Price=${:.2}, Confidence={:.2}",
|
Price=${:.2}, Confidence={:.2}",
|
||||||
symbol,
|
signal.symbol,
|
||||||
signal.signal.as_str(),
|
signal.signal.as_str(),
|
||||||
signal.rsi,
|
signal.rsi,
|
||||||
signal.macd_histogram,
|
signal.macd_histogram,
|
||||||
@@ -405,16 +464,56 @@ impl TradingBot {
|
|||||||
signal.confidence
|
signal.confidence
|
||||||
);
|
);
|
||||||
|
|
||||||
if signal.signal.is_buy() {
|
signals.push(signal);
|
||||||
self.execute_buy(symbol, &signal).await;
|
|
||||||
} else if signal.signal.is_sell() {
|
// Small delay between symbols for rate limiting
|
||||||
self.execute_sell(symbol, &signal).await;
|
sleep(TokioDuration::from_millis(500)).await;
|
||||||
} else {
|
}
|
||||||
tracing::info!("{}: Holding position (no action)", symbol);
|
|
||||||
|
// 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
|
if effective_signal.signal.is_sell() {
|
||||||
sleep(TokioDuration::from_millis(500)).await;
|
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<String> = 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
|
// Save equity snapshot for dashboard
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ lazy_static! {
|
|||||||
path
|
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.
|
/// Path to the trading log file.
|
||||||
pub static ref LOG_FILE: PathBuf = {
|
pub static ref LOG_FILE: PathBuf = {
|
||||||
let mut path = DATA_DIR.clone();
|
let mut path = DATA_DIR.clone();
|
||||||
|
|||||||
Reference in New Issue
Block a user