├── trade ├── index.ts └── trade.ts ├── wallet ├── index.ts └── wallet.ts ├── constants ├── index.ts └── constants.ts ├── indicators ├── index.ts └── indicators.ts ├── assets └── tradie.png ├── .env.copy ├── utils ├── logger.ts └── index.ts ├── package.json ├── tsconfig.json ├── LICENSE ├── .gitignore ├── README.md └── index.ts /trade/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trade' 2 | -------------------------------------------------------------------------------- /wallet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './wallet' 2 | -------------------------------------------------------------------------------- /constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants' 2 | -------------------------------------------------------------------------------- /indicators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './indicators'; 2 | -------------------------------------------------------------------------------- /assets/tradie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oboshto/tradie/HEAD/assets/tradie.png -------------------------------------------------------------------------------- /.env.copy: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY= 2 | CRYPTO_COMPARE_API_KEY= 3 | RPC_ENDPOINT= 4 | BUY_TOKEN_ADDRESS= 5 | QUOTE_SYMBOL=USDC 6 | CANDLE_AGGREGATE_MINUTES=15 7 | GET_MARKET_DATA_INTERVAL_SECONDS=30 8 | SLIPPAGE_PERCENT=10 9 | STOP_LOSS=7 10 | TAKE_PROFIT=25 11 | RSI_TO_BUY=30 12 | RSI_TO_SELL=70 13 | TRANSACTION_PRIORITY_FEE=auto 14 | LOG_LEVEL=info 15 | -------------------------------------------------------------------------------- /utils/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from "pino"; 2 | 3 | const transport = pino.transport({ 4 | target: 'pino-pretty', 5 | }); 6 | 7 | export const logger = pino( 8 | { 9 | level: 'info', 10 | serializers: { 11 | error: pino.stdSerializers.err, 12 | }, 13 | base: undefined, 14 | }, 15 | transport, 16 | ); 17 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger' 2 | 3 | export const request = async function request( 4 | url: string, 5 | config: RequestInit 6 | ): Promise { 7 | const response = await fetch(url, config); 8 | return await response.json(); 9 | } 10 | 11 | export const sleep = (ms:number) => new Promise(res => setTimeout(res, ms)); 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tradie", 3 | "version": "1.0.1", 4 | "description": "Tradie is a SOLANA trading bot written in typescript. It uses RSI, EMA and Bollinger-Bands technical indicators to buy or sell cryptocurrencies.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node index.ts" 8 | }, 9 | "author": "oboshto@gmail.com", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@debut/indicators": "^1.3.20", 13 | "@metaplex-foundation/js": "^0.20.1", 14 | "@project-serum/anchor": "^0.26.0", 15 | "@solana/spl-token": "^0.4.3", 16 | "@solana/spl-token-registry": "^0.2.4574", 17 | "@solana/web3.js": "^1.91.4", 18 | "bs58": "^5.0.0", 19 | "cross-fetch": "^4.0.0", 20 | "dotenv": "^16.4.5", 21 | "pino": "^8.20.0", 22 | "pino-pretty": "^11.0.0", 23 | "pino-std-serializers": "^6.2.2", 24 | "ts-node": "^10.9.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | "target": "es2016", 5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "module": "commonjs", 7 | /* Specify what module code is generated. */ 8 | "resolveJsonModule": true, 9 | /* Enable importing .json files. */ 10 | "esModuleInterop": true, 11 | /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 12 | "forceConsistentCasingInFileNames": true, 13 | /* Ensure that casing is correct in imports. */ 14 | "strict": true, 15 | /* Enable all strict type-checking options. */ 16 | "skipLibCheck": true, 17 | /* Skip type checking all .d.ts files. */ 18 | "types": [ 19 | "node" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 oboshto@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /indicators/indicators.ts: -------------------------------------------------------------------------------- 1 | import {BollingerBands, EMA, RSI} from "@debut/indicators"; 2 | 3 | export const getRSI = (dataFrame: any[]) => { 4 | const dataLength = dataFrame.length 5 | let resultRSI!:number 6 | const rsi = new RSI(14) 7 | 8 | dataFrame.forEach((point: { close: number; }, index: number) => { 9 | rsi.nextValue(point?.close) 10 | 11 | if (index === dataLength - 1) { 12 | resultRSI = rsi.momentValue(point?.close) 13 | } 14 | }) 15 | 16 | return resultRSI 17 | } 18 | 19 | export const getBB = (dataFrame: any[]) => { 20 | const dataLength = dataFrame.length 21 | let resultBB!:{lower:number, middle:number, upper:number} 22 | const bb = new BollingerBands(14) 23 | 24 | dataFrame.forEach((point: { close: number; }, index: number) => { 25 | bb.nextValue(point?.close) 26 | 27 | if (index === dataLength - 1) { 28 | resultBB = bb.momentValue(point?.close) 29 | } 30 | }) 31 | 32 | return resultBB 33 | } 34 | 35 | export const getEMA = (dataFrame: any[], period = 14) => { 36 | const dataLength = dataFrame.length 37 | let resultEMA!:number 38 | const ema = new EMA(period) 39 | 40 | dataFrame.forEach((point: { close: number; }, index: number) => { 41 | ema.nextValue(point?.close) 42 | 43 | if (index === dataLength - 1) { 44 | resultEMA = ema.momentValue(point?.close) 45 | } 46 | }) 47 | 48 | return resultEMA 49 | } 50 | -------------------------------------------------------------------------------- /constants/constants.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import {logger} from "../utils"; 3 | 4 | dotenv.config(); 5 | 6 | export const PRIVATE_KEY = retrieveEnvVariable('PRIVATE_KEY') 7 | export const CRYPTO_COMPARE_API_KEY = retrieveEnvVariable('CRYPTO_COMPARE_API_KEY') 8 | export const RPC_ENDPOINT = retrieveEnvVariable('RPC_ENDPOINT') 9 | export const CANDLE_AGGREGATE_MINUTES = retrieveEnvVariable('CANDLE_AGGREGATE_MINUTES') 10 | export const GET_MARKET_DATA_INTERVAL_SECONDS = Number(retrieveEnvVariable('GET_MARKET_DATA_INTERVAL_SECONDS')) 11 | export const BUY_TOKEN_ADDRESS = retrieveEnvVariable('BUY_TOKEN_ADDRESS') 12 | export const QUOTE_SYMBOL = retrieveEnvVariable('QUOTE_SYMBOL') 13 | export const SLIPPAGE_PERCENT = Number(retrieveEnvVariable('SLIPPAGE_PERCENT')) 14 | export const STOP_LOSS = Number(retrieveEnvVariable('STOP_LOSS')) 15 | export const TAKE_PROFIT = Number(retrieveEnvVariable('TAKE_PROFIT')) 16 | export const RSI_TO_BUY = Number(retrieveEnvVariable('RSI_TO_BUY')) 17 | export const RSI_TO_SELL = Number(retrieveEnvVariable('RSI_TO_SELL')) 18 | export const LOG_LEVEL = retrieveEnvVariable('LOG_LEVEL') 19 | 20 | const transactionPriorityFee = Number(retrieveEnvVariable('TRANSACTION_PRIORITY_FEE')) 21 | export const TRANSACTION_PRIORITY_FEE: 'auto' | number = isNaN(transactionPriorityFee) ? 'auto' : transactionPriorityFee // 'auto' or SOL value like 0.004 22 | 23 | 24 | function retrieveEnvVariable(variableName: string) { 25 | const variable = process.env[variableName] || ''; 26 | if (!variable) { 27 | logger.error(`${variableName} is not set`); 28 | process.exit(1); 29 | } 30 | return variable; 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # PNPM 126 | pnpm-lock.yaml 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | # JetBrains 136 | .idea -------------------------------------------------------------------------------- /wallet/wallet.ts: -------------------------------------------------------------------------------- 1 | import {Wallet} from "@project-serum/anchor"; 2 | import {Keypair, Connection, PublicKey, LAMPORTS_PER_SOL, GetProgramAccountsFilter,} from "@solana/web3.js"; 3 | import {TOKEN_PROGRAM_ID} from "@solana/spl-token" 4 | import bs58 from "bs58" 5 | import {Metaplex} from "@metaplex-foundation/js"; 6 | import {ENV, TokenListProvider} from "@solana/spl-token-registry"; 7 | 8 | import {PRIVATE_KEY, RPC_ENDPOINT} from "../constants" 9 | import pino from "pino"; 10 | import Logger = pino.Logger; 11 | 12 | const connection = new Connection(RPC_ENDPOINT); 13 | 14 | export const wallet = new Wallet(Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY))); 15 | 16 | export async function getSolBalance() { 17 | const publicKey = wallet.publicKey.toString() 18 | let balance = await connection.getBalance(new PublicKey(publicKey)); 19 | return balance / LAMPORTS_PER_SOL 20 | } 21 | 22 | export async function getTokenAmountByAddress(mintAddress: string, logger: Logger) { 23 | const publicKey = wallet.publicKey.toString() 24 | 25 | const filters: GetProgramAccountsFilter[] = [ 26 | { 27 | dataSize: 165, 28 | }, 29 | { 30 | memcmp: { 31 | offset: 32, 32 | bytes: publicKey 33 | } 34 | }, 35 | { 36 | memcmp: { 37 | offset: 0, 38 | bytes: mintAddress, 39 | } 40 | } 41 | ] 42 | 43 | const accounts = await connection.getParsedProgramAccounts( 44 | TOKEN_PROGRAM_ID, 45 | {filters: filters} 46 | ) 47 | 48 | if (!accounts.length) { 49 | logger.debug(`Token not found on active account. Trying to get meta data from another sources.`) 50 | 51 | let tokenInfo 52 | try { 53 | tokenInfo = await getTokenMetadata(mintAddress) 54 | 55 | return {amount: 0, decimals: tokenInfo.decimals} 56 | } catch (error) { 57 | throw new Error(`Token account not found for address: ${mintAddress}`) 58 | } 59 | } 60 | 61 | const parsedAccountInfo: any = accounts[0]?.account?.data; 62 | const {amount, decimals} = parsedAccountInfo.parsed.info.tokenAmount 63 | 64 | return {amount, decimals} 65 | } 66 | 67 | 68 | async function getTokenMetadata(address: string) { 69 | const metaplex = Metaplex.make(connection); 70 | 71 | const mintAddress = new PublicKey(address); 72 | 73 | const metadataAccount = metaplex 74 | .nfts() 75 | .pdas() 76 | .metadata({mint: mintAddress}); 77 | 78 | const metadataAccountInfo = await connection.getAccountInfo(metadataAccount); 79 | 80 | if (metadataAccountInfo) { 81 | const token = await metaplex.nfts().findByMint({mintAddress: mintAddress}); 82 | return token.mint 83 | } else { 84 | const provider = await new TokenListProvider().resolve(); 85 | const tokenList = provider.filterByChainId(ENV.MainnetBeta).getList(); 86 | const tokenMap = tokenList.reduce((map, item) => { 87 | map.set(item.address, item); 88 | return map; 89 | }, new Map()); 90 | 91 | const token = tokenMap.get(mintAddress.toBase58()); 92 | 93 | return token.mint 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /trade/trade.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | LAMPORTS_PER_SOL, 4 | PublicKey, 5 | VersionedTransaction, 6 | } from "@solana/web3.js"; 7 | import { 8 | RPC_ENDPOINT, 9 | SLIPPAGE_PERCENT, 10 | TRANSACTION_PRIORITY_FEE, 11 | } from "../constants"; 12 | import fetch from "cross-fetch"; 13 | import {wallet} from "../wallet"; 14 | import pino from "pino"; 15 | import Logger = pino.Logger; 16 | 17 | const referralKey: string = 'J1igXZJiJjsBWKLGPna9egHFYZjW5dPDGv7ajPDqp4Pv' 18 | const platformFeePercent = 0.2 19 | 20 | const prioritizationFeeLamports = TRANSACTION_PRIORITY_FEE === 'auto' ? 'auto' : TRANSACTION_PRIORITY_FEE * (10 ** LAMPORTS_PER_SOL) 21 | 22 | const connection = new Connection(RPC_ENDPOINT) 23 | 24 | const feeAccounts = [ 25 | 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm', 26 | 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 27 | 'So11111111111111111111111111111111111111112', 28 | 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 29 | ] 30 | 31 | export const buyToken = async (mintIn: string, mintOut: string, amount: string, logger: Logger) => { 32 | logger.debug(`[buyToken call] with: mintIn: ${mintIn}, mintOut: ${mintOut}, amount: ${amount}`) 33 | 34 | let feeAccount = null 35 | if (feeAccounts.includes(mintOut)) { 36 | logger.debug('fee account found!'); 37 | [feeAccount] = PublicKey.findProgramAddressSync( 38 | [ 39 | Buffer.from("referral_ata"), 40 | new PublicKey(referralKey).toBuffer(), 41 | new PublicKey(mintOut).toBuffer(), 42 | ], 43 | new PublicKey("REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3") 44 | ) 45 | } else { 46 | logger.debug('fee account is not found') 47 | } 48 | 49 | 50 | const quoteResponse = await ( 51 | await fetch(`https://quote-api.jup.ag/v6/quote?inputMint=${mintIn}\ 52 | &outputMint=${mintOut}\ 53 | &amount=${amount}\ 54 | &slippageBps=${SLIPPAGE_PERCENT * 100}\ 55 | &platformFeeBps=${platformFeePercent * 100}` 56 | ) 57 | ).json(); 58 | 59 | const {swapTransaction} = await ( 60 | await fetch('https://quote-api.jup.ag/v6/swap', { 61 | method: 'POST', 62 | headers: { 63 | 'Content-Type': 'application/json' 64 | }, 65 | body: JSON.stringify({ 66 | quoteResponse, 67 | userPublicKey: wallet.publicKey.toString(), 68 | wrapAndUnwrapSol: true, 69 | feeAccount, 70 | dynamicComputeUnitLimit: false, 71 | prioritizationFeeLamports 72 | }) 73 | }) 74 | ).json() 75 | 76 | const swapTransactionBuf = Buffer.from(swapTransaction, 'base64') 77 | const transaction = VersionedTransaction.deserialize(swapTransactionBuf) 78 | 79 | transaction.sign([wallet.payer]) 80 | 81 | const latestBlockhash = await connection.getLatestBlockhash({ 82 | commitment: 'finalized', 83 | }); 84 | 85 | const signature = await connection.sendRawTransaction(transaction.serialize(), { 86 | preflightCommitment: 'processed', 87 | maxRetries: 15 88 | }) 89 | 90 | logger.info(`Sent buy tx with signature: ${signature}.`) 91 | 92 | const confirmation = await connection.confirmTransaction( 93 | { 94 | signature, 95 | lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, 96 | blockhash: latestBlockhash.blockhash, 97 | }, 98 | 'processed', 99 | ); 100 | 101 | if (confirmation.value.err) { 102 | logger.debug(confirmation.value.err); 103 | throw new Error(`Error confirming buy tx: ${confirmation.value.err}`) 104 | } 105 | 106 | logger.debug(`Buy transaction completed.`); 107 | logger.info(`Confirmed buy tx: https://solscan.io/tx/${signature}?cluster=mainnet-beta`); 108 | } 109 | 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tradie - Solana Trading Bot 2 | 3 | Tradie is an innovative, open-source trading bot tailored for the Solana blockchain. Leveraging Jupiter for token swapping and CryptoCompare for market analysis, Tradie aims to empower traders with automated, sophisticated trading strategies. Developed with TypeScript, Tradie offers robust, type-safe code, enhancing both performance and maintainability. 4 | 5 | ![Tradie](./assets/tradie.png) 6 | 7 | ## Features 8 | 9 | - **Efficient Token Swapping**: Utilizes Jupiter for seamless token exchanges on the Solana blockchain. 10 | - **Detailed Market Analysis**: Leverages candlestick data from CryptoCompare to guide trading decisions. 11 | - **Advanced Technical Indicators**: Incorporates indicators like Relative Strength Index (RSI), short and medium Exponential Moving Averages (EMA), and Bollinger Bands (BB) for market analysis. 12 | - **Customizable Trading Strategies**: Allows detailed configuration of slippage, stop loss, take profit, RSI. 13 | - **Real-Time Data Processing**: Fetches market data at user-defined intervals for timely trading decisions. 14 | - **Transparent Logging**: Provides extensive logging to monitor operations and performance. 15 | 16 | ## Prerequisites 17 | 18 | - Node.js (version 21+ recommended, but should be compatible with older versions) installed on your system. Visit [Node.js](https://nodejs.org/) for installation instructions. 19 | - A Solana wallet private key. 20 | - A CryptoCompare API key for market data access, obtainable [here](https://www.cryptocompare.com/cryptopian/api-keys). 21 | - Solana RPC endpoint, obtainable [here](https://quicknodes.com/). 22 | 23 | ## Technical Details 24 | 25 | - **TypeScript**: Tradie is written in TypeScript, offering enhanced code quality and developer experience. Make sure you're familiar with TypeScript for any contributions or customizations. 26 | 27 | ## Installation 28 | 29 | 1. Clone the Tradie repository: 30 | ```bash 31 | git clone https://github.com/oboshto/tradie.git 32 | cd tradie 33 | ``` 34 | 35 | 2. Install dependencies: 36 | ```bash 37 | npm install 38 | ``` 39 | 40 | ## Configuration 41 | 42 | 1. Rename `.env.copy` to `.env` and configure your settings. 43 | 2. Open the .env file in a text editor and fill in your details: 44 | 45 | - `PRIVATE_KEY`: Your Solana wallet private key. 46 | - `CRYPTO_COMPARE_API_KEY`: Your API key from CryptoCompare. 47 | - `RPC_ENDPOINT`: Your Solana RPC endpoint URL. 48 | - `BUY_TOKEN_ADDRESS`: The address of the token you want to buy. 49 | - `QUOTE_SYMBOL`: The symbol of the quote currency (`default: USDC`). 50 | - `CANDLE_AGGREGATE_MINUTES`: Candlestick aggregation period in minutes (`default: 15`). 51 | - `GET_MARKET_DATA_INTERVAL_SECONDS`: Interval for fetching market data in seconds (`default: 30`). 52 | - `SLIPPAGE_PERCENT`: Maximum acceptable slippage percentage (`default: 10`). 53 | - `STOP_LOSS`: Stop loss percentage (`default: 7`). 54 | - `TAKE_PROFIT`: Take profit percentage (`default: 25`). 55 | - `RSI_TO_BUY`: RSI lower value to buy (`default: 30`). 56 | - `RSI_TO_SELL`: RSI upper value to sell (`default: 70`). 57 | - `TRANSACTION_PRIORITY_FEE`: solana transaction priority fee (`default: 'auto'`, or in SOL e.g. `0.005`). 58 | - `LOG_LEVEL`: Logging level (`deafult: info`). 59 | 60 | ## Usage 61 | 62 | Start the bot with `npm start`. Tradie will execute trades based on your settings. 63 | 64 | ## Future Plans 65 | 66 | - **Indicator Customization**: Plans for more customizable indicator values. 67 | - **More Technical Indicators**: Expanding the array of technical analysis tools available. 68 | 69 | ## Risk Disclaimer 70 | 71 | The use of Tradie and any automated trading software inherently involves financial risks, including the potential loss of funds. The developer(s) of Tradie cannot be held responsible for any financial losses incurred while using the bot. Users should trade with caution and only with funds they can afford to lose. This software is provided "as is", with no guarantee of profitability or performance. 72 | 73 | ## Community 74 | 75 | Join our Discord for discussions and support: [Tradie Discord](https://discord.gg/ApF28mbYkf). 76 | 77 | ## Contributing 78 | 79 | Fork the repo, create a feature branch, and submit your pull request for improvements. 80 | 81 | ## License 82 | 83 | Available under the [MIT License](LICENSE), promoting open collaboration. 84 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getEMA, 3 | getBB, 4 | getRSI 5 | } from './indicators' 6 | import { 7 | CRYPTO_COMPARE_API_KEY, 8 | LOG_LEVEL, 9 | CANDLE_AGGREGATE_MINUTES, 10 | GET_MARKET_DATA_INTERVAL_SECONDS, 11 | STOP_LOSS, 12 | TAKE_PROFIT, 13 | BUY_TOKEN_ADDRESS, 14 | QUOTE_SYMBOL, 15 | SLIPPAGE_PERCENT, 16 | TRANSACTION_PRIORITY_FEE, 17 | RSI_TO_BUY, 18 | RSI_TO_SELL, 19 | } from './constants' 20 | 21 | import { 22 | logger, 23 | request, 24 | sleep, 25 | } from "./utils" 26 | 27 | import {version} from './package.json' 28 | import * as fs from "fs" 29 | import { 30 | getSolBalance, 31 | getTokenAmountByAddress, 32 | } from "./wallet"; 33 | import {buyToken} from "./trade"; 34 | 35 | let activePosition: any | null = null 36 | const positionFilePath = './position.json' 37 | 38 | let buySymbol: string 39 | let quoteAddress: string 40 | 41 | let solBalance: number 42 | let buyTokenBalance: number 43 | let buyTokenDecimals: number 44 | let quoteTokenBalance: number 45 | let quoteTokenDecimals: number 46 | 47 | const main = async () => { 48 | await init() 49 | 50 | try { 51 | await analyzeMarket() 52 | setTimeout(runAnalyzeMarket, GET_MARKET_DATA_INTERVAL_SECONDS * 1000) 53 | } catch (error) { 54 | logger.error(error, 'Error occurred while analyzing market:') 55 | } 56 | } 57 | 58 | const runAnalyzeMarket = async () => { 59 | try { 60 | await analyzeMarket() 61 | } catch (error) { 62 | logger.error(error, 'Error occurred while analyzing market:') 63 | } finally { 64 | setTimeout(runAnalyzeMarket, GET_MARKET_DATA_INTERVAL_SECONDS * 1000) 65 | } 66 | } 67 | 68 | async function init() { 69 | logger.level = LOG_LEVEL 70 | logger.info(` 71 | 88888888888 8888888b. d8888 8888888b. 8888888 8888888888 72 | 888 888 Y88b d88888 888 Y88b 888 888 73 | 888 888 888 d88P888 888 888 888 888 74 | 888 888 d88P d88P 888 888 888 888 8888888 75 | 888 8888888P" d88P 888 888 888 888 888 76 | 888 888 T88b d88P 888 888 888 888 888 77 | 888 888 T88b d8888888888 888 .d88P 888 888 78 | 888 888 T88b d88P 888 8888888P" 8888888 8888888888 79 | 80 | Solana ultimate trading bot. version: ${version} 81 | `) 82 | 83 | logger.info(`Stop Loss is ${STOP_LOSS}%.`) 84 | logger.info(`Take Profit is ${TAKE_PROFIT}%.`) 85 | logger.info(`Analyzing market price every ${GET_MARKET_DATA_INTERVAL_SECONDS} seconds.`) 86 | logger.info(`Candle aggregate for ${CANDLE_AGGREGATE_MINUTES} min.`) 87 | logger.info(`Slippage is ${SLIPPAGE_PERCENT}%.`) 88 | logger.info(`Transaction priority fee is ${TRANSACTION_PRIORITY_FEE}.`) 89 | 90 | try { 91 | const assets = await getAssetsData() 92 | buySymbol = assets.buySymbol 93 | quoteAddress = assets.quoteAddress 94 | logger.debug(`Quote Token Symbol: ${QUOTE_SYMBOL}. Found Address: ${quoteAddress}`) 95 | } catch (error) { 96 | logger.error(error, 'Error occurred while getting assets data') 97 | process.exit(1) 98 | } 99 | 100 | await getBalances() 101 | 102 | logger.info(`Start trading ${buySymbol}-${QUOTE_SYMBOL}.`) 103 | 104 | try { 105 | await loadSavedPosition() 106 | if (activePosition) { 107 | logger.info(`Saved position found. Balance: ${activePosition.amount} ${activePosition.buySymbol}. BuyPrice is ${activePosition.buyPrice} ${activePosition.quoteSymbol}.`) 108 | } 109 | } catch (error) { 110 | logger.error(error, 'Error occurred while loading the saved position from file') 111 | } 112 | 113 | logger.info('———————————————————————') 114 | } 115 | 116 | async function analyzeMarket() { 117 | const candleData = await getCandleData() 118 | if (!candleData || !candleData.Data || !candleData.Data.Data || !candleData.Data.Data.length) { 119 | if (candleData.Response === 'Error') { 120 | throw new Error(`Failed to fetch candle data: ${candleData.Message}`) 121 | } else { 122 | throw new Error('Failed to fetch candle data or data is empty') 123 | } 124 | } 125 | 126 | const data = candleData.Data.Data 127 | const closePrice = data[data.length - 1].close 128 | const emaShort = getEMA(data, 5) 129 | const emaMedium = getEMA(data, 20) 130 | const bb = getBB(data) 131 | const rsi = getRSI(data) 132 | 133 | logger.info(`Price: ${closePrice} ${QUOTE_SYMBOL}`) 134 | logger.info(`EMA short: ${emaShort}`) 135 | logger.info(`EMA medium: ${emaMedium}`) 136 | logger.info(`BB lower: ${bb.lower}`) 137 | logger.info(`BB upper: ${bb.upper}`) 138 | logger.info(`RSI: ${rsi}`) 139 | 140 | if (buyTokenBalance > 0) { 141 | logger.debug(`Buy Token balance is ${buyTokenBalance} ${buySymbol}. Looking for sell signal...`) 142 | 143 | if (activePosition) { 144 | if (closePrice <= activePosition.buyPrice * (100 - STOP_LOSS) / 100) { 145 | logger.warn(`Stop Loss is reached. Start selling...`) 146 | await sell(closePrice) 147 | } 148 | 149 | if (closePrice >= activePosition.buyPrice * (100 + TAKE_PROFIT) / 100) { 150 | logger.warn(`Take Profit is reached. Start selling...`) 151 | await sell(closePrice) 152 | } 153 | } 154 | 155 | if (((emaShort < emaMedium) || (closePrice > bb.upper)) && rsi >= RSI_TO_SELL) { 156 | logger.warn(`SELL signal is detected. Start selling...`) 157 | await sell(closePrice) 158 | } 159 | } 160 | 161 | if (quoteTokenBalance > 0) { 162 | logger.debug(`Quote Token balance is ${quoteTokenBalance} ${QUOTE_SYMBOL}. Looking for buy signal...`) 163 | 164 | if (((emaShort > emaMedium) || (closePrice < bb.lower)) && rsi <= RSI_TO_BUY) { 165 | logger.warn(`BUY signal is detected. Buying...`) 166 | await buy(closePrice) 167 | } 168 | } 169 | 170 | logger.info('———————————————————————') 171 | } 172 | 173 | async function sell(price: number) { 174 | if (activePosition) { 175 | logger.warn(`Price difference is ${price - activePosition.buyPrice} (${Math.sign(price - activePosition.buyPrice) * Math.round((activePosition.buyPrice / price) * 100 - 100) / 100}%)`) 176 | } 177 | await getBalances() 178 | 179 | const amountWithDecimals = buyTokenBalance * (10 ** buyTokenDecimals) 180 | 181 | try { 182 | await buyToken(BUY_TOKEN_ADDRESS, quoteAddress, amountWithDecimals.toString(), logger) 183 | } catch (error: any) { 184 | if (error.err) { 185 | logger.error(`Got error on sell transaction: ${error.err.message}`) 186 | } 187 | 188 | logger.error(error, `Got some error on sell transaction`) 189 | 190 | return 191 | } 192 | 193 | logger.warn(`Sold ${buyTokenBalance} ${buySymbol}.`) 194 | 195 | logger.info(`sleeping for 30s`) 196 | await sleep(30000) // sleep for 15s / todo: make it nicer 197 | await getBalances() 198 | logger.warn(`Bought ${quoteTokenBalance} ${QUOTE_SYMBOL}. 1 ${buySymbol} = ${price} ${QUOTE_SYMBOL}`) 199 | await clearSavedPosition() 200 | } 201 | 202 | async function buy(price: number) { 203 | await getBalances() 204 | 205 | const amountWithDecimals = quoteTokenBalance * (10 ** quoteTokenDecimals) 206 | 207 | try { 208 | await buyToken(quoteAddress, BUY_TOKEN_ADDRESS, amountWithDecimals.toString(), logger) 209 | } catch (error: any) { 210 | if (error.err) { 211 | logger.error(`Got error on buy transaction: ${error.err.message}`) 212 | } 213 | 214 | logger.error(error, `Got some error on buy transaction`) 215 | return 216 | } 217 | 218 | if (activePosition) { 219 | logger.info('Previous active position found. Updating...') 220 | } 221 | 222 | logger.warn(`Sold ${quoteTokenBalance} ${QUOTE_SYMBOL}. For ${price} ${QUOTE_SYMBOL} per ${buySymbol}`) 223 | 224 | logger.info(`sleeping for 30s`) 225 | await sleep(30000) // sleep for 15s / todo: make it nicer 226 | await getBalances() 227 | 228 | logger.warn(`Bought ${buyTokenBalance} ${buySymbol}.`) 229 | 230 | 231 | activePosition = { 232 | buyPrice: activePosition ? (activePosition + price) / 2 : price, 233 | amount: buyTokenBalance, 234 | buySymbol: buySymbol, 235 | quoteSymbol: QUOTE_SYMBOL 236 | } 237 | await savePosition() 238 | } 239 | 240 | async function getAssetDataByAddress(address: string): Promise { 241 | return await request( 242 | 'https://data-api.cryptocompare.com/onchain/v1/data/by/address?chain_symbol=SOL' + 243 | '&address=' + address + 244 | '&api_key=' + CRYPTO_COMPARE_API_KEY, 245 | {}) 246 | } 247 | 248 | async function getAssetDataByToken(token: string): Promise { 249 | return await request( 250 | `https://price.jup.ag/v4/price?ids=${token}`, 251 | {}) 252 | } 253 | 254 | async function getCandleData(): Promise { 255 | return await request( 256 | 'https://min-api.cryptocompare.com/data/v2/histominute?limit=50' + 257 | '&fsym=' + buySymbol + 258 | '&tsym=' + QUOTE_SYMBOL + 259 | '&aggregate=' + CANDLE_AGGREGATE_MINUTES, 260 | { 261 | method: 'GET', 262 | headers: {'authorization': CRYPTO_COMPARE_API_KEY}, 263 | }) 264 | } 265 | 266 | async function loadSavedPosition() { 267 | if (fs.existsSync(positionFilePath)) { 268 | const data = fs.readFileSync(positionFilePath, 'utf8') 269 | activePosition = JSON.parse(data) 270 | if (activePosition.buySymbol !== buySymbol || activePosition.quoteSymbol !== QUOTE_SYMBOL) { 271 | logger.warn(`Previously saved pair is ${activePosition.buySymbol}-${activePosition.quoteSymbol}. But now trading ${buySymbol}-${QUOTE_SYMBOL}. Clearing saved position...`) 272 | await clearSavedPosition() 273 | } 274 | logger.debug('Position loaded from file.') 275 | } else { 276 | logger.info('No previous position found. Starting fresh.') 277 | } 278 | } 279 | 280 | async function savePosition() { 281 | if (activePosition) { 282 | const data = JSON.stringify(activePosition) 283 | fs.writeFileSync(positionFilePath, data, 'utf8') 284 | logger.debug(activePosition, 'Position file saved') 285 | } 286 | } 287 | 288 | async function clearSavedPosition() { 289 | activePosition = null 290 | if (fs.existsSync(positionFilePath)) { 291 | fs.unlinkSync(positionFilePath) 292 | logger.debug('Position file was cleared.') 293 | } 294 | } 295 | 296 | async function getAssetsData() { 297 | const buyAssetData = await getAssetDataByAddress(BUY_TOKEN_ADDRESS) 298 | if (buyAssetData?.Err?.message) { 299 | throw new Error(buyAssetData.Err.message) 300 | } 301 | 302 | const buySymbol: string = buyAssetData?.Data?.SYMBOL 303 | 304 | const quoteAssetData = await getAssetDataByToken(QUOTE_SYMBOL) 305 | if (!Object.keys(quoteAssetData?.data?.[QUOTE_SYMBOL]).length) { 306 | throw new Error(`Token ${QUOTE_SYMBOL} is not found.`) 307 | } 308 | 309 | const quoteAddress: string = quoteAssetData?.data[QUOTE_SYMBOL]?.id 310 | 311 | return {buySymbol, quoteAddress} 312 | } 313 | 314 | async function getBalances() { 315 | try { 316 | logger.debug(`Getting balance amounts.`) 317 | solBalance = await getSolBalance() 318 | const buyTokenAmount = await getTokenAmountByAddress(BUY_TOKEN_ADDRESS, logger) 319 | const quoteTokenAmount = await getTokenAmountByAddress(quoteAddress, logger) 320 | 321 | buyTokenBalance = buyTokenAmount.amount / (10 ** buyTokenAmount.decimals) 322 | buyTokenDecimals = buyTokenAmount.decimals 323 | quoteTokenBalance = quoteTokenAmount.amount / (10 ** quoteTokenAmount.decimals) 324 | quoteTokenDecimals = quoteTokenAmount.decimals 325 | 326 | if (solBalance < 0.001) { 327 | logger.error('Insufficient SOL balance. 0.001 SOL required to work properly.') 328 | process.exit(1) 329 | } 330 | 331 | logger.info(`SOL Balance: ${solBalance} SOL`) 332 | logger.info(`Token balance: ${buyTokenBalance} ${buySymbol} and ${quoteTokenBalance} ${QUOTE_SYMBOL}`) 333 | } catch (error) { 334 | logger.error(error, 'Error occurred while getting wallet balances') 335 | process.exit(1) 336 | } 337 | } 338 | 339 | main(); 340 | --------------------------------------------------------------------------------