mirror of
https://github.com/fdundjer/solana-sniper-bot.git
synced 2025-11-09 20:12:06 +10:00
feat: improve exit position logic
This commit is contained in:
@ -8,7 +8,7 @@ COMMITMENT_LEVEL=confirmed
|
||||
|
||||
# Bot
|
||||
LOG_LEVEL=trace
|
||||
ONE_TOKEN_AT_A_TIME=true
|
||||
MAX_TOKENS_AT_THE_TIME=1
|
||||
PRE_LOAD_EXISTING_MARKETS=false
|
||||
CACHE_NEW_MARKETS=false
|
||||
# default or warp or jito
|
||||
@ -34,6 +34,8 @@ PRICE_CHECK_INTERVAL=2000
|
||||
PRICE_CHECK_DURATION=600000
|
||||
TAKE_PROFIT=40
|
||||
STOP_LOSS=20
|
||||
TRAILING_STOP_LOSS=true
|
||||
SKIP_SELLING_IF_LOST_MORE_THAN=90
|
||||
SELL_SLIPPAGE=20
|
||||
|
||||
# Filters
|
||||
|
||||
@ -36,7 +36,7 @@ You should see the following output:
|
||||
#### Bot
|
||||
|
||||
- `LOG_LEVEL` - Set logging level, e.g., `info`, `debug`, `trace`, etc.
|
||||
- `ONE_TOKEN_AT_A_TIME` - Set to `true` to process buying one token at a time.
|
||||
- `MAX_TOKENS_AT_A_TIME` - Set to `1` to process buying one token at a time.
|
||||
- `COMPUTE_UNIT_LIMIT` - Compute limit used to calculate fees.
|
||||
- `COMPUTE_UNIT_PRICE` - Compute price used to calculate fees.
|
||||
- `PRE_LOAD_EXISTING_MARKETS` - Bot will load all existing markets in memory on start.
|
||||
@ -72,6 +72,9 @@ You should see the following output:
|
||||
- Take profit is calculated based on quote mint.
|
||||
- `STOP_LOSS` - Percentage loss at which to stop the loss.
|
||||
- Stop loss is calculated based on quote mint.
|
||||
- `TRAILING_STOP_LOSS` - Set to `true` to use trailing stop loss.
|
||||
- `SKIP_SELLING_IF_LOST_MORE_THAN` - If token loses more than X% of value, bot will not try to sell
|
||||
- This config is useful if you find yourself in a situation when rugpull happen, and you failed to sell. In this case there is a big loss of value, and sometimes it's more beneficial to keep the token, instead of selling it for almost nothing.
|
||||
- `SELL_SLIPPAGE` - Slippage %.
|
||||
|
||||
#### Snipe list
|
||||
|
||||
110
bot.ts
110
bot.ts
@ -19,22 +19,19 @@ import { MarketCache, PoolCache, SnipeListCache } from './cache';
|
||||
import { PoolFilters } from './filters';
|
||||
import { TransactionExecutor } from './transactions';
|
||||
import { createPoolKeys, logger, NETWORK, sleep } from './helpers';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { Semaphore } from 'async-mutex';
|
||||
import BN from 'bn.js';
|
||||
import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
|
||||
import { JitoTransactionExecutor } from './transactions/jito-rpc-transaction-executor';
|
||||
|
||||
export interface BotConfig {
|
||||
wallet: Keypair;
|
||||
checkRenounced: boolean;
|
||||
checkFreezable: boolean;
|
||||
checkBurned: boolean;
|
||||
minPoolSize: TokenAmount;
|
||||
maxPoolSize: TokenAmount;
|
||||
quoteToken: Token;
|
||||
quoteAmount: TokenAmount;
|
||||
quoteAta: PublicKey;
|
||||
oneTokenAtATime: boolean;
|
||||
maxTokensAtTheTime: number;
|
||||
useSnipeList: boolean;
|
||||
autoSell: boolean;
|
||||
autoBuyDelay: number;
|
||||
@ -45,6 +42,8 @@ export interface BotConfig {
|
||||
unitPrice: number;
|
||||
takeProfit: number;
|
||||
stopLoss: number;
|
||||
trailingStopLoss: boolean;
|
||||
skipSellingIfLostMoreThan: number;
|
||||
buySlippage: number;
|
||||
sellSlippage: number;
|
||||
priceCheckInterval: number;
|
||||
@ -61,8 +60,9 @@ export class Bot {
|
||||
private readonly snipeListCache?: SnipeListCache;
|
||||
|
||||
// one token at the time
|
||||
private readonly mutex: Mutex;
|
||||
private readonly semaphore: Semaphore;
|
||||
private sellExecutionCount = 0;
|
||||
private readonly stopLoss = new Map<string, TokenAmount>();
|
||||
public readonly isWarp: boolean = false;
|
||||
public readonly isJito: boolean = false;
|
||||
|
||||
@ -76,7 +76,7 @@ export class Bot {
|
||||
this.isWarp = txExecutor instanceof WarpTransactionExecutor;
|
||||
this.isJito = txExecutor instanceof JitoTransactionExecutor;
|
||||
|
||||
this.mutex = new Mutex();
|
||||
this.semaphore = new Semaphore(config.maxTokensAtTheTime);
|
||||
this.poolFilters = new PoolFilters(connection, {
|
||||
quoteToken: this.config.quoteToken,
|
||||
minPoolSize: this.config.minPoolSize,
|
||||
@ -115,18 +115,18 @@ export class Bot {
|
||||
await sleep(this.config.autoBuyDelay);
|
||||
}
|
||||
|
||||
if (this.config.oneTokenAtATime) {
|
||||
if (this.mutex.isLocked() || this.sellExecutionCount > 0) {
|
||||
logger.debug(
|
||||
{ mint: poolState.baseMint.toString() },
|
||||
`Skipping buy because one token at a time is turned on and token is already being processed`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.mutex.acquire();
|
||||
const numberOfActionsBeingProcessed =
|
||||
this.config.maxTokensAtTheTime - this.semaphore.getValue() + this.sellExecutionCount;
|
||||
if (this.semaphore.isLocked() || numberOfActionsBeingProcessed >= this.config.maxTokensAtTheTime) {
|
||||
logger.debug(
|
||||
{ mint: poolState.baseMint.toString() },
|
||||
`Skipping buy because max tokens to process at the same time is ${this.config.maxTokensAtTheTime} and currently ${numberOfActionsBeingProcessed} tokens is being processed`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.semaphore.acquire();
|
||||
|
||||
try {
|
||||
const [market, mintAta] = await Promise.all([
|
||||
this.marketStorage.get(poolState.marketId.toString()),
|
||||
@ -190,16 +190,12 @@ export class Bot {
|
||||
} catch (error) {
|
||||
logger.error({ mint: poolState.baseMint.toString(), error }, `Failed to buy token`);
|
||||
} finally {
|
||||
if (this.config.oneTokenAtATime) {
|
||||
this.mutex.release();
|
||||
}
|
||||
this.semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async sell(accountId: PublicKey, rawAccount: RawAccount) {
|
||||
if (this.config.oneTokenAtATime) {
|
||||
this.sellExecutionCount++;
|
||||
}
|
||||
this.sellExecutionCount++;
|
||||
|
||||
try {
|
||||
logger.trace({ mint: rawAccount.mint }, `Processing new token...`);
|
||||
@ -227,10 +223,14 @@ export class Bot {
|
||||
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);
|
||||
|
||||
for (let i = 0; i < this.config.maxSellRetries; i++) {
|
||||
try {
|
||||
const shouldSell = await this.waitForSellSignal(tokenAmountIn, poolKeys);
|
||||
|
||||
if (!shouldSell) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ mint: rawAccount.mint },
|
||||
`Send sell transaction attempt: ${i + 1}/${this.config.maxSellRetries}`,
|
||||
@ -276,9 +276,7 @@ export class Bot {
|
||||
} catch (error) {
|
||||
logger.error({ mint: rawAccount.mint.toString(), error }, `Failed to sell token`);
|
||||
} finally {
|
||||
if (this.config.oneTokenAtATime) {
|
||||
this.sellExecutionCount--;
|
||||
}
|
||||
this.sellExecutionCount--;
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,19 +388,27 @@ export class Bot {
|
||||
return false;
|
||||
}
|
||||
|
||||
private async priceMatch(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
|
||||
private async waitForSellSignal(amountIn: TokenAmount, poolKeys: LiquidityPoolKeysV4) {
|
||||
if (this.config.priceCheckDuration === 0 || this.config.priceCheckInterval === 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const timesToCheck = this.config.priceCheckDuration / this.config.priceCheckInterval;
|
||||
const profitFraction = this.config.quoteAmount.mul(this.config.takeProfit).numerator.div(new BN(100));
|
||||
const profitAmount = new TokenAmount(this.config.quoteToken, profitFraction, true);
|
||||
const takeProfit = this.config.quoteAmount.add(profitAmount);
|
||||
let stopLoss: TokenAmount;
|
||||
|
||||
if (!this.stopLoss.get(poolKeys.baseMint.toString())) {
|
||||
const lossFraction = this.config.quoteAmount.mul(this.config.stopLoss).numerator.div(new BN(100));
|
||||
const lossAmount = new TokenAmount(this.config.quoteToken, lossFraction, true);
|
||||
stopLoss = this.config.quoteAmount.subtract(lossAmount);
|
||||
|
||||
this.stopLoss.set(poolKeys.baseMint.toString(), stopLoss);
|
||||
} else {
|
||||
stopLoss = this.stopLoss.get(poolKeys.baseMint.toString())!;
|
||||
}
|
||||
|
||||
const lossFraction = this.config.quoteAmount.mul(this.config.stopLoss).numerator.div(new BN(100));
|
||||
const lossAmount = new TokenAmount(this.config.quoteToken, lossFraction, true);
|
||||
const stopLoss = this.config.quoteAmount.subtract(lossAmount);
|
||||
const slippage = new Percent(this.config.sellSlippage, 100);
|
||||
let timesChecked = 0;
|
||||
|
||||
@ -419,7 +425,39 @@ export class Bot {
|
||||
amountIn: amountIn,
|
||||
currencyOut: this.config.quoteToken,
|
||||
slippage,
|
||||
}).amountOut;
|
||||
}).amountOut as TokenAmount;
|
||||
|
||||
if (this.config.trailingStopLoss) {
|
||||
const trailingLossFraction = amountOut.mul(this.config.stopLoss).numerator.div(new BN(100));
|
||||
const trailingLossAmount = new TokenAmount(this.config.quoteToken, trailingLossFraction, true);
|
||||
const trailingStopLoss = amountOut.subtract(trailingLossAmount);
|
||||
|
||||
if (trailingStopLoss.gt(stopLoss)) {
|
||||
logger.trace(
|
||||
{ mint: poolKeys.baseMint.toString() },
|
||||
`Updating trailing stop loss from ${stopLoss.toFixed()} to ${trailingStopLoss.toFixed()}`,
|
||||
);
|
||||
this.stopLoss.set(poolKeys.baseMint.toString(), trailingStopLoss);
|
||||
stopLoss = trailingStopLoss;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.skipSellingIfLostMoreThan > 0) {
|
||||
const stopSellingFraction = this.config.quoteAmount
|
||||
.mul(this.config.skipSellingIfLostMoreThan)
|
||||
.numerator.div(new BN(100));
|
||||
|
||||
const stopSellingAmount = new TokenAmount(this.config.quoteToken, stopSellingFraction, true);
|
||||
|
||||
if (amountOut.lt(stopSellingAmount)) {
|
||||
logger.debug(
|
||||
{ mint: poolKeys.baseMint.toString() },
|
||||
`Token dropped more than ${this.config.skipSellingIfLostMoreThan}%, sell stopped. Initial: ${this.config.quoteAmount.toFixed()} | Current: ${amountOut.toFixed()}`,
|
||||
);
|
||||
this.stopLoss.delete(poolKeys.baseMint.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
{ mint: poolKeys.baseMint.toString() },
|
||||
@ -427,10 +465,12 @@ export class Bot {
|
||||
);
|
||||
|
||||
if (amountOut.lt(stopLoss)) {
|
||||
this.stopLoss.delete(poolKeys.baseMint.toString());
|
||||
break;
|
||||
}
|
||||
|
||||
if (amountOut.gt(takeProfit)) {
|
||||
this.stopLoss.delete(poolKeys.baseMint.toString());
|
||||
break;
|
||||
}
|
||||
|
||||
@ -441,5 +481,7 @@ export class Bot {
|
||||
timesChecked++;
|
||||
}
|
||||
} while (timesChecked < timesToCheck);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ export const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOIN
|
||||
|
||||
// Bot
|
||||
export const LOG_LEVEL = retrieveEnvVariable('LOG_LEVEL', logger);
|
||||
export const ONE_TOKEN_AT_A_TIME = retrieveEnvVariable('ONE_TOKEN_AT_A_TIME', logger) === 'true';
|
||||
export const MAX_TOKENS_AT_THE_TIME = Number(retrieveEnvVariable('MAX_TOKENS_AT_THE_TIME', logger));
|
||||
export const COMPUTE_UNIT_LIMIT = Number(retrieveEnvVariable('COMPUTE_UNIT_LIMIT', logger));
|
||||
export const COMPUTE_UNIT_PRICE = Number(retrieveEnvVariable('COMPUTE_UNIT_PRICE', logger));
|
||||
export const PRE_LOAD_EXISTING_MARKETS = retrieveEnvVariable('PRE_LOAD_EXISTING_MARKETS', logger) === 'true';
|
||||
@ -46,9 +46,11 @@ export const AUTO_SELL_DELAY = Number(retrieveEnvVariable('AUTO_SELL_DELAY', log
|
||||
export const MAX_SELL_RETRIES = Number(retrieveEnvVariable('MAX_SELL_RETRIES', logger));
|
||||
export const TAKE_PROFIT = Number(retrieveEnvVariable('TAKE_PROFIT', logger));
|
||||
export const STOP_LOSS = Number(retrieveEnvVariable('STOP_LOSS', logger));
|
||||
export const TRAILING_STOP_LOSS = retrieveEnvVariable('TRAILING_STOP_LOSS', logger) === 'true';
|
||||
export const PRICE_CHECK_INTERVAL = Number(retrieveEnvVariable('PRICE_CHECK_INTERVAL', logger));
|
||||
export const PRICE_CHECK_DURATION = Number(retrieveEnvVariable('PRICE_CHECK_DURATION', logger));
|
||||
export const SELL_SLIPPAGE = Number(retrieveEnvVariable('SELL_SLIPPAGE', logger));
|
||||
export const SKIP_SELLING_IF_LOST_MORE_THAN = Number(retrieveEnvVariable('SKIP_SELLING_IF_LOST_MORE_THAN', logger));
|
||||
|
||||
// Filters
|
||||
export const FILTER_CHECK_INTERVAL = Number(retrieveEnvVariable('FILTER_CHECK_INTERVAL', logger));
|
||||
|
||||
36
index.ts
36
index.ts
@ -14,17 +14,12 @@ import {
|
||||
RPC_WEBSOCKET_ENDPOINT,
|
||||
PRE_LOAD_EXISTING_MARKETS,
|
||||
LOG_LEVEL,
|
||||
CHECK_IF_MUTABLE,
|
||||
CHECK_IF_MINT_IS_RENOUNCED,
|
||||
CHECK_IF_FREEZABLE,
|
||||
CHECK_IF_BURNED,
|
||||
QUOTE_MINT,
|
||||
MAX_POOL_SIZE,
|
||||
MIN_POOL_SIZE,
|
||||
QUOTE_AMOUNT,
|
||||
PRIVATE_KEY,
|
||||
USE_SNIPE_LIST,
|
||||
ONE_TOKEN_AT_A_TIME,
|
||||
AUTO_SELL_DELAY,
|
||||
MAX_SELL_RETRIES,
|
||||
AUTO_SELL,
|
||||
@ -45,6 +40,14 @@ import {
|
||||
FILTER_CHECK_INTERVAL,
|
||||
FILTER_CHECK_DURATION,
|
||||
CONSECUTIVE_FILTER_MATCHES,
|
||||
MAX_TOKENS_AT_THE_TIME,
|
||||
CHECK_IF_MINT_IS_RENOUNCED,
|
||||
CHECK_IF_FREEZABLE,
|
||||
CHECK_IF_BURNED,
|
||||
CHECK_IF_MUTABLE,
|
||||
CHECK_IF_SOCIALS,
|
||||
TRAILING_STOP_LOSS,
|
||||
SKIP_SELLING_IF_LOST_MORE_THAN,
|
||||
} from './helpers';
|
||||
import { version } from './package.json';
|
||||
import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
|
||||
@ -80,10 +83,8 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
|
||||
logger.info(`Wallet: ${wallet.publicKey.toString()}`);
|
||||
|
||||
logger.info('- Bot -');
|
||||
logger.info(`Using transaction executor: ${TRANSACTION_EXECUTOR}`);
|
||||
|
||||
logger.info(
|
||||
`Using ${TRANSACTION_EXECUTOR} executer: ${bot.isWarp || bot.isJito || (TRANSACTION_EXECUTOR === 'default' ? true : false)}`,
|
||||
);
|
||||
if (bot.isWarp || bot.isJito) {
|
||||
logger.info(`${TRANSACTION_EXECUTOR} fee: ${CUSTOM_FEE}`);
|
||||
} else {
|
||||
@ -91,7 +92,7 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
|
||||
logger.info(`Compute Unit price (micro lamports): ${botConfig.unitPrice}`);
|
||||
}
|
||||
|
||||
logger.info(`Single token at the time: ${botConfig.oneTokenAtATime}`);
|
||||
logger.info(`Max tokens at the time: ${botConfig.maxTokensAtTheTime}`);
|
||||
logger.info(`Pre load existing markets: ${PRE_LOAD_EXISTING_MARKETS}`);
|
||||
logger.info(`Cache new markets: ${CACHE_NEW_MARKETS}`);
|
||||
logger.info(`Log level: ${LOG_LEVEL}`);
|
||||
@ -112,6 +113,8 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
|
||||
logger.info(`Price check duration: ${botConfig.priceCheckDuration} ms`);
|
||||
logger.info(`Take profit: ${botConfig.takeProfit}%`);
|
||||
logger.info(`Stop loss: ${botConfig.stopLoss}%`);
|
||||
logger.info(`Trailing stop loss: ${botConfig.trailingStopLoss}`);
|
||||
logger.info(`Skip selling if lost more than: ${botConfig.skipSellingIfLostMoreThan}%`);
|
||||
|
||||
logger.info('- Snipe list -');
|
||||
logger.info(`Snipe list: ${botConfig.useSnipeList}`);
|
||||
@ -125,9 +128,11 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
|
||||
logger.info(`Filter check interval: ${botConfig.filterCheckInterval} ms`);
|
||||
logger.info(`Filter check duration: ${botConfig.filterCheckDuration} ms`);
|
||||
logger.info(`Consecutive filter matches: ${botConfig.consecutiveMatchCount}`);
|
||||
logger.info(`Check renounced: ${botConfig.checkRenounced}`);
|
||||
logger.info(`Check freezable: ${botConfig.checkFreezable}`);
|
||||
logger.info(`Check burned: ${botConfig.checkBurned}`);
|
||||
logger.info(`Check renounced: ${CHECK_IF_MINT_IS_RENOUNCED}`);
|
||||
logger.info(`Check freezable: ${CHECK_IF_FREEZABLE}`);
|
||||
logger.info(`Check burned: ${CHECK_IF_BURNED}`);
|
||||
logger.info(`Check mutable: ${CHECK_IF_MUTABLE}`);
|
||||
logger.info(`Check socials: ${CHECK_IF_SOCIALS}`);
|
||||
logger.info(`Min pool size: ${botConfig.minPoolSize.toFixed()}`);
|
||||
logger.info(`Max pool size: ${botConfig.maxPoolSize.toFixed()}`);
|
||||
}
|
||||
@ -165,14 +170,11 @@ const runListener = async () => {
|
||||
const botConfig = <BotConfig>{
|
||||
wallet,
|
||||
quoteAta: getAssociatedTokenAddressSync(quoteToken.mint, wallet.publicKey),
|
||||
checkRenounced: CHECK_IF_MINT_IS_RENOUNCED,
|
||||
checkFreezable: CHECK_IF_FREEZABLE,
|
||||
checkBurned: CHECK_IF_BURNED,
|
||||
minPoolSize: new TokenAmount(quoteToken, MIN_POOL_SIZE, false),
|
||||
maxPoolSize: new TokenAmount(quoteToken, MAX_POOL_SIZE, false),
|
||||
quoteToken,
|
||||
quoteAmount: new TokenAmount(quoteToken, QUOTE_AMOUNT, false),
|
||||
oneTokenAtATime: ONE_TOKEN_AT_A_TIME,
|
||||
maxTokensAtTheTime: MAX_TOKENS_AT_THE_TIME,
|
||||
useSnipeList: USE_SNIPE_LIST,
|
||||
autoSell: AUTO_SELL,
|
||||
autoSellDelay: AUTO_SELL_DELAY,
|
||||
@ -183,6 +185,8 @@ const runListener = async () => {
|
||||
unitPrice: COMPUTE_UNIT_PRICE,
|
||||
takeProfit: TAKE_PROFIT,
|
||||
stopLoss: STOP_LOSS,
|
||||
trailingStopLoss: TRAILING_STOP_LOSS,
|
||||
skipSellingIfLostMoreThan: SKIP_SELLING_IF_LOST_MORE_THAN,
|
||||
buySlippage: BUY_SLIPPAGE,
|
||||
sellSlippage: SELL_SLIPPAGE,
|
||||
priceCheckInterval: PRICE_CHECK_INTERVAL,
|
||||
|
||||
Reference in New Issue
Block a user