From 5d85e731d3c188346e2047134d7c03fc2f7e6ab4 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 7 Aug 2026 04:09:05 +0200 Subject: [PATCH] Velocity strategy --- Cargo.lock | 11 +++ Cargo.toml | 2 +- src/bot.rs | 8 +- src/executor/mod.rs | 6 +- src/executor/pump_fun.rs | 8 +- src/strategy/burst.rs | 58 ------------- src/strategy/mod.rs | 2 +- src/strategy/veloc.rs | 175 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 198 insertions(+), 72 deletions(-) delete mode 100644 src/strategy/burst.rs create mode 100644 src/strategy/veloc.rs diff --git a/Cargo.lock b/Cargo.lock index 185c41f..1528550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,11 +1453,22 @@ dependencies = [ "num-traits", "rand 0.8.7", "rkyv", + "rust_decimal_macros", "serde", "serde_json", "wasm-bindgen", ] +[[package]] +name = "rust_decimal_macros" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" +dependencies = [ + "quote", + "syn 2.0.119", +] + [[package]] name = "rustc-hash" version = "2.1.3" diff --git a/Cargo.toml b/Cargo.toml index a8a7081..37de6f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,6 @@ tokio = { version = "1.53.1", features = [ tokio-tungstenite = { version = "0.30.0", features = ["native-tls"] } log = "0.4.33" env_logger = "0.11.11" -rust_decimal = "1.42.1" +rust_decimal = { version = "1.42.1", features = ["macros"] } reqwest = { version = "0.13.4", features = ["json"] } async-trait = "0.1.91" diff --git a/src/bot.rs b/src/bot.rs index fa88d58..d58870a 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; use anyhow::Context; use futures_util::{SinkExt, StreamExt}; @@ -9,7 +9,7 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async}; use crate::{ account::AccountManager, executor::Executor, - strategy::{Strategy, burst::Burst}, + strategy::{Strategy, veloc::MomentumVelocityStrategy}, }; pub struct Bot { @@ -69,9 +69,7 @@ impl Bot { ws: Mutex::new(connect_async("wss://pumpdev.io/ws").await?.0), executor: Mutex::new(Box::new(account.executor())), accounts: Mutex::new(accounts), - strategy: Mutex::new(Box::new(Burst { - tokens: HashMap::new(), - })), + strategy: Mutex::new(Box::new(MomentumVelocityStrategy::new())), })) } diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 61c449a..f180176 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -9,7 +9,7 @@ use crate::account::Account; pub trait Executor: Send + Sync { async fn buy( &self, - mint: String, + mint: &str, amount: Decimal, priority: Decimal, slippage: u16, @@ -19,7 +19,7 @@ pub trait Executor: Send + Sync { async fn sell( &self, - mint: String, + mint: &str, amount: Decimal, priority: Decimal, slippage: u16, @@ -29,7 +29,7 @@ pub trait Executor: Send + Sync { async fn sell_percent( &self, - mint: String, + mint: &str, amount: u8, priority: Decimal, slippage: u16, diff --git a/src/executor/pump_fun.rs b/src/executor/pump_fun.rs index 34d192e..9261192 100644 --- a/src/executor/pump_fun.rs +++ b/src/executor/pump_fun.rs @@ -39,7 +39,7 @@ impl PumpDev { async fn trade( &self, action: &str, - mint: String, + mint: &str, amount: String, priority: Decimal, slippage: u16, @@ -88,7 +88,7 @@ impl PumpDev { impl Executor for PumpDev { async fn buy( &self, - mint: String, + mint: &str, amount: Decimal, priority: Decimal, slippage: u16, @@ -106,7 +106,7 @@ impl Executor for PumpDev { async fn sell( &self, - mint: String, + mint: &str, amount: Decimal, priority: Decimal, slippage: u16, @@ -124,7 +124,7 @@ impl Executor for PumpDev { async fn sell_percent( &self, - mint: String, + mint: &str, amount: u8, priority: Decimal, slippage: u16, diff --git a/src/strategy/burst.rs b/src/strategy/burst.rs deleted file mode 100644 index 08ae730..0000000 --- a/src/strategy/burst.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use crate::{ - bot::Bot, - strategy::Strategy, - types::{NewToken, Trade}, -}; - -pub enum Mode { - Observing, - WaitingForEntry, - WaitingForExit, -} - -pub struct Token { - pub mode: Mode, - pub execute_next: bool, -} - -pub struct Burst { - pub tokens: HashMap, -} - -#[async_trait::async_trait] -impl Strategy for Burst { - async fn on_new_coin(&mut self, bot: Arc, token: NewToken) -> anyhow::Result<()> { - self.tokens.insert( - token.mint.clone(), - Token { - mode: Mode::Observing, - execute_next: false, - }, - ); - - bot.subscribe(&token.mint).await?; - - Ok(()) - } - - async fn on_trade(&mut self, bot: Arc, trade: Trade) -> anyhow::Result<()> { - log::info!("Trade: {trade:?}"); - - let Some(token) = self.tokens.get_mut(&trade.mint) else { - log::error!("Token not found on trade: {:?}", trade.mint); - return Ok(()); - }; - - match &token.mode { - Mode::Observing => {} - - Mode::WaitingForEntry => {} - - Mode::WaitingForExit => {} - } - - Ok(()) - } -} diff --git a/src/strategy/mod.rs b/src/strategy/mod.rs index 166c113..6ca4731 100644 --- a/src/strategy/mod.rs +++ b/src/strategy/mod.rs @@ -1,4 +1,4 @@ -pub mod burst; +pub mod veloc; use std::sync::Arc; diff --git a/src/strategy/veloc.rs b/src/strategy/veloc.rs new file mode 100644 index 0000000..e41a77b --- /dev/null +++ b/src/strategy/veloc.rs @@ -0,0 +1,175 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use rust_decimal::{Decimal, dec}; + +use crate::bot::Bot; +use crate::strategy::Strategy; +use crate::types::{NewToken, Trade, TradeType}; + +const BUY_AMOUNT_SOL: Decimal = dec!(0.2); +const PRIORITY: Decimal = dec!(0.0002); +const SLIPPAGE: u16 = 10; + +// --- Strategy Implementation --- + +struct TokenTracker { + created_at: Instant, + unique_buyers: HashSet, + net_sol_flow: f64, + trade_count: usize, +} + +struct OpenPosition { + entry_price_sol: f64, + highest_price_sol: f64, + last_high_time: Instant, +} + +pub struct MomentumVelocityStrategy { + // Configurable thresholds + min_unique_buyers: usize, + min_net_sol_flow: f64, + max_tracking_duration: Duration, + + // In-memory state tracking + trackers: HashMap, + positions: HashMap, +} + +impl MomentumVelocityStrategy { + pub fn new() -> Self { + Self { + min_unique_buyers: 4, + min_net_sol_flow: 1.5, + max_tracking_duration: Duration::from_secs(45), + trackers: HashMap::new(), + positions: HashMap::new(), + } + } + + /// Calculate approximate token price in SOL using the bonding curve reserves + fn calculate_price_sol(&self, trade: &Trade) -> f64 { + if trade.v_tokens_in_bonding_curve == 0.0 { + return 0.0; + } + trade.v_sol_in_bonding_curve / trade.v_tokens_in_bonding_curve + } +} + +#[async_trait::async_trait] +impl Strategy for MomentumVelocityStrategy { + async fn on_new_coin(&mut self, _bot: Arc, token: NewToken) -> anyhow::Result<()> { + // Cleanup old untracked tokens to keep memory lean + self.trackers + .retain(|_, v| v.created_at.elapsed() < Duration::from_secs(120)); + + // Initialize tracking for new token + self.trackers.insert( + token.mint, + TokenTracker { + created_at: Instant::now(), + unique_buyers: HashSet::new(), + net_sol_flow: 0.0, + trade_count: 0, + }, + ); + + Ok(()) + } + + async fn on_trade(&mut self, bot: Arc, trade: Trade) -> anyhow::Result<()> { + let mint = &trade.mint; + let current_price = self.calculate_price_sol(&trade); + + // ------------------------------------------------------------- + // 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; + + // Track peak price for trailing stop logic + if current_price > pos.highest_price_sol { + pos.highest_price_sol = current_price; + pos.last_high_time = Instant::now(); + } + + let drop_from_peak = (pos.highest_price_sol - current_price) / pos.highest_price_sol; + + let should_sell = match () { + _ if price_change_pct >= 0.40 => true, // Take Profit: +40% + _ if price_change_pct <= -0.15 => true, // Hard Stop Loss: -15% + _ if drop_from_peak >= 0.12 && price_change_pct > 0.10 => true, // Trailing stop: 12% drop from peak + _ if pos.last_high_time.elapsed() >= Duration::from_secs(25) => true, // Momentum stalled for 25s + _ => false, + }; + + if should_sell { + bot.executor + .lock() + .await + .sell_percent(mint, 100, PRIORITY, SLIPPAGE) + .await?; + + self.positions.remove(mint); + self.trackers.remove(mint); + } + + return Ok(()); + } + + // ------------------------------------------------------------- + // 2. Evaluate Potential Buys + // ------------------------------------------------------------- + if let Some(tracker) = self.trackers.get_mut(mint) { + // Stop monitoring if the token is older than maximum initial evaluation window + if tracker.created_at.elapsed() > self.max_tracking_duration { + self.trackers.remove(mint); + return Ok(()); + } + + // Update stats + tracker.trade_count += 1; + match trade.tx_type { + TradeType::Buy => { + tracker.unique_buyers.insert(trade.trader); + tracker.net_sol_flow += trade.sol_amount; + } + TradeType::Sell => { + tracker.net_sol_flow -= trade.sol_amount; + } + } + + // Check BUY conditions + let has_enough_buyers = tracker.unique_buyers.len() >= self.min_unique_buyers; + let has_volume_surge = tracker.net_sol_flow >= self.min_net_sol_flow; + + // Skip if bonding curve is already close to completing (e.g. >30 SOL in curve) + let is_early_curve = trade.v_sol_in_bonding_curve < 60.0; + + if has_enough_buyers && has_volume_surge && is_early_curve { + bot.executor + .lock() + .await + .buy(mint, BUY_AMOUNT_SOL, PRIORITY, SLIPPAGE) + .await?; + + // Register open position + self.positions.insert( + mint.clone(), + OpenPosition { + entry_price_sol: current_price, + highest_price_sol: current_price, + last_high_time: Instant::now(), + }, + ); + + // Stop tracking buy metrics for this token + self.trackers.remove(mint); + } + } + + Ok(()) + } +}