Переглянути джерело

Merge pull request #88 from fdundjer/master

feat: real time pool filtering
Filip Dunđer 1 рік тому
батько
коміт
5a49cfd12a
9 змінених файлів з 110 додано та 46 видалено
  1. 9 6
      .env.copy
  2. 17 7
      README.md
  3. 53 14
      bot.ts
  4. 4 4
      filters/burn.filter.ts
  5. 5 5
      filters/pool-filters.ts
  6. 4 4
      filters/pool-size.filter.ts
  7. 4 4
      filters/renounced.filter.ts
  8. 3 0
      helpers/constants.ts
  9. 11 2
      index.ts

+ 9 - 6
.env.copy

@@ -24,22 +24,25 @@ QUOTE_MINT=WSOL
 QUOTE_AMOUNT=0.001
 AUTO_BUY_DELAY=0
 MAX_BUY_RETRIES=10
-BUY_SLIPPAGE=5
+BUY_SLIPPAGE=20
 
 # Sell
 AUTO_SELL=true
 MAX_SELL_RETRIES=10
 AUTO_SELL_DELAY=0
 PRICE_CHECK_INTERVAL=2000
-PRICE_CHECK_DURATION=60000
-TAKE_PROFIT=20
-STOP_LOSS=15
-SELL_SLIPPAGE=5
+PRICE_CHECK_DURATION=600000
+TAKE_PROFIT=40
+STOP_LOSS=20
+SELL_SLIPPAGE=20
 
 # Filters
 USE_SNIPE_LIST=false
 SNIPE_LIST_REFRESH_INTERVAL=30000
+FILTER_CHECK_DURATION=60000
+FILTER_CHECK_INTERVAL=2000
+CONSECUTIVE_FILTER_MATCHES=3
 CHECK_IF_MINT_IS_RENOUNCED=true
-CHECK_IF_BURNED=false
+CHECK_IF_BURNED=true
 MIN_POOL_SIZE=5
 MAX_POOL_SIZE=50

+ 17 - 7
README.md

@@ -1,11 +1,9 @@
 
-# Solana Sniper Bot (Poc)
-This code is written as proof of concept to demonstrate how we can buy new tokens immediately after the liquidity pool is open for trading.
+# Solana Trading Bot (Beta)
+The Solana Trading Bot is a software tool designed to automate the buying and selling of tokens on the Solana blockchain. 
+It is configured to execute trades based on predefined parameters and strategies set by the user. 
 
-Script listens to new Raydium USDC or SOL pools and buys tokens for a fixed amount in USDC/SOL.  
-Depending on the speed of the RPC node, the purchase usually happens before the token is available on Raydium UI for swapping.
-
-This is provided as is, for learning purposes.
+The bot can monitor market conditions in real-time, such as pool burn, mint renounced and other factors, and it will execute trades when these conditions are fulfilled.
 
 ## Setup
 To run the script you need to:
@@ -55,11 +53,14 @@ You should see the following output:
 
 #### Sell
 - `AUTO_SELL` - Set to `true` to enable automatic selling of tokens.
+  - If you want to manually sell bought tokens, disable this option.
 - `MAX_SELL_RETRIES` - Maximum number of retries for selling a token.
 - `AUTO_SELL_DELAY` -  Delay in milliseconds before auto-selling a token.
 - `PRICE_CHECK_INTERVAL` - Interval in milliseconds for checking the take profit and stop loss conditions.
+  - Set to zero to disable take profit and stop loss.
 - `PRICE_CHECK_DURATION` - Time in milliseconds to wait for stop loss/take profit conditions.
   - If you don't reach profit or loss bot will auto sell after this time.
+  - Set to zero to disable take profit and stop loss.
 - `TAKE_PROFIT` - Percentage profit at which to take profit.
   - Take profit is calculated based on quote mint.
 - `STOP_LOSS` - Percentage loss at which to stop the loss.
@@ -67,6 +68,13 @@ You should see the following output:
 - `SELL_SLIPPAGE` - Slippage %.
 
 #### Filters
+- `FILTER_CHECK_INTERVAL` - Interval in milliseconds for checking if pool match the filters.
+  - Set to zero to disable filters.
+- `FILTER_CHECK_DURATION` - Time in milliseconds to wait for pool to match the filters.
+  - If pool doesn't match the filter buy will not happen.
+  - Set to zero to disable filters.
+- `CONSECUTIVE_FILTER_MATCHES` - How many times in a row pool needs to match the filters.
+  - This is useful because when pool is burned (and rugged), other filters may not report the same behavior. eg. pool size may still have old value
 - `USE_SNIPE_LIST` - Set to `true` to enable buying only tokens listed in `snipe-list.txt`.
   - Pool must not exist before the script starts.
 - `SNIPE_LIST_REFRESH_INTERVAL` - Interval in milliseconds to refresh the snipe list.
@@ -123,4 +131,6 @@ To collect more information on an issue, please change `LOG_LEVEL` to `debug`.
 
 ## Disclaimer
 
-Use this script at your own risk.
+The Solana Trading Bot is provided as is, for learning purposes.
+Trading cryptocurrencies and tokens involves risk, and past performance is not indicative of future results.
+The use of this bot is at your own risk, and we are not responsible for any losses incurred while using the bot.

+ 53 - 14
bot.ts

@@ -47,6 +47,9 @@ export interface BotConfig {
   sellSlippage: number;
   priceCheckInterval: number;
   priceCheckDuration: number;
+  filterCheckInterval: number;
+  filterCheckDuration: number;
+  consecutiveMatchCount: number;
 }
 
 export class Bot {
@@ -121,21 +124,21 @@ export class Bot {
     }
 
     try {
-      const shouldBuy = await this.poolFilters.execute(poolState);
+      const [market, mintAta] = await Promise.all([
+        this.marketStorage.get(poolState.marketId.toString()),
+        getAssociatedTokenAddress(poolState.baseMint, this.config.wallet.publicKey),
+      ]);
+      const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(accountId, poolState, market);
 
-      if (!shouldBuy) {
-        logger.debug({ mint: poolState.baseMint.toString() }, `Skipping buy because pool doesn't match filters`);
+      const match = await this.filterMatch(poolKeys);
+
+      if (!match) {
+        logger.trace({ mint: poolKeys.baseMint.toString() }, `Skipping buy because pool doesn't match filters`);
         return;
       }
 
       for (let i = 0; i < this.config.maxBuyRetries; i++) {
         try {
-          const [market, mintAta] = await Promise.all([
-            this.marketStorage.get(poolState.marketId.toString()),
-            getAssociatedTokenAddress(poolState.baseMint, this.config.wallet.publicKey),
-          ]);
-          const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(accountId, poolState, market);
-
           logger.info(
             { mint: poolState.baseMint.toString() },
             `Send buy transaction attempt: ${i + 1}/${this.config.maxBuyRetries}`,
@@ -214,13 +217,13 @@ export class Bot {
         await sleep(this.config.autoSellDelay);
       }
 
-      for (let i = 0; i < this.config.maxSellRetries; i++) {
-        try {
-          const market = await this.marketStorage.get(poolData.state.marketId.toString());
-          const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(new PublicKey(poolData.id), poolData.state, market);
+      const market = await this.marketStorage.get(poolData.state.marketId.toString());
+      const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(new PublicKey(poolData.id), poolData.state, market);
 
-          await this.priceMatch(tokenAmountIn, poolKeys);
+      await this.priceMatch(tokenAmountIn, poolKeys);
 
+      for (let i = 0; i < this.config.maxSellRetries; i++) {
+        try {
           logger.info(
             { mint: rawAccount.mint },
             `Send sell transaction attempt: ${i + 1}/${this.config.maxSellRetries}`,
@@ -342,6 +345,42 @@ export class Bot {
     return this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash);
   }
 
+  private async filterMatch(poolKeys: LiquidityPoolKeysV4) {
+    if (this.config.filterCheckInterval === 0 || this.config.filterCheckDuration === 0) {
+      return;
+    }
+
+    const timesToCheck = this.config.filterCheckDuration / this.config.filterCheckInterval;
+    let timesChecked = 0;
+    let matchCount = 0;
+
+    do {
+      try {
+        const shouldBuy = await this.poolFilters.execute(poolKeys);
+
+        if (shouldBuy) {
+          matchCount++;
+
+          if (this.config.consecutiveMatchCount <= matchCount) {
+            logger.debug(
+              { mint: poolKeys.baseMint.toString() },
+              `Filter match ${matchCount}/${this.config.consecutiveMatchCount}`,
+            );
+            return true;
+          }
+        } else {
+          matchCount = 0;
+        }
+
+        await sleep(this.config.filterCheckInterval);
+      } finally {
+        timesChecked++;
+      }
+    } while (timesChecked < timesToCheck);
+
+    return false;
+  }
+
   private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
     if (this.config.priceCheckDuration === 0 || this.config.priceCheckInterval === 0) {
       return;

+ 4 - 4
filters/burn.filter.ts

@@ -1,14 +1,14 @@
 import { Filter, FilterResult } from './pool-filters';
 import { Connection } from '@solana/web3.js';
-import { LiquidityStateV4 } from '@raydium-io/raydium-sdk';
+import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk';
 import { logger } from '../helpers';
 
 export class BurnFilter implements Filter {
   constructor(private readonly connection: Connection) {}
 
-  async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
+  async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
     try {
-      const amount = await this.connection.getTokenSupply(poolState.lpMint, this.connection.commitment);
+      const amount = await this.connection.getTokenSupply(poolKeys.lpMint, this.connection.commitment);
       const burned = amount.value.uiAmount === 0;
       return { ok: burned, message: burned ? undefined : "Burned -> Creator didn't burn LP" };
     } catch (e: any) {
@@ -16,7 +16,7 @@ export class BurnFilter implements Filter {
         return { ok: true };
       }
 
-      logger.error({ mint: poolState.baseMint }, `Failed to check if LP is burned`);
+      logger.error({ mint: poolKeys.baseMint }, `Failed to check if LP is burned`);
     }
 
     return { ok: false, message: 'Failed to check if LP is burned' };

+ 5 - 5
filters/pool-filters.ts

@@ -1,12 +1,12 @@
 import { Connection } from '@solana/web3.js';
-import { LiquidityStateV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
+import { LiquidityPoolKeysV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
 import { BurnFilter } from './burn.filter';
 import { RenouncedFilter } from './renounced.filter';
 import { PoolSizeFilter } from './pool-size.filter';
 import { CHECK_IF_BURNED, CHECK_IF_MINT_IS_RENOUNCED, logger } from '../helpers';
 
 export interface Filter {
-  execute(poolState: LiquidityStateV4): Promise<FilterResult>;
+  execute(poolKeysV4: LiquidityPoolKeysV4): Promise<FilterResult>;
 }
 
 export interface FilterResult {
@@ -40,12 +40,12 @@ export class PoolFilters {
     }
   }
 
-  public async execute(poolState: LiquidityStateV4): Promise<boolean> {
+  public async execute(poolKeys: LiquidityPoolKeysV4): Promise<boolean> {
     if (this.filters.length === 0) {
       return true;
     }
 
-    const result = await Promise.all(this.filters.map((f) => f.execute(poolState)));
+    const result = await Promise.all(this.filters.map((f) => f.execute(poolKeys)));
     const pass = result.every((r) => r.ok);
 
     if (pass) {
@@ -53,7 +53,7 @@ export class PoolFilters {
     }
 
     for (const filterResult of result.filter((r) => !r.ok)) {
-      logger.info(filterResult.message);
+      logger.trace(filterResult.message);
     }
 
     return false;

+ 4 - 4
filters/pool-size.filter.ts

@@ -1,5 +1,5 @@
 import { Filter, FilterResult } from './pool-filters';
-import { LiquidityStateV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
+import { LiquidityPoolKeysV4, Token, TokenAmount } from '@raydium-io/raydium-sdk';
 import { Connection } from '@solana/web3.js';
 import { logger } from '../helpers';
 
@@ -11,9 +11,9 @@ export class PoolSizeFilter implements Filter {
     private readonly maxPoolSize: TokenAmount,
   ) {}
 
-  async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
+  async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
     try {
-      const response = await this.connection.getTokenAccountBalance(poolState.quoteVault, this.connection.commitment);
+      const response = await this.connection.getTokenAccountBalance(poolKeys.quoteVault, this.connection.commitment);
       const poolSize = new TokenAmount(this.quoteToken, response.value.amount, true);
       let inRange = true;
 
@@ -35,7 +35,7 @@ export class PoolSizeFilter implements Filter {
 
       return { ok: inRange };
     } catch (error) {
-      logger.error({ mint: poolState.baseMint }, `Failed to check pool size`);
+      logger.error({ mint: poolKeys.baseMint }, `Failed to check pool size`);
     }
 
     return { ok: false, message: 'PoolSize -> Failed to check pool size' };

+ 4 - 4
filters/renounced.filter.ts

@@ -1,15 +1,15 @@
 import { Filter, FilterResult } from './pool-filters';
 import { MintLayout } from '@solana/spl-token';
 import { Connection } from '@solana/web3.js';
-import { LiquidityStateV4 } from '@raydium-io/raydium-sdk';
+import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk';
 import { logger } from '../helpers';
 
 export class RenouncedFilter implements Filter {
   constructor(private readonly connection: Connection) {}
 
-  async execute(poolState: LiquidityStateV4): Promise<FilterResult> {
+  async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
     try {
-      const accountInfo = await this.connection.getAccountInfo(poolState.baseMint, this.connection.commitment);
+      const accountInfo = await this.connection.getAccountInfo(poolKeys.baseMint, this.connection.commitment);
       if (!accountInfo?.data) {
         return { ok: false, message: 'Renounced -> Failed to fetch account data' };
       }
@@ -18,7 +18,7 @@ export class RenouncedFilter implements Filter {
       const renounced = deserialize.mintAuthorityOption === 0;
       return { ok: renounced, message: renounced ? undefined : 'Renounced -> Creator can mint more tokens' };
     } catch (e) {
-      logger.error({ mint: poolState.baseMint }, `Failed to check if mint is renounced`);
+      logger.error({ mint: poolKeys.baseMint }, `Failed to check if mint is renounced`);
     }
 
     return { ok: false, message: 'Renounced -> Failed to check if mint is renounced' };

+ 3 - 0
helpers/constants.ts

@@ -51,6 +51,9 @@ export const PRICE_CHECK_DURATION = Number(retrieveEnvVariable('PRICE_CHECK_DURA
 export const SELL_SLIPPAGE = Number(retrieveEnvVariable('SELL_SLIPPAGE', logger));
 
 // Filters
+export const FILTER_CHECK_INTERVAL = Number(retrieveEnvVariable('FILTER_CHECK_INTERVAL', logger));
+export const FILTER_CHECK_DURATION = Number(retrieveEnvVariable('FILTER_CHECK_DURATION', logger));
+export const CONSECUTIVE_FILTER_MATCHES = Number(retrieveEnvVariable('CONSECUTIVE_FILTER_MATCHES', logger));
 export const CHECK_IF_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true';
 export const CHECK_IF_BURNED = retrieveEnvVariable('CHECK_IF_BURNED', logger) === 'true';
 export const MIN_POOL_SIZE = retrieveEnvVariable('MIN_POOL_SIZE', logger);

+ 11 - 2
index.ts

@@ -40,6 +40,9 @@ import {
   SNIPE_LIST_REFRESH_INTERVAL,
   TRANSACTION_EXECUTOR,
   WARP_FEE,
+  FILTER_CHECK_INTERVAL,
+  FILTER_CHECK_DURATION,
+  CONSECUTIVE_FILTER_MATCHES,
 } from './helpers';
 import { version } from './package.json';
 import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
@@ -78,8 +81,7 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
   logger.info(`Using warp: ${bot.isWarp}`);
   if (bot.isWarp) {
     logger.info(`Warp fee: ${WARP_FEE}`);
-  }
-  else {
+  } else {
     logger.info(`Compute Unit limit: ${botConfig.unitLimit}`);
     logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
   }
@@ -109,6 +111,9 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
   logger.info('- Filters -');
   logger.info(`Snipe list: ${botConfig.useSnipeList}`);
   logger.info(`Snipe list refresh interval: ${SNIPE_LIST_REFRESH_INTERVAL} ms`);
+  logger.info(`Filter check interval: ${botConfig.filterCheckInterval} ms`);
+  logger.info(`Filter check duration: ${botConfig.filterCheckDuration} ms`);
+  logger.info(`Consecutive filter matches: ${botConfig.consecutiveMatchCount} ms`);
   logger.info(`Check renounced: ${botConfig.checkRenounced}`);
   logger.info(`Check burned: ${botConfig.checkBurned}`);
   logger.info(`Min pool size: ${botConfig.minPoolSize.toFixed()}`);
@@ -151,6 +156,7 @@ const runListener = async () => {
     quoteAmount: new TokenAmount(quoteToken, QUOTE_AMOUNT, false),
     oneTokenAtATime: ONE_TOKEN_AT_A_TIME,
     useSnipeList: USE_SNIPE_LIST,
+    autoSell: AUTO_SELL,
     autoSellDelay: AUTO_SELL_DELAY,
     maxSellRetries: MAX_SELL_RETRIES,
     autoBuyDelay: AUTO_BUY_DELAY,
@@ -163,6 +169,9 @@ const runListener = async () => {
     sellSlippage: SELL_SLIPPAGE,
     priceCheckInterval: PRICE_CHECK_INTERVAL,
     priceCheckDuration: PRICE_CHECK_DURATION,
+    filterCheckInterval: FILTER_CHECK_INTERVAL,
+    filterCheckDuration: FILTER_CHECK_DURATION,
+    consecutiveMatchCount: CONSECUTIVE_FILTER_MATCHES,
   };
 
   const bot = new Bot(connection, marketCache, poolCache, txExecutor, botConfig);