From f2b9089745191a7fcb72a1e1a8b46978db51953b Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sat, 11 May 2024 12:00:53 +0200 Subject: [PATCH 1/5] Add trade log Add capability to log trades into a journal file --- .env.copy | 1 + README.md | 1 + bot.ts | 81 +++++++++++++++++++++++++++++++++- helpers/constants.ts | 1 + helpers/index.ts | 1 + helpers/trade.ts | 101 +++++++++++++++++++++++++++++++++++++++++++ index.ts | 5 +++ package-lock.json | 61 ++++++++++++++++++-------- package.json | 3 ++ 9 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 helpers/trade.ts diff --git a/.env.copy b/.env.copy index 07681b95..e28c87d1 100644 --- a/.env.copy +++ b/.env.copy @@ -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 diff --git a/README.md b/README.md index 92f00ae3..ab27ad4c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bot.ts b/bot.ts index 835aca8e..66938581 100644 --- a/bot.ts +++ b/bot.ts @@ -5,6 +5,7 @@ import { PublicKey, TransactionMessage, VersionedTransaction, + LAMPORTS_PER_SOL, } from '@solana/web3.js'; import { createAssociatedTokenAccountIdempotentInstruction, @@ -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; @@ -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 = new Map(); + private logFilename: string= ''; // snipe list private readonly snipeListCache?: SnipeListCache; @@ -87,6 +94,25 @@ export class Bot { this.snipeListCache = new SnipeListCache(); this.snipeListCache.init(); } + + this.logFilename = this.config.logFilename; + } + + async init() { + await this.updateBalance(); + 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() { @@ -127,6 +153,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()), @@ -143,6 +171,8 @@ export class Bot { } } + trade.transitionStart(); + for (let i = 0; i < this.config.maxBuyRetries; i++) { try { logger.info( @@ -172,6 +202,9 @@ export class Bot { `Confirmed buy tx`, ); + trade.transitionEnd(); + trade.open(); + this.trades.set(poolState.baseMint.toString(), trade); break; } @@ -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...`); @@ -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( @@ -258,6 +300,11 @@ export class Bot { }, `Confirmed sell tx`, ); + + if (trade) { + trade.transitionEnd(); + trade.close(); + } break; } @@ -275,7 +322,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--; } @@ -351,7 +410,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) { @@ -442,4 +506,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) + } + } + } } diff --git a/helpers/constants.ts b/helpers/constants.ts index 96ca547d..a1b03766 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -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)); diff --git a/helpers/index.ts b/helpers/index.ts index 3ff8cee1..8e305dc0 100644 --- a/helpers/index.ts +++ b/helpers/index.ts @@ -5,3 +5,4 @@ export * from './constants'; export * from './token'; export * from './wallet'; export * from './promises' +export * from './trade' diff --git a/helpers/trade.ts b/helpers/trade.ts new file mode 100644 index 00000000..7dbc7d50 --- /dev/null +++ b/helpers/trade.ts @@ -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(); + 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; + } + } + } +} diff --git a/index.ts b/index.ts index e4439799..bbc49d9a 100644 --- a/index.ts +++ b/index.ts @@ -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, @@ -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 -'); @@ -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}`); @@ -164,6 +167,7 @@ const runListener = async () => { const quoteToken = getToken(QUOTE_MINT); const botConfig = { wallet, + logFilename: LOG_FILENAME, quoteAta: getAssociatedTokenAddressSync(quoteToken.mint, wallet.publicKey), checkRenounced: CHECK_IF_MINT_IS_RENOUNCED, checkFreezable: CHECK_IF_FREEZABLE, @@ -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) { diff --git a/package-lock.json b/package-lock.json index d37e0588..6e3fe3c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@raydium-io/raydium-sdk": "^1.3.1-beta.47", "@solana/spl-token": "^0.4.0", "@solana/web3.js": "^1.89.1", + "@types/moment-duration-format": "^2.2.6", "async-mutex": "^0.5.0", "axios": "^1.6.8", "bigint-buffer": "^1.1.5", @@ -21,6 +22,8 @@ "dotenv": "^16.4.1", "ed25519-hd-key": "^1.3.0", "i": "^0.3.7", + "moment": "^2.30.1", + "moment-duration-format": "^2.3.2", "npm": "^10.5.2", "pino": "^8.18.0", "pino-pretty": "^10.3.1", @@ -353,24 +356,6 @@ "base-x": "^3.0.2" } }, - "node_modules/@solana/web3.js/node_modules/node-fetch": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "dev": true, @@ -406,6 +391,14 @@ "@types/node": "*" } }, + "node_modules/@types/moment-duration-format": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@types/moment-duration-format/-/moment-duration-format-2.2.6.tgz", + "integrity": "sha512-Qw+6ys3sQCatqukjoIBsL1UJq6S7ep4ixrNvDkdViSgzw2ZG3neArXNu3ww7mEG8kwP32ZDoON/esWb7OUckRQ==", + "dependencies": { + "moment": ">=2.14.0" + } + }, "node_modules/@types/node": { "version": "12.20.55", "license": "MIT" @@ -1007,10 +1000,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-duration-format": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-2.3.2.tgz", + "integrity": "sha512-cBMXjSW+fjOb4tyaVHuaVE/A5TqkukDWiOfxxAjY+PEqmmBQlLwn+8OzwPiG3brouXKY5Un4pBjAeB6UToXHaQ==" + }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp-build": { "version": "4.8.0", "license": "MIT", diff --git a/package.json b/package.json index d619d4ef..89650730 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@raydium-io/raydium-sdk": "^1.3.1-beta.47", "@solana/spl-token": "^0.4.0", "@solana/web3.js": "^1.89.1", + "@types/moment-duration-format": "^2.2.6", "async-mutex": "^0.5.0", "axios": "^1.6.8", "bigint-buffer": "^1.1.5", @@ -21,6 +22,8 @@ "dotenv": "^16.4.1", "ed25519-hd-key": "^1.3.0", "i": "^0.3.7", + "moment": "^2.30.1", + "moment-duration-format": "^2.3.2", "npm": "^10.5.2", "pino": "^8.18.0", "pino-pretty": "^10.3.1", From 8abb49597bcfe823e2eaf4c16a9df6d48b0ced9f Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sat, 11 May 2024 12:33:50 +0200 Subject: [PATCH 2/5] add comment --- bot.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot.ts b/bot.ts index 66938581..8938179e 100644 --- a/bot.ts +++ b/bot.ts @@ -100,6 +100,8 @@ export class Bot { 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)); From e7a1194223b6bb912bab316a26188b598e095cc4 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sat, 11 May 2024 18:07:25 +0200 Subject: [PATCH 3/5] nit renaming --- bot.ts | 4 ++-- helpers/trade.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot.ts b/bot.ts index 8938179e..55be81f6 100644 --- a/bot.ts +++ b/bot.ts @@ -331,7 +331,7 @@ export class Bot { if (trade) { await this.updateBalance(); this.tradesCount++; - const err = trade.complete(this.balance, this.tradesCount); + const err = trade.completeAndLog(this.balance, this.tradesCount); if (err) { logger.warn({ error: err }, `Failed to write trade in journal`); } @@ -517,7 +517,7 @@ export class Bot { if (direction === 'sell') { if (trade) { const amountOut = Number(computedAmountOut.amountOut.toFixed()); - trade.confirmedSell(amountOut) + trade.computeProfit(amountOut) } } } diff --git a/helpers/trade.ts b/helpers/trade.ts index 7dbc7d50..33061b3e 100644 --- a/helpers/trade.ts +++ b/helpers/trade.ts @@ -78,14 +78,14 @@ export class Trade { } // Trade sold, compute profit. - confirmedSell(amountOut: number) { + computeProfit(amountOut: number) { this.data.amountOut = amountOut; this.data.profit = this.data.amountOut - this.data.amountIn.valueOf(); this.data.profitPercent = (this.data.profit / this.data.amountIn.valueOf()) * 100; } // Trade completed, save data to log file - complete(balance: number, id: number) { + completeAndLog(balance: number, id: number) { this.data.balance = balance; this.data.end = new Date(); this.data.id = id; From 9105091671cc6922c397cc773117f150b21f58a8 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Sun, 12 May 2024 09:49:51 +0200 Subject: [PATCH 4/5] add fee into profit computation --- bot.ts | 3 ++- helpers/trade.ts | 4 ++-- index.ts | 10 ++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/bot.ts b/bot.ts index 55be81f6..453b2091 100644 --- a/bot.ts +++ b/bot.ts @@ -46,6 +46,7 @@ export interface BotConfig { maxSellRetries: number; unitLimit: number; unitPrice: number; + fee: number, takeProfit: number; stopLoss: number; buySlippage: number; @@ -517,7 +518,7 @@ export class Bot { if (direction === 'sell') { if (trade) { const amountOut = Number(computedAmountOut.amountOut.toFixed()); - trade.computeProfit(amountOut) + trade.computeProfit(amountOut, this.config.fee); } } } diff --git a/helpers/trade.ts b/helpers/trade.ts index 33061b3e..88166674 100644 --- a/helpers/trade.ts +++ b/helpers/trade.ts @@ -78,9 +78,9 @@ export class Trade { } // Trade sold, compute profit. - computeProfit(amountOut: number) { + computeProfit(amountOut: number, fee: number) { this.data.amountOut = amountOut; - this.data.profit = this.data.amountOut - this.data.amountIn.valueOf(); + this.data.profit = this.data.amountOut - this.data.amountIn.valueOf() - 2 * fee; this.data.profitPercent = (this.data.profit / this.data.amountIn.valueOf()) * 100; } diff --git a/index.ts b/index.ts index bbc49d9a..9e440ddd 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,7 @@ import { MarketCache, PoolCache } from './cache'; import { Listeners } from './listeners'; -import { Connection, KeyedAccountInfo, Keypair } from '@solana/web3.js'; -import { LIQUIDITY_STATE_LAYOUT_V4, MARKET_STATE_LAYOUT_V3, Token, TokenAmount } from '@raydium-io/raydium-sdk'; +import { Connection, KeyedAccountInfo, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js'; +import { LIQUIDITY_STATE_LAYOUT_V4, MARKET_STATE_LAYOUT_V3, Currency, CurrencyAmount, Token, TokenAmount } from '@raydium-io/raydium-sdk'; import { AccountLayout, getAssociatedTokenAddressSync } from '@solana/spl-token'; import { Bot, BotConfig } from './bot'; import { DefaultTransactionExecutor, TransactionExecutor } from './transactions'; @@ -147,17 +147,22 @@ const runListener = async () => { const marketCache = new MarketCache(connection); const poolCache = new PoolCache(); let txExecutor: TransactionExecutor; + let fee = 0; switch (TRANSACTION_EXECUTOR) { case 'warp': { txExecutor = new WarpTransactionExecutor(CUSTOM_FEE); + fee = new CurrencyAmount(Currency.SOL, CUSTOM_FEE, false).raw.toNumber() / LAMPORTS_PER_SOL; break; } case 'jito': { txExecutor = new JitoTransactionExecutor(CUSTOM_FEE, connection); + fee = new CurrencyAmount(Currency.SOL, CUSTOM_FEE, false).raw.toNumber() / LAMPORTS_PER_SOL; break; } default: { + const MICROLAMPORTS_PER_LAMPORT = 0.000001; + fee = COMPUTE_UNIT_LIMIT * COMPUTE_UNIT_PRICE * MICROLAMPORTS_PER_LAMPORT * LAMPORTS_PER_SOL; txExecutor = new DefaultTransactionExecutor(connection); break; } @@ -185,6 +190,7 @@ const runListener = async () => { maxBuyRetries: MAX_BUY_RETRIES, unitLimit: COMPUTE_UNIT_LIMIT, unitPrice: COMPUTE_UNIT_PRICE, + fee: fee, takeProfit: TAKE_PROFIT, stopLoss: STOP_LOSS, buySlippage: BUY_SLIPPAGE, From fb7f006e2bf8466670b3eee434d30babb20723c1 Mon Sep 17 00:00:00 2001 From: Theo Brigitte Date: Thu, 16 May 2024 16:00:01 +0200 Subject: [PATCH 5/5] rework trade log implementation account for solana transaction fee --- bot.ts | 40 ++++++++-------- helpers/trade.ts | 116 +++++++++++++++++++++++++---------------------- 2 files changed, 82 insertions(+), 74 deletions(-) diff --git a/bot.ts b/bot.ts index 453b2091..8feeb305 100644 --- a/bot.ts +++ b/bot.ts @@ -156,8 +156,6 @@ 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()), @@ -174,7 +172,9 @@ 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 { @@ -204,10 +204,6 @@ export class Bot { }, `Confirmed buy tx`, ); - - trade.transitionEnd(); - trade.open(); - this.trades.set(poolState.baseMint.toString(), trade); break; } @@ -225,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(); @@ -303,11 +300,6 @@ export class Bot { }, `Confirmed sell tx`, ); - - if (trade) { - trade.transitionEnd(); - trade.close(); - } break; } @@ -326,18 +318,19 @@ export class Bot { } catch (error) { logger.error({ mint: rawAccount.mint.toString(), error }, `Failed to sell token`); if (trade) { - trade.closeFailed(); + trade.close(0, 0, 'sell_failed'); + this.balance += trade.profit; } } finally { + await this.updateBalance(); if (trade) { - await this.updateBalance(); 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()); } - this.trades.delete(rawAccount.mint.toString()); if (this.config.oneTokenAtATime) { this.sellExecutionCount--; } @@ -511,14 +504,23 @@ export class Bot { } 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 === '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') { - if (trade) { + 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.computeProfit(amountOut, this.config.fee); + trade.close(amountOut, this.config.fee + (Number(computedAmountOut.fee.toFixed()) / LAMPORTS_PER_SOL), 'closed'); + this.balance += trade.profit; } } } diff --git a/helpers/trade.ts b/helpers/trade.ts index 88166674..b90f878e 100644 --- a/helpers/trade.ts +++ b/helpers/trade.ts @@ -2,92 +2,90 @@ 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, - } +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 +} - private transition_start: Date - private transition_end: Date +export class Trade { + private data: TradeData + private transition_start: number + private transition_end: number 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.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 = new Date(); - this.transition_end = new Date(); + this.transition_start = 0; + this.transition_end = 0; this.logFilename = logFilename; } // Help mesure entry and exit time of a trade transitionStart() { - this.transition_start = new Date(); + this.transition_start = Date.now(); } // Help mesure entry and exit time of a trade transitionEnd() { - this.transition_end = new Date(); + this.transition_end = Date.now(); } // Trade position entered - open() { - const duration = this.transition_end.getTime() - this.transition_start.getTime(); + 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 exited - close() { - const duration = this.transition_end.getTime() - this.transition_start.getTime(); + // 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.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. - computeProfit(amountOut: number, fee: number) { + this.data.end = new Date(); this.data.amountOut = amountOut; - this.data.profit = this.data.amountOut - this.data.amountIn.valueOf() - 2 * fee; - this.data.profitPercent = (this.data.profit / this.data.amountIn.valueOf()) * 100; + 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.end = new Date(); this.data.id = id; if (this.logFilename !== 'none') { @@ -98,4 +96,12 @@ export class Trade { } } } + + get profit() { + return this.data.profit; + } + + get amountIn() { + return this.data.amountIn; + } }