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 all 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
88 changes: 85 additions & 3 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 All @@ -43,6 +46,7 @@ export interface BotConfig {
maxSellRetries: number;
unitLimit: number;
unitPrice: number;
fee: number,
takeProfit: number;
stopLoss: number;
buySlippage: number;
Expand All @@ -56,6 +60,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 +95,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 @@ -143,6 +172,10 @@ export class Bot {
}
}

let trade = new Trade(poolState.baseMint.toString(), this.trade_log_filename);
trade.transitionStart();
this.trades.set(poolState.baseMint.toString(), trade);

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

break;
}

Expand All @@ -189,6 +221,7 @@ export class Bot {
}
} catch (error) {
logger.error({ mint: poolState.baseMint.toString(), error }, `Failed to buy token`);
this.trades.delete(poolState.baseMint.toString());
} finally {
if (this.config.oneTokenAtATime) {
this.mutex.release();
Expand All @@ -201,6 +234,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 +267,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 @@ -275,7 +317,20 @@ export class Bot {
}
} catch (error) {
logger.error({ mint: rawAccount.mint.toString(), error }, `Failed to sell token`);
if (trade) {
trade.close(0, 0, 'sell_failed');
this.balance += trade.profit;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not really needed since we later on call this.updatebalance, but that's paving the way for a future dry run mode implementation.

}
} finally {
await this.updateBalance();
if (trade) {
this.tradesCount++;
const err = trade.completeAndLog(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 +406,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 +502,26 @@ export class Bot {
}
} while (timesChecked < timesToCheck);
}

async swap_log(direction: string, tokenIn: Token, tokenOut: Token, amountIn: TokenAmount, computedAmountOut: any) {
if (direction === 'buy') {
let trade = this.trades.get(tokenOut.mint.toString());
if (!trade) {
logger.error({ mint: tokenOut.mint.toString() }, `Trade not found`);
} else {
const amountIn = Number(amountIn.toFixed());
trade.open(amountIn, this.config.fee + (Number(computedAmountOut.fee.toFixed()) / LAMPORTS_PER_SOL));
}
}
if (direction === 'sell') {
let trade = this.trades.get(tokenIn.mint.toString());
if (!trade) {
logger.error({ mint: tokenIn.mint.toString() }, `Trade not found`);
} else {
const amountOut = Number(computedAmountOut.amountOut.toFixed());
trade.close(amountOut, this.config.fee + (Number(computedAmountOut.fee.toFixed()) / LAMPORTS_PER_SOL), 'closed');
this.balance += trade.profit;
}
}
}
}
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'
107 changes: 107 additions & 0 deletions helpers/trade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as fs from 'fs';
import * as moment from 'moment';
import 'moment-duration-format';

export interface TradeData {
amountIn: number,
amountOut: number,
fee: number,
start: Date,
end: Date,
time_to_entry: string,
time_to_exit: string,
profit: number,
profitPercent: number,
balance: number,
mint: string,
status: string,
id: number
}

export class Trade {
private data: TradeData
private transition_start: number
private transition_end: number

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

this.transition_start = 0;
this.transition_end = 0;
this.logFilename = logFilename;
}

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

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

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

// Trade position closed
// Compute profit
close(amountOut: number, fee: number, status: string) {
this.transition_end = Date.now();
const duration = this.transition_end - this.transition_start;
this.data.time_to_exit = moment.duration({ milliseconds: duration }).format();
this.data.end = new Date();
this.data.amountOut = amountOut;
this.data.fee += fee;
this.data.profit = this.data.amountOut - this.data.amountIn - this.data.fee;
this.data.profitPercent = (this.data.profit / this.data.amountIn) * 100;
this.data.status = status;
}

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

if (this.logFilename !== 'none') {
try {
fs.appendFileSync(this.logFilename, JSON.stringify(this.data) + '\n');
} catch (err) {
return err;
}
}
}

get profit() {
return this.data.profit;
}

get amountIn() {
return this.data.amountIn;
}
}
Loading