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:
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user