Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trade log capability #107

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.copy
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ COMMITMENT_LEVEL=confirmed

# Bot
LOG_LEVEL=trace
LOG_FILENAME=trades_journal.json
ONE_TOKEN_AT_A_TIME=true
PRE_LOAD_EXISTING_MARKETS=false
CACHE_NEW_MARKETS=false
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ You should see the following output:
#### Bot

- `LOG_LEVEL` - Set logging level, e.g., `info`, `debug`, `trace`, etc.
- `LOG_FILENAME` - Filename for trade log journal, set to `none` to disable.
- `ONE_TOKEN_AT_A_TIME` - Set to `true` 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.
Expand Down
83 changes: 81 additions & 2 deletions bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
PublicKey,
TransactionMessage,
VersionedTransaction,
LAMPORTS_PER_SOL,
} from '@solana/web3.js';
import {
createAssociatedTokenAccountIdempotentInstruction,
Expand All @@ -18,14 +19,16 @@ import { Liquidity, LiquidityPoolKeysV4, LiquidityStateV4, Percent, Token, Token
import { MarketCache, PoolCache, SnipeListCache } from './cache';
import { PoolFilters } from './filters';
import { TransactionExecutor } from './transactions';
import { createPoolKeys, logger, NETWORK, sleep } from './helpers';
import { createPoolKeys, logger, NETWORK, sleep, Trade } from './helpers';
import { Mutex } from 'async-mutex';
import BN from 'bn.js';
import { WarpTransactionExecutor } from './transactions/warp-transaction-executor';
import { JitoTransactionExecutor } from './transactions/jito-rpc-transaction-executor';
import * as fs from 'fs';

export interface BotConfig {
wallet: Keypair;
logFilename: string;
checkRenounced: boolean;
checkFreezable: boolean;
checkBurned: boolean;
Expand Down Expand Up @@ -56,6 +59,10 @@ export interface BotConfig {

export class Bot {
private readonly poolFilters: PoolFilters;
public balance: number = 0;
private tradesCount: number = 0;
private trades: Map<string, Trade> = new Map<string, Trade>();
private logFilename: string= '';

// snipe list
private readonly snipeListCache?: SnipeListCache;
Expand Down Expand Up @@ -87,6 +94,27 @@ export class Bot {
this.snipeListCache = new SnipeListCache();
this.snipeListCache.init();
}

this.logFilename = this.config.logFilename;
}

async init() {
await this.updateBalance();

// Read trades from log file, and get last trade id
const data = fs.readFileSync(this.logFilename, { flag: 'a+' });
const lines = data.toString().split('\n').filter((line) => line.length > 0);
const objects = lines.map(line => JSON.parse(line));
const lastTrade = objects[objects.length - 1];
if (lastTrade) {
this.tradesCount = lastTrade.id;
}
}

async updateBalance() {
const solBalance = (await this.connection.getBalance(this.config.wallet.publicKey)) / LAMPORTS_PER_SOL;
const quoteBalance = (await this.connection.getBalance(this.config.quoteAta)) / LAMPORTS_PER_SOL;
this.balance = solBalance + quoteBalance;
}

async validate() {
Expand Down Expand Up @@ -127,6 +155,8 @@ export class Bot {
await this.mutex.acquire();
}

let trade: Trade = new Trade(poolState.baseMint.toString(), Number(this.config.quoteAmount.toFixed()), this.logFilename);

try {
const [market, mintAta] = await Promise.all([
this.marketStorage.get(poolState.marketId.toString()),
Expand All @@ -143,6 +173,8 @@ export class Bot {
}
}

trade.transitionStart();

for (let i = 0; i < this.config.maxBuyRetries; i++) {
try {
logger.info(
Expand Down Expand Up @@ -172,6 +204,9 @@ export class Bot {
`Confirmed buy tx`,
);

trade.transitionEnd();
trade.open();
this.trades.set(poolState.baseMint.toString(), trade);
break;
}

Expand Down Expand Up @@ -201,6 +236,11 @@ export class Bot {
this.sellExecutionCount++;
}

let trade = this.trades.get(rawAccount.mint.toString());
if (!trade) {
logger.error({ mint: rawAccount.mint.toString() }, `Trade not found`);
}

try {
logger.trace({ mint: rawAccount.mint }, `Processing new token...`);

Expand Down Expand Up @@ -229,6 +269,10 @@ export class Bot {

await this.priceMatch(tokenAmountIn, poolKeys);

if (trade) {
trade.transitionStart();
}

for (let i = 0; i < this.config.maxSellRetries; i++) {
try {
logger.info(
Expand Down Expand Up @@ -258,6 +302,11 @@ export class Bot {
},
`Confirmed sell tx`,
);

if (trade) {
trade.transitionEnd();
trade.close();
}
break;
}

Expand All @@ -275,7 +324,19 @@ export class Bot {
}
} catch (error) {
logger.error({ mint: rawAccount.mint.toString(), error }, `Failed to sell token`);
if (trade) {
trade.closeFailed();
}
} finally {
if (trade) {
await this.updateBalance();
this.tradesCount++;
const err = trade.complete(this.balance, this.tradesCount);
if (err) {
logger.warn({ error: err }, `Failed to write trade in journal`);
}
}
this.trades.delete(rawAccount.mint.toString());
if (this.config.oneTokenAtATime) {
this.sellExecutionCount--;
}
Expand Down Expand Up @@ -351,7 +412,12 @@ export class Bot {
const transaction = new VersionedTransaction(messageV0);
transaction.sign([wallet, ...innerTransaction.signers]);

return this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash);
const transactionResult = await this.txExecutor.executeAndConfirm(transaction, wallet, latestBlockhash);
if (transactionResult.confirmed) {
await this.swap_log(direction, tokenIn, tokenOut, amountIn, computedAmountOut);
}

return transactionResult;
}

private async filterMatch(poolKeys: LiquidityPoolKeysV4) {
Expand Down Expand Up @@ -442,4 +508,17 @@ export class Bot {
}
} while (timesChecked < timesToCheck);
}

async swap_log(direction: string, tokenIn: Token, tokenOut: Token, amountIn: TokenAmount, computedAmountOut: any) {
let trade = this.trades.get(tokenIn.mint.toString());
if (!trade) {
logger.error({ mint: tokenIn.mint.toString() }, `Trade not found`);
}
if (direction === 'sell') {
if (trade) {
const amountOut = Number(computedAmountOut.amountOut.toFixed());
trade.confirmedSell(amountOut)
}
}
}
}
1 change: 1 addition & 0 deletions helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const RPC_WEBSOCKET_ENDPOINT = retrieveEnvVariable('RPC_WEBSOCKET_ENDPOIN

// Bot
export const LOG_LEVEL = retrieveEnvVariable('LOG_LEVEL', logger);
export const LOG_FILENAME = retrieveEnvVariable('LOG_FILENAME', logger);
export const ONE_TOKEN_AT_A_TIME = retrieveEnvVariable('ONE_TOKEN_AT_A_TIME', logger) === 'true';
export const COMPUTE_UNIT_LIMIT = Number(retrieveEnvVariable('COMPUTE_UNIT_LIMIT', logger));
export const COMPUTE_UNIT_PRICE = Number(retrieveEnvVariable('COMPUTE_UNIT_PRICE', logger));
Expand Down
1 change: 1 addition & 0 deletions helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './constants';
export * from './token';
export * from './wallet';
export * from './promises'
export * from './trade'
101 changes: 101 additions & 0 deletions helpers/trade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as fs from 'fs';
import * as moment from 'moment';
import 'moment-duration-format';

export class Trade {
private data = {
mint: '',
amountIn: 0,
amountOut: 0,
start: new Date(),
end: new Date(),
time_to_entry: '',
time_to_exit: '',
profit: 0,
profitPercent: 0,
balance: 0,
status: '',
id: 0,
}

private transition_start: Date
private transition_end: Date

constructor(
private readonly mint: string,
private readonly amountIn: number,
private readonly logFilename: string
) {
this.data.mint = mint;
this.data.amountIn = amountIn;
this.data.amountOut = 0;
this.data.profit = 0;
this.data.profitPercent = 0;
this.data.start = new Date();
this.data.end = new Date();
this.data.time_to_entry = '0';
this.data.time_to_exit = '0';
this.data.balance = 0;
this.data.status = 'initiated';
this.data.id = 0;

this.transition_start = new Date();
this.transition_end = new Date();
this.logFilename = logFilename;
}

// Help mesure entry and exit time of a trade
transitionStart() {
this.transition_start = new Date();
}

// Help mesure entry and exit time of a trade
transitionEnd() {
this.transition_end = new Date();
}

// Trade position entered
open() {
const duration = this.transition_end.getTime() - this.transition_start.getTime();
this.data.time_to_entry = moment.duration({ milliseconds: duration }).format();
this.data.start = new Date();
this.data.status = 'open';
}

// Trade position exited
close() {
const duration = this.transition_end.getTime() - this.transition_start.getTime();
this.data.time_to_exit = moment.duration({ milliseconds: duration }).format();
this.data.status = 'closed';
}

// Trade failed to sell
closeFailed() {
this.data.profit = -this.data.amountIn.valueOf();
this.data.profitPercent = -100;
this.data.time_to_exit = '0';
this.data.status = 'sell_failed';
}

// Trade sold, compute profit.
confirmedSell(amountOut: number) {
this.data.amountOut = amountOut;
this.data.profit = this.data.amountOut - this.data.amountIn.valueOf();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: profit computation does not account for fee

this.data.profitPercent = (this.data.profit / this.data.amountIn.valueOf()) * 100;
}

// Trade completed, save data to log file
complete(balance: number, id: number) {
this.data.balance = balance;
this.data.end = new Date();
this.data.id = id;

if (this.logFilename !== 'none') {
try {
fs.appendFileSync(this.logFilename, JSON.stringify(this.data) + '\n');
} catch (err) {
return err;
}
}
}
}
5 changes: 5 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
RPC_WEBSOCKET_ENDPOINT,
PRE_LOAD_EXISTING_MARKETS,
LOG_LEVEL,
LOG_FILENAME,
CHECK_IF_MUTABLE,
CHECK_IF_MINT_IS_RENOUNCED,
CHECK_IF_FREEZABLE,
Expand Down Expand Up @@ -78,6 +79,7 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {

logger.info('------- CONFIGURATION START -------');
logger.info(`Wallet: ${wallet.publicKey.toString()}`);
logger.info(`Balance: ${bot.balance} ${quoteToken.symbol}`);

logger.info('- Bot -');

Expand All @@ -95,6 +97,7 @@ function printDetails(wallet: Keypair, quoteToken: Token, bot: Bot) {
logger.info(`Pre load existing markets: ${PRE_LOAD_EXISTING_MARKETS}`);
logger.info(`Cache new markets: ${CACHE_NEW_MARKETS}`);
logger.info(`Log level: ${LOG_LEVEL}`);
logger.info(`Log file: ${LOG_FILENAME}`);

logger.info('- Buy -');
logger.info(`Buy amount: ${botConfig.quoteAmount.toFixed()} ${botConfig.quoteToken.name}`);
Expand Down Expand Up @@ -164,6 +167,7 @@ const runListener = async () => {
const quoteToken = getToken(QUOTE_MINT);
const botConfig = <BotConfig>{
wallet,
logFilename: LOG_FILENAME,
quoteAta: getAssociatedTokenAddressSync(quoteToken.mint, wallet.publicKey),
checkRenounced: CHECK_IF_MINT_IS_RENOUNCED,
checkFreezable: CHECK_IF_FREEZABLE,
Expand Down Expand Up @@ -193,6 +197,7 @@ const runListener = async () => {
};

const bot = new Bot(connection, marketCache, poolCache, txExecutor, botConfig);
await bot.init();
const valid = await bot.validate();

if (!valid) {
Expand Down
Loading