diff --git a/src/bot.rs b/src/bot.rs index c078352..df2a95b 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -10,6 +10,7 @@ use crate::{ account::AccountManager, executor::Executor, strategy::{Strategy, veloc::MomentumVelocityStrategy}, + tradelog::TradeLog, }; pub struct Bot { @@ -17,6 +18,7 @@ pub struct Bot { pub accounts: Mutex, pub executor: Mutex>, pub strategy: Mutex>, + pub trade_log: Mutex>, } impl Bot { @@ -70,6 +72,7 @@ impl Bot { executor: Mutex::new(Box::new(account.executor())), accounts: Mutex::new(accounts), strategy: Mutex::new(Box::new(MomentumVelocityStrategy::new())), + trade_log: Mutex::new(Vec::new()), })) } diff --git a/src/main.rs b/src/main.rs index c929842..32b793e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ pub mod bot; pub mod executor; pub mod strategy; pub mod types; +pub mod tradelog; use crate::bot::Bot; diff --git a/src/strategy/veloc.rs b/src/strategy/veloc.rs index a7aa753..a8db349 100644 --- a/src/strategy/veloc.rs +++ b/src/strategy/veloc.rs @@ -7,6 +7,7 @@ use rust_decimal::{Decimal, dec}; use crate::bot::Bot; use crate::strategy::Strategy; +use crate::tradelog::{ExitReason, TradeLog}; use crate::types::{NewToken, Trade, TradeType}; const BUY_AMOUNT_SOL: Decimal = dec!(0.2); @@ -22,7 +23,8 @@ struct TokenTracker { } struct OpenPosition { - entry_price_sol: f64, + trade: TradeLog, + highest_price_sol: f64, last_high_time: Instant, } @@ -133,7 +135,8 @@ impl Strategy for MomentumVelocityStrategy { // 1. Manage Active Positions (Take Profit / Stop Loss / Stall) // ------------------------------------------------------------- if let Some(pos) = self.positions.get_mut(mint) { - let price_change_pct = (current_price - pos.entry_price_sol) / pos.entry_price_sol; + let price_change_pct = + (current_price - pos.trade.entry_price_sol) / pos.trade.entry_price_sol; if current_price > pos.highest_price_sol { pos.highest_price_sol = current_price; @@ -143,38 +146,38 @@ impl Strategy for MomentumVelocityStrategy { let drop_from_peak = (pos.highest_price_sol - current_price) / pos.highest_price_sol; - let (should_sell, reason) = match () { - _ if price_change_pct >= 0.40 => ( - true, - format!("Take Profit (+{:.1}%)", price_change_pct * 100.0), - ), - _ if price_change_pct <= -0.1 => ( - true, - format!("Hard Stop Loss ({:.1}%)", price_change_pct * 100.0), - ), - _ if drop_from_peak >= 0.12 && price_change_pct > 0.10 => ( - true, - format!( - "Trailing Stop (Peak drop: {:.1}%, gain: +{:.1}%)", - drop_from_peak * 100.0, - price_change_pct * 100.0 - ), - ), - _ if pos.last_high_time.elapsed() >= Duration::from_secs(25) => { - (true, format!("Momentum Stalled (no high for 25s)")) + let should_sell = match () { + _ if price_change_pct >= 0.40 => Some(ExitReason::TakeProfit), + _ if price_change_pct <= -0.1 => Some(ExitReason::StopLoss), + _ if drop_from_peak >= 0.12 && price_change_pct > 0.10 => { + Some(ExitReason::TrailingStop) } - _ => (false, String::new()), + _ if pos.last_high_time.elapsed() >= Duration::from_secs(25) => { + Some(ExitReason::MomentumStalled) + } + _ => None, }; - if should_sell { - info!("[{}] EXECUTING SELL. Reason: {}", mint, reason); + if let Some(reason) = should_sell { + info!( + "[{}] EXECUTING SELL. Reason: {:?} {:.1}%", + mint, + reason, + price_change_pct * 100.0 + ); + bot.executor .lock() .await .sell_percent(mint, 100, PRIORITY, SLIPPAGE) .await?; - self.positions.remove(mint); + if let Some(mut pos) = self.positions.remove(mint) { + pos.trade.close(current_price, reason); + + info!("TRADE RESULT: {:?}", pos.trade); + } + self.cleanup_and_unsubscribe(&bot, mint).await?; } @@ -245,7 +248,13 @@ impl Strategy for MomentumVelocityStrategy { self.positions.insert( mint.clone(), OpenPosition { - entry_price_sol: current_price, + trade: TradeLog::new( + mint.clone(), + current_price, + tracker.unique_buyers.len(), + tracker.net_sol_flow, + trade.v_sol_in_bonding_curve, + ), highest_price_sol: current_price, last_high_time: Instant::now(), }, diff --git a/src/tradelog.rs b/src/tradelog.rs new file mode 100644 index 0000000..2a39d2e --- /dev/null +++ b/src/tradelog.rs @@ -0,0 +1,72 @@ +use std::time::{Duration, Instant}; + +#[derive(Debug, Clone)] +pub enum ExitReason { + TakeProfit, + StopLoss, + TrailingStop, + MomentumStalled, +} + +#[derive(Debug, Clone)] +pub struct TradeLog { + pub mint: String, + + pub opened_at: Instant, + pub closed_at: Option, + + pub entry_price_sol: f64, + pub exit_price_sol: Option, + + pub pnl_percent: Option, + pub duration: Option, + + pub exit_reason: Option, + + // Conditions when entered + pub unique_buyers: usize, + pub net_sol_flow: f64, + pub curve_sol: f64, +} + +impl TradeLog { + pub fn new( + mint: String, + entry_price_sol: f64, + unique_buyers: usize, + net_sol_flow: f64, + curve_sol: f64, + ) -> Self { + Self { + mint, + opened_at: Instant::now(), + closed_at: None, + + entry_price_sol, + exit_price_sol: None, + + pnl_percent: None, + duration: None, + + exit_reason: None, + + unique_buyers, + net_sol_flow, + curve_sol, + } + } + + pub fn close(&mut self, exit_price_sol: f64, reason: ExitReason) { + let now = Instant::now(); + + let pnl = ((exit_price_sol - self.entry_price_sol) / self.entry_price_sol) * 100.0; + + self.exit_price_sol = Some(exit_price_sol); + self.pnl_percent = Some(pnl); + + self.closed_at = Some(now); + self.duration = Some(now.duration_since(self.opened_at)); + + self.exit_reason = Some(reason); + } +}