Velocity strategy
This commit is contained in:
Generated
+11
@@ -1453,11 +1453,22 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.8.7",
|
"rand 0.8.7",
|
||||||
"rkyv",
|
"rkyv",
|
||||||
|
"rust_decimal_macros",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
|
|||||||
+1
-1
@@ -19,6 +19,6 @@ tokio = { version = "1.53.1", features = [
|
|||||||
tokio-tungstenite = { version = "0.30.0", features = ["native-tls"] }
|
tokio-tungstenite = { version = "0.30.0", features = ["native-tls"] }
|
||||||
log = "0.4.33"
|
log = "0.4.33"
|
||||||
env_logger = "0.11.11"
|
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"] }
|
reqwest = { version = "0.13.4", features = ["json"] }
|
||||||
async-trait = "0.1.91"
|
async-trait = "0.1.91"
|
||||||
|
|||||||
+3
-5
@@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
@@ -9,7 +9,7 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async};
|
|||||||
use crate::{
|
use crate::{
|
||||||
account::AccountManager,
|
account::AccountManager,
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
strategy::{Strategy, burst::Burst},
|
strategy::{Strategy, veloc::MomentumVelocityStrategy},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Bot {
|
pub struct Bot {
|
||||||
@@ -69,9 +69,7 @@ impl Bot {
|
|||||||
ws: Mutex::new(connect_async("wss://pumpdev.io/ws").await?.0),
|
ws: Mutex::new(connect_async("wss://pumpdev.io/ws").await?.0),
|
||||||
executor: Mutex::new(Box::new(account.executor())),
|
executor: Mutex::new(Box::new(account.executor())),
|
||||||
accounts: Mutex::new(accounts),
|
accounts: Mutex::new(accounts),
|
||||||
strategy: Mutex::new(Box::new(Burst {
|
strategy: Mutex::new(Box::new(MomentumVelocityStrategy::new())),
|
||||||
tokens: HashMap::new(),
|
|
||||||
})),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ use crate::account::Account;
|
|||||||
pub trait Executor: Send + Sync {
|
pub trait Executor: Send + Sync {
|
||||||
async fn buy(
|
async fn buy(
|
||||||
&self,
|
&self,
|
||||||
mint: String,
|
mint: &str,
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
priority: Decimal,
|
priority: Decimal,
|
||||||
slippage: u16,
|
slippage: u16,
|
||||||
@@ -19,7 +19,7 @@ pub trait Executor: Send + Sync {
|
|||||||
|
|
||||||
async fn sell(
|
async fn sell(
|
||||||
&self,
|
&self,
|
||||||
mint: String,
|
mint: &str,
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
priority: Decimal,
|
priority: Decimal,
|
||||||
slippage: u16,
|
slippage: u16,
|
||||||
@@ -29,7 +29,7 @@ pub trait Executor: Send + Sync {
|
|||||||
|
|
||||||
async fn sell_percent(
|
async fn sell_percent(
|
||||||
&self,
|
&self,
|
||||||
mint: String,
|
mint: &str,
|
||||||
amount: u8,
|
amount: u8,
|
||||||
priority: Decimal,
|
priority: Decimal,
|
||||||
slippage: u16,
|
slippage: u16,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl PumpDev {
|
|||||||
async fn trade(
|
async fn trade(
|
||||||
&self,
|
&self,
|
||||||
action: &str,
|
action: &str,
|
||||||
mint: String,
|
mint: &str,
|
||||||
amount: String,
|
amount: String,
|
||||||
priority: Decimal,
|
priority: Decimal,
|
||||||
slippage: u16,
|
slippage: u16,
|
||||||
@@ -88,7 +88,7 @@ impl PumpDev {
|
|||||||
impl Executor for PumpDev {
|
impl Executor for PumpDev {
|
||||||
async fn buy(
|
async fn buy(
|
||||||
&self,
|
&self,
|
||||||
mint: String,
|
mint: &str,
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
priority: Decimal,
|
priority: Decimal,
|
||||||
slippage: u16,
|
slippage: u16,
|
||||||
@@ -106,7 +106,7 @@ impl Executor for PumpDev {
|
|||||||
|
|
||||||
async fn sell(
|
async fn sell(
|
||||||
&self,
|
&self,
|
||||||
mint: String,
|
mint: &str,
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
priority: Decimal,
|
priority: Decimal,
|
||||||
slippage: u16,
|
slippage: u16,
|
||||||
@@ -124,7 +124,7 @@ impl Executor for PumpDev {
|
|||||||
|
|
||||||
async fn sell_percent(
|
async fn sell_percent(
|
||||||
&self,
|
&self,
|
||||||
mint: String,
|
mint: &str,
|
||||||
amount: u8,
|
amount: u8,
|
||||||
priority: Decimal,
|
priority: Decimal,
|
||||||
slippage: u16,
|
slippage: u16,
|
||||||
|
|||||||
@@ -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<String, Token>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl Strategy for Burst {
|
|
||||||
async fn on_new_coin(&mut self, bot: Arc<Bot>, 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<Bot>, 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
pub mod burst;
|
pub mod veloc;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|||||||
@@ -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<String>,
|
||||||
|
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<String, TokenTracker>,
|
||||||
|
positions: HashMap<String, OpenPosition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Bot>, 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<Bot>, 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user