Skip to content

Commit

Permalink
Merge pull request #1 from fdundjer/master
Browse files Browse the repository at this point in the history
  • Loading branch information
kapocius authored May 6, 2024
2 parents 6bb1443 + 04e5ca7 commit 51112e9
Show file tree
Hide file tree
Showing 11 changed files with 831 additions and 92 deletions.
4 changes: 3 additions & 1 deletion .env.copy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
696 changes: 674 additions & 22 deletions LICENSE.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
127 changes: 83 additions & 44 deletions bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,6 +42,8 @@ export interface BotConfig {
unitPrice: number;
takeProfit: number;
stopLoss: number;
trailingStopLoss: boolean;
skipSellingIfLostMoreThan: number;
buySlippage: number;
sellSlippage: number;
priceCheckInterval: number;
Expand All @@ -55,14 +54,12 @@ export interface BotConfig {
}

export class Bot {
private readonly poolFilters: PoolFilters;

// snipe list
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;

Expand All @@ -75,13 +72,7 @@ export class Bot {
) {
this.isWarp = txExecutor instanceof WarpTransactionExecutor;
this.isJito = txExecutor instanceof JitoTransactionExecutor;

this.mutex = new Mutex();
this.poolFilters = new PoolFilters(connection, {
quoteToken: this.config.quoteToken,
minPoolSize: this.config.minPoolSize,
maxPoolSize: this.config.maxPoolSize,
});
this.semaphore = new Semaphore(config.maxTokensAtTheTime);

if (this.config.useSnipeList) {
this.snipeListCache = new SnipeListCache();
Expand Down Expand Up @@ -115,18 +106,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()),
Expand Down Expand Up @@ -190,16 +181,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...`);
Expand Down Expand Up @@ -227,10 +214,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}`,
Expand Down Expand Up @@ -276,9 +267,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--;
}
}

Expand Down Expand Up @@ -359,13 +348,19 @@ export class Bot {
return true;
}

const filters = new PoolFilters(this.connection, {
quoteToken: this.config.quoteToken,
minPoolSize: this.config.minPoolSize,
maxPoolSize: this.config.maxPoolSize,
});

const timesToCheck = this.config.filterCheckDuration / this.config.filterCheckInterval;
let timesChecked = 0;
let matchCount = 0;

do {
try {
const shouldBuy = await this.poolFilters.execute(poolKeys);
const shouldBuy = await filters.execute(poolKeys);

if (shouldBuy) {
matchCount++;
Expand All @@ -390,19 +385,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;

Expand All @@ -419,18 +422,52 @@ 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() },
`Take profit: ${takeProfit.toFixed()} | Stop loss: ${stopLoss.toFixed()} | Current: ${amountOut.toFixed()}`,
);

if (amountOut.lt(stopLoss)) {
this.stopLoss.delete(poolKeys.baseMint.toString());
break;
}

if (amountOut.gt(takeProfit)) {
this.stopLoss.delete(poolKeys.baseMint.toString());
break;
}

Expand All @@ -441,5 +478,7 @@ export class Bot {
timesChecked++;
}
} while (timesChecked < timesToCheck);

return true;
}
}
14 changes: 13 additions & 1 deletion filters/burn.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ import { LiquidityPoolKeysV4 } from '@raydium-io/raydium-sdk';
import { logger } from '../helpers';

export class BurnFilter implements Filter {
private cachedResult: FilterResult | undefined = undefined;

constructor(private readonly connection: Connection) {}

async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
if (this.cachedResult) {
return this.cachedResult;
}

try {
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" };
const result = { ok: burned, message: burned ? undefined : "Burned -> Creator didn't burn LP" };

if (result.ok) {
this.cachedResult = result;
}

return result;
} catch (e: any) {
if (e.code == -32602) {
return { ok: true };
Expand Down
15 changes: 13 additions & 2 deletions filters/mutable.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { logger } from '../helpers';

export class MutableFilter implements Filter {
private readonly errorMessage: string[] = [];
private cachedResult: FilterResult | undefined = undefined;

constructor(
private readonly connection: Connection,
Expand All @@ -25,6 +26,10 @@ export class MutableFilter implements Filter {
}

async execute(poolKeys: LiquidityPoolKeysV4): Promise<FilterResult> {
if (this.cachedResult) {
return this.cachedResult;
}

try {
const metadataPDA = getPdaMetadataKey(poolKeys.baseMint);
const metadataAccount = await this.connection.getAccountInfo(metadataPDA.publicKey, this.connection.commitment);
Expand All @@ -47,7 +52,13 @@ export class MutableFilter implements Filter {
message.push('has no socials');
}

return { ok: ok, message: ok ? undefined : `MutableSocials -> Token ${message.join(' and ')}` };
const result = { ok: ok, message: ok ? undefined : `MutableSocials -> Token ${message.join(' and ')}` };

if (!mutable) {
this.cachedResult = result;
}

return result;
} catch (e) {
logger.error({ mint: poolKeys.baseMint }, `MutableSocials -> Failed to check ${this.errorMessage.join(' and ')}`);
}
Expand All @@ -61,6 +72,6 @@ export class MutableFilter implements Filter {
private async hasSocials(metadata: MetadataAccountData) {
const response = await fetch(metadata.uri);
const data = await response.json();
return Object.values(data?.extensions ?? {}).some((value: any) => value !== null && value.length > 0);
return Object.values(data?.extensions ?? {}).filter((value: any) => value).length > 0;
}
}
Loading

0 comments on commit 51112e9

Please sign in to comment.