copy_trading/position/
create_checked.rs

1/**
2Given a position that is a buy, perform our checks to see if we want to execute on it.
3
4If we already have a position open on the same token, don't execute on it (double buy).
5
6If the wallet buy size is less than our buy size, then we buy max the wallet buy size. When we cap
7buy sizes, we don't cap them at the exact amount that the wallet bought at in lamports. This will
8make it obvious we are copying their trade and trade amount exactly. Therefore, we'll take their
9buy size, and take a random percentage from 85 to 100% and use that as our buy size.
10
11Minimum sol liquidity on the pool should be 15 SOL.
12
13Filter out meteora dbc and dammv2 swaps for now
14
15Check if the token is legit
16*/
17use crate::types::position::Position;
18use crate::types::position_close_reason::PositionCloseReason;
19use crate::types::position_config::PositionConfig;
20use crate::types::position_manager::PositionManager;
21use crate::types::position_status::PositionStatus;
22use rand::Rng;
23use solana_central::Pools;
24use solana_central::SwapTx;
25use solana_central::is_legit_token;
26use solana_sdk::native_token::LAMPORTS_PER_SOL;
27use solana_sdk::pubkey::Pubkey;
28use std::collections::HashMap;
29use std::sync::{Arc, Mutex};
30
31impl Position {
32  /// Create a new position after performing validation checks. Validates that: no duplicate
33  /// position exists, token is legitimate, pool has sufficient liquidity, and buy amount doesn't
34  /// exceed wallet's buy size. Caps buy size to 85-100% of wallet's buy amount to avoid exact copy
35  /// detection.
36  pub fn create_checked(
37    token_address: Pubkey,
38    sol_token_a: bool,
39    swap_tx: &SwapTx,
40    mut config: PositionConfig,
41    open_positions: &HashMap<Pubkey, Arc<Mutex<Position>>>,
42    position_manager: &Arc<PositionManager>,
43  ) -> Option<Position> {
44    // Double buy/position open check
45    if open_positions.contains_key(&swap_tx.market_address) {
46      return None;
47    }
48
49    // Legit token check
50    if !is_legit_token(&token_address, &position_manager.central_context) {
51      println!(
52        "Not opening position, token not legit: {}, wallet: {:?}, signature: {}",
53        token_address, swap_tx.signers, swap_tx.signature
54      );
55      return None;
56    }
57
58    let pool = position_manager.central_context.pools_map.read().unwrap();
59    let pool = pool.get(&swap_tx.market_address).unwrap();
60
61    // Pool type, sol liquidity check
62    let (pool_type, sol_liquidity_lp) = {
63      let pool = pool.read().unwrap();
64      if sol_token_a {
65        (*pool.pool_type(), pool.token_a_amount_units())
66      } else {
67        (*pool.pool_type(), pool.token_b_amount_units())
68      }
69    };
70    if pool_type == Pools::MeteoraDammV2 || pool_type == Pools::MeteoraDbc {
71      return None;
72    }
73    // println!("Sol liquidity lp: {:?}", sol_liquidity_lp);
74    // 15 sol minimum liquidity for launchpads, 75 sol minimum liquidity for amms
75    let too_low_liquidity =
76      if pool_type == Pools::PfBondingCurve || pool_type == Pools::RaydiumLaunchpad {
77        sol_liquidity_lp < 15 * LAMPORTS_PER_SOL
78      } else {
79        sol_liquidity_lp < 75 * LAMPORTS_PER_SOL
80      };
81    if too_low_liquidity {
82      println!(
83        "Not opening position, too low liquidity: token: {}, wallet: {:?}, signature: {}, liquidity observed: {:?}",
84        token_address, swap_tx.signers, swap_tx.signature, sol_liquidity_lp
85      );
86      return None;
87    }
88
89    // All checks passed, we'll open a position
90
91    // Cap token amount
92    let buy_amount_lp = {
93      if config.buy_amount_lp > swap_tx.swapped_amount_in {
94        rand::rng().random_range(85..100) * swap_tx.swapped_amount_in / 100
95      } else {
96        config.buy_amount_lp
97      }
98    };
99    config.buy_amount_lp = buy_amount_lp;
100
101    println!(
102      "POSITION: Opening, token: {}, wallet: {:?}, tx: {}",
103      token_address, swap_tx.signers, swap_tx.signature
104    );
105
106    Some(Self {
107      config,
108      token_address,
109      pool: pool.clone(),
110      status: PositionStatus::Created,
111      migrated: false,
112      pool_address: swap_tx.market_address,
113      open_time: 0,
114      buy_confirmation_time: 0,
115      sell_confirmation_time: 0,
116      close_time: 0,
117      entry_price_lp: 0,
118      exit_price_lp: 0,
119      stop_loss_price_lp: 0,
120      wallet: position_manager.wallet.clone(),
121      sol_token_a,
122      lowest_acceptable_price_lp: 0,
123      tokens_purchased: 0,
124      close_reason: PositionCloseReason::HoldTime,
125      sell_amount_lp: 0,
126    })
127  }
128}