feat: real time filtering

This commit is contained in:
Filip Dunder
2024-04-19 12:09:56 +02:00
parent 7186467d10
commit da9512b8bb
9 changed files with 110 additions and 46 deletions

View File

@ -24,22 +24,25 @@ QUOTE_MINT=WSOL
QUOTE_AMOUNT=0.001 QUOTE_AMOUNT=0.001
AUTO_BUY_DELAY=0 AUTO_BUY_DELAY=0
MAX_BUY_RETRIES=10 MAX_BUY_RETRIES=10
BUY_SLIPPAGE=5 BUY_SLIPPAGE=20
# Sell # Sell
AUTO_SELL=true AUTO_SELL=true
MAX_SELL_RETRIES=10 MAX_SELL_RETRIES=10
AUTO_SELL_DELAY=0 AUTO_SELL_DELAY=0
PRICE_CHECK_INTERVAL=2000 PRICE_CHECK_INTERVAL=2000
PRICE_CHECK_DURATION=60000 PRICE_CHECK_DURATION=600000
TAKE_PROFIT=20 TAKE_PROFIT=40
STOP_LOSS=15 STOP_LOSS=20
SELL_SLIPPAGE=5 SELL_SLIPPAGE=20
# Filters # Filters
USE_SNIPE_LIST=false USE_SNIPE_LIST=false
SNIPE_LIST_REFRESH_INTERVAL=30000 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_MINT_IS_RENOUNCED=true
CHECK_IF_BURNED=false CHECK_IF_BURNED=true
MIN_POOL_SIZE=5 MIN_POOL_SIZE=5
MAX_POOL_SIZE=50 MAX_POOL_SIZE=50

View File

@ -1,11 +1,9 @@
# Solana Sniper Bot (Poc) # Solana Trading Bot (Beta)
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. 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. 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.
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.
## Setup ## Setup
To run the script you need to: To run the script you need to:
@ -55,11 +53,14 @@ You should see the following output:
#### Sell #### Sell
- `AUTO_SELL` - Set to `true` to enable automatic selling of tokens. - `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. - `MAX_SELL_RETRIES` - Maximum number of retries for selling a token.
- `AUTO_SELL_DELAY` - Delay in milliseconds before auto-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. - `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. - `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. - 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` - Percentage profit at which to take profit.
- Take profit is calculated based on quote mint. - Take profit is calculated based on quote mint.
- `STOP_LOSS` - Percentage loss at which to stop the loss. - `STOP_LOSS` - Percentage loss at which to stop the loss.
@ -67,6 +68,13 @@ You should see the following output:
- `SELL_SLIPPAGE` - Slippage %. - `SELL_SLIPPAGE` - Slippage %.
#### Filters #### 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 because of distributed RPC endpoints (eg. helius network)
- `USE_SNIPE_LIST` - Set to `true` to enable buying only tokens listed in `snipe-list.txt`. - `USE_SNIPE_LIST` - Set to `true` to enable buying only tokens listed in `snipe-list.txt`.
- Pool must not exist before the script starts. - Pool must not exist before the script starts.
- `SNIPE_LIST_REFRESH_INTERVAL` - Interval in milliseconds to refresh the snipe list. - `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 ## 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.

61
bot.ts
View File

@ -47,6 +47,9 @@ export interface BotConfig {
sellSlippage: number; sellSlippage: number;
priceCheckInterval: number; priceCheckInterval: number;
priceCheckDuration: number; priceCheckDuration: number;
filterCheckInterval: number;
filterCheckDuration: number;
consecutiveMatchCount: number;
} }
export class Bot { export class Bot {
@ -120,15 +123,6 @@ export class Bot {
await this.mutex.acquire(); await this.mutex.acquire();
} }
try {
const shouldBuy = await this.poolFilters.execute(poolState);
if (!shouldBuy) {
logger.debug({ mint: poolState.baseMint.toString() }, `Skipping buy because pool doesn't match filters`);
return;
}
for (let i = 0; i < this.config.maxBuyRetries; i++) {
try { try {
const [market, mintAta] = await Promise.all([ const [market, mintAta] = await Promise.all([
this.marketStorage.get(poolState.marketId.toString()), this.marketStorage.get(poolState.marketId.toString()),
@ -136,6 +130,15 @@ export class Bot {
]); ]);
const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(accountId, poolState, market); const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(accountId, poolState, market);
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 {
logger.info( logger.info(
{ mint: poolState.baseMint.toString() }, { mint: poolState.baseMint.toString() },
`Send buy transaction attempt: ${i + 1}/${this.config.maxBuyRetries}`, `Send buy transaction attempt: ${i + 1}/${this.config.maxBuyRetries}`,
@ -214,13 +217,13 @@ export class Bot {
await sleep(this.config.autoSellDelay); 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 market = await this.marketStorage.get(poolData.state.marketId.toString());
const poolKeys: LiquidityPoolKeysV4 = createPoolKeys(new PublicKey(poolData.id), poolData.state, market); 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( logger.info(
{ mint: rawAccount.mint }, { mint: rawAccount.mint },
`Send sell transaction attempt: ${i + 1}/${this.config.maxSellRetries}`, `Send sell transaction attempt: ${i + 1}/${this.config.maxSellRetries}`,
@ -342,6 +345,42 @@ export class Bot {
return this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash); 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) { private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
if (this.config.priceCheckDuration === 0 || this.config.priceCheckInterval === 0) { if (this.config.priceCheckDuration === 0 || this.config.priceCheckInterval === 0) {
return; return;

View File

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

View File

@ -1,12 +1,12 @@
import { Connection } from '@solana/web3.js'; 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 { BurnFilter } from './burn.filter';
import { RenouncedFilter } from './renounced.filter'; import { RenouncedFilter } from './renounced.filter';
import { PoolSizeFilter } from './pool-size.filter'; import { PoolSizeFilter } from './pool-size.filter';
import { CHECK_IF_BURNED, CHECK_IF_MINT_IS_RENOUNCED, logger } from '../helpers'; import { CHECK_IF_BURNED, CHECK_IF_MINT_IS_RENOUNCED, logger } from '../helpers';
export interface Filter { export interface Filter {
execute(poolState: LiquidityStateV4): Promise<FilterResult>; execute(poolKeysV4: LiquidityPoolKeysV4): Promise<FilterResult>;
} }
export interface 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) { if (this.filters.length === 0) {
return true; 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); const pass = result.every((r) => r.ok);
if (pass) { if (pass) {
@ -53,7 +53,7 @@ export class PoolFilters {
} }
for (const filterResult of result.filter((r) => !r.ok)) { for (const filterResult of result.filter((r) => !r.ok)) {
logger.info(filterResult.message); logger.trace(filterResult.message);
} }
return false; return false;

View File

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

View File

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

View File

@ -51,6 +51,9 @@ export const PRICE_CHECK_DURATION = Number(retrieveEnvVariable('PRICE_CHECK_DURA
export const SELL_SLIPPAGE = Number(retrieveEnvVariable('SELL_SLIPPAGE', logger)); export const SELL_SLIPPAGE = Number(retrieveEnvVariable('SELL_SLIPPAGE', logger));
// Filters // 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_MINT_IS_RENOUNCED = retrieveEnvVariable('CHECK_IF_MINT_IS_RENOUNCED', logger) === 'true';
export const CHECK_IF_BURNED = retrieveEnvVariable('CHECK_IF_BURNED', logger) === 'true'; export const CHECK_IF_BURNED = retrieveEnvVariable('CHECK_IF_BURNED', logger) === 'true';
export const MIN_POOL_SIZE = retrieveEnvVariable('MIN_POOL_SIZE', logger); export const MIN_POOL_SIZE = retrieveEnvVariable('MIN_POOL_SIZE', logger);

View File

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