├── .gitignore ├── README.md ├── binancedata.py ├── config.py ├── requirements.txt ├── run.py └── telegrammodule.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyGrid grid bot 2 | 3 | Use at your own risk! I'm not a professional coder and I'm doing this project just for fun. 4 | 5 | 6 | Installation steps: 7 | 1. Clone this repo 8 | 2. Use the command "pip3 install -r requirements.txt" to install the requirements. 9 | 3. Configure the config.py file to your liking. 10 | 4. Make sure you pay trading fees with BNB, else the bot will get stuck eventually. 11 | 5. Use the command "python3 run.py" to start the bot. 12 | 13 | TG Commands: 14 | /balance 15 | 16 | Tips: 17 | - Use PM2 to manage the process. 18 | - Use FTX exchange, you can get 0% maker fee when you are staking 25 FTT. 19 | 20 | Tested on: 21 | - FTX with 0% fee 22 | - Binance, pay fee with BNB 23 | 24 | 25 | Bug reports will be appreciated. 26 | 27 | Features: 28 | - Trades assets in a grid-like manner. The grids are dynamic; this means there's no hassle with choosing a range! 29 | - Automatically buys BNB to pay for trading fees if your BNB balance is < 0.1 30 | - Telegram messages 31 | - Compounding 32 | - CCXT implementation 33 | 34 | 35 | 36 | Upcoming features: 37 | - Slow mode -> this means no extra buy orders will be created when price moves up and fills sell orders. 38 | - Complete refactoring of code 39 | - Decreasing amount of API calls since we get rate limited very easily. 40 | 41 | 42 | 43 | Thanks! 44 | -------------------------------------------------------------------------------- /binancedata.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | from urllib import request 3 | from config import API_KEY,API_KEY_SECRET,COMPOUND_WALLET_PERC,SYMBOL,COMPOUND,DOLLARQUANTITY,EXCHANGE 4 | from decimal import * 5 | import json 6 | import requests 7 | import math 8 | import ccxt 9 | 10 | 11 | exchange_id = EXCHANGE 12 | exchange_class = getattr(ccxt, exchange_id) 13 | exchange = exchange_class({ 14 | 'apiKey': API_KEY, 15 | 'secret': API_KEY_SECRET, 16 | 'enableRateLimit': True, 17 | }) 18 | 19 | lastQuantity = [] 20 | # fetch the BTC/USDT ticker for use in converting assets to price in USDT 21 | ticker = exchange.fetch_ticker(SYMBOL) 22 | 23 | # calculate the ticker price of BTC in terms of USDT by taking the midpoint of the best bid and ask 24 | priceUSDT = Decimal((float(ticker['ask']) + float(ticker['bid'])) / 2) 25 | print("LUNA Price = " + str(priceUSDT)) 26 | 27 | 28 | def getCurrentPrice(): 29 | priceUSDT = Decimal((float(ticker['ask']) + float(ticker['bid'])) / 2) 30 | return priceUSDT 31 | 32 | def getBalance(): 33 | balance = exchange.fetchBalance()['BUSD']['free'] 34 | print(str(balance)) 35 | return str(balance) 36 | 37 | def getDecimalAmounts(symbol): 38 | #response = requests.get("https://api.binance.com/api/v3/exchangeInfo?symbol="+symbol) 39 | #minQty = float(response.json()["symbols"][0]["filters"][2]["minQty"]) 40 | amountDecimals = 3 41 | return amountDecimals 42 | 43 | 44 | 45 | def getQuantity(): 46 | marketStructure = exchange.markets[SYMBOL] 47 | # print(marketStructure) 48 | # print(marketStructure['precision']) 49 | #lenstr = len(str(marketStructure['precision']['amount'])) 50 | lenstr = 3 51 | if COMPOUND == True: 52 | try: 53 | balance = exchange.fetchBalance()['USD']['free'] 54 | quotePrice = getCurrentPrice() 55 | quantityDollars = Decimal(balance)*Decimal((COMPOUND_WALLET_PERC/100)) 56 | quoteQuantity = quantityDollars/quotePrice 57 | 58 | 59 | lastQuantity.insert(0,round(quoteQuantity,lenstr)) 60 | print(f"Length lastQuantity = {len(lastQuantity)}") 61 | 62 | if len(lastQuantity) > 3: 63 | lastQuantity.pop() 64 | return round(quoteQuantity,lenstr) if quantityDollars > DOLLARQUANTITY else round(Decimal(DOLLARQUANTITY)/getCurrentPrice(),lenstr) 65 | except: return lastQuantity[0] 66 | else: 67 | 68 | 69 | 70 | return round(Decimal(DOLLARQUANTITY)/getCurrentPrice(),lenstr) 71 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from decimal import * 2 | EXCHANGE = 'binance' 3 | TRADING_FEE = 0.1 4 | PAY_FEE_BNB = False 5 | API_KEY = '' 6 | API_KEY_SECRET = '' #Please turn on IP whitelist for extra security.. 7 | SYMBOL = 'LUNAUSDT' 8 | DOLLARQUANTITY = 0.4 #set COMPOUND to False if you want to use this. 9 | COMPOUND = True #Set to True if you want to use a percentage of your wallet instead of an absolute number. 10 | COMPOUND_WALLET_PERC = 1 #percentage of USDT in your wallet to use for creating buy orders. 11 | GRIDPERC = 0.25 #Percentage between grids 12 | GRIDS = 3 13 | TG_TOKEN = '' 14 | TG_CHAT_ID = '' 15 | TG_ENABLED = False #current command(s): /balance 16 | AUTO_BUY_BNB = True #automatically buys BNB if there's not enough left in your wallet 17 | SUB_ACCOUNT = "" 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-binance==1.0.12 2 | python-dateutil==2.8.2 3 | python-telegram-bot==13.7 4 | dateparser==1.0.0 5 | schedule==1.1.0 6 | python-dateutil==2.8.2 7 | ujson==4.1.0 8 | ccxt==1.68.53 9 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | from urllib import request 3 | from config import * 4 | import schedule 5 | from time import * 6 | from random import * 7 | import time 8 | import random 9 | from decimal import * 10 | from datetime import datetime 11 | from telegrammodule import main,sendMessage 12 | from binancedata import getBalance, getQuantity,getDecimalAmounts 13 | import threading 14 | import math 15 | import ccxt 16 | 17 | 18 | 19 | subaccount = SUB_ACCOUNT 20 | exchange_id = EXCHANGE 21 | exchange_class = getattr(ccxt, exchange_id) 22 | exchange = exchange_class({ 23 | 'apiKey': API_KEY, 24 | 'secret': API_KEY_SECRET, 25 | 'enableRateLimit': True, 26 | } 27 | ) 28 | 29 | # fetch the BTC/USDT ticker for use in converting assets to price in USDT 30 | ticker = exchange.fetch_ticker(SYMBOL) 31 | 32 | # calculate the ticker price of BTC in terms of USDT by taking the midpoint of the best bid and ask 33 | priceUSDT = Decimal((float(ticker['ask']) + float(ticker['bid'])) / 2) 34 | startTime = time.time() 35 | 36 | 37 | def getCurrentPrice(): 38 | tickerr = exchange.fetch_ticker(SYMBOL) 39 | priceUSDT = Decimal((float(tickerr['ask']) + float(tickerr['bid'])) / 2) 40 | return round(priceUSDT,2) 41 | 42 | 43 | print('Welcome to PyGRID v0.2') 44 | 45 | currentprice = priceUSDT 46 | stepsize = Decimal(Decimal(GRIDPERC)/Decimal(100)*currentprice) 47 | step = 1 48 | isAPIAvailable = False 49 | stepprice = [currentprice-stepsize] 50 | dust = 0 51 | startBalance = Decimal(getBalance()) 52 | yesterdayBalance = Decimal(getBalance()) 53 | totalProfitSinceStartup = 0 54 | 55 | state = True 56 | enoughBalance = True 57 | 58 | buyOrders = {} 59 | buyOrderQuantity = {} 60 | sellOrders = [] 61 | 62 | 63 | 64 | 65 | def truncate(number) -> Decimal: 66 | stepper = Decimal(10.0) ** Decimal(getDecimalAmounts(SYMBOL)) 67 | return Decimal(math.trunc(Decimal(stepper) * Decimal(number)) / Decimal(stepper)) 68 | 69 | def createOrder(type,quantity,price): 70 | currentprice = round(getCurrentPrice(),2) 71 | if type == "buy": 72 | neworder = exchange.createOrder(SYMBOL,"limit","buy",quantity,price) 73 | response = neworder['id'] 74 | saveOrder(response,price,quantity) 75 | if type == "sell": 76 | neworder = exchange.createOrder(SYMBOL,"limit","sell",quantity,price) 77 | response = neworder['id'] 78 | sellOrders.insert(0,response) 79 | buyOrders.pop(max(buyOrders, key=buyOrders.get)) 80 | sendMessage(f"Created {'BUY' if type=='buy' else 'SELL'} order at {price} \nQuantity is {quantity}\nCurrent balance is {getBalance()}\nCurrent price is {currentprice}") 81 | 82 | 83 | 84 | def saveOrder(orderid,price,quantity=0): 85 | buyOrders[orderid] = price 86 | #buyOrderQuantity[orderid] = quantity 87 | 88 | def calculateProfit(cost): 89 | sellTotal = cost 90 | buyTotal = Decimal(cost) / Decimal((1+(GRIDPERC/100))) 91 | 92 | totalprofit = sellTotal - buyTotal 93 | global totalProfitSinceStartup 94 | totalProfitSinceStartup += totalprofit 95 | return round(totalprofit,2) 96 | 97 | def getSellPriceHighestBuyOrder(): 98 | if buyOrders: 99 | highestValue = max(buyOrders.values()) 100 | sellPrice = Decimal(truncate(highestValue * Decimal(((GRIDPERC/100)+1)))) 101 | return round(sellPrice,2) 102 | 103 | 104 | def checkSellOrder(): 105 | if len(sellOrders) > 0: 106 | try: 107 | order = exchange.fetchOrder(sellOrders[0], symbol = SYMBOL, params = {}) 108 | if order['status'] == 'closed': 109 | endTime = time.time() 110 | timeElapsed = endTime - startTime 111 | timeElapsedInHours = timeElapsed / 3600 112 | profitPerHour = Decimal(totalProfitSinceStartup) / Decimal(timeElapsedInHours) 113 | apr = round(((profitPerHour*8760/startBalance)*100),2) 114 | sendMessage(f"Sell order filled\nYour profit in this trade: {calculateProfit(Decimal(order['cost']))}\nYour total profit since bot start-up: {totalProfitSinceStartup}\nRuntime: {round(timeElapsed,2)} seconds\nProfit per hour: {round(profitPerHour,2)}\nAPR: {apr}") 115 | sellOrders.pop(0) 116 | except Exception as e: print(e) 117 | 118 | 119 | 120 | 121 | def balanceChecker(): 122 | try: 123 | currentprice = getCurrentPrice() 124 | balance = getBalance() 125 | buyorder = Decimal(getQuantity())*currentprice 126 | global enoughBalance 127 | if Decimal(balance) < buyorder: 128 | enoughBalance = False 129 | print(f"Insufficient funds to create buy orders") 130 | sendMessage(f"Insufficient funds to create buy orders") 131 | else: 132 | enoughBalance = True 133 | except Exception as e: 134 | print(e) 135 | 136 | 137 | 138 | 139 | while step < GRIDS: 140 | stepprice.append(stepprice[step-1]-stepsize) 141 | step += 1 142 | 143 | round_to_tenths = [truncate(num) for num in stepprice] 144 | 145 | 146 | 147 | def startup(): 148 | #balanceChecker() 149 | if enoughBalance == True: 150 | counter = 0 151 | print(f'Creating buy orders...') 152 | #autoBuyBNB() 153 | while(counter < GRIDS): 154 | createOrder("buy",getQuantity(),round_to_tenths[counter]) 155 | counter += 1 156 | 157 | 158 | 159 | def connectivityCheck(): 160 | global isAPIAvailable 161 | print(f"Checking connectivity to the API...") 162 | try: 163 | status = exchange.fetchStatus(params = {}) 164 | if status['status'] == 'ok': 165 | isAPIAvailable = True 166 | else: isAPIAvailable = False 167 | except Exception as e: 168 | print(f"Error occured with pinging the API server") 169 | isAPIAvailable = False 170 | 171 | # def autoBuyBNB(): 172 | # if(AUTO_BUY_BNB == True): 173 | # sleep(randint(1,5)) 174 | # try: 175 | # balance = client.get_asset_balance(asset='BNB')['free'] 176 | # if Decimal(balance) <= 0.05: 177 | # print("Not enough BNB in wallet, bot will market buy 0.1 BNB") 178 | # order = client.order_market_buy( 179 | # symbol='BNBUSDT', 180 | # quantity=0.1) 181 | # except Exception as e: 182 | # print(e) 183 | 184 | # def setDust(): 185 | # global dust 186 | # dust = Decimal(truncate(client.get_asset_balance(asset='LUNA')['free'])) 187 | 188 | 189 | 190 | startup() 191 | 192 | 193 | 194 | def job(): 195 | try: printLengths() 196 | except: pass 197 | 198 | 199 | now = datetime.now() 200 | dt_string = now.strftime("%d/%m/%Y %H:%M:%S") 201 | connectivityCheck() 202 | if isAPIAvailable == True and state == True: 203 | balanceChecker() 204 | checkSellOrder() 205 | print(f"{dt_string} Checking if orders have been filled...") 206 | try: 207 | if len(buyOrders) != 0: 208 | idnumber = str(max(buyOrders,key=buyOrders.get)) 209 | order = exchange.fetchOrder(idnumber, symbol = SYMBOL, params = {}) 210 | if(order['status'] == 'closed'): 211 | print(f"{dt_string} Order filled, calculating sell price...") 212 | try: 213 | marketStructure = exchange.markets[SYMBOL] 214 | lenstr = 3 215 | createOrder("sell",round(Decimal(order['filled']),lenstr),getSellPriceHighestBuyOrder()) 216 | #setDust() 217 | except Exception as e: 218 | print("Error occured at codeblock creating sell order (1)") 219 | print(e) 220 | except Exception as e: 221 | print("Error occured at codeblock creating sell order (2)") 222 | print(e) 223 | 224 | 225 | 226 | print("Checking if bot can create new buy orders...") 227 | if enoughBalance == True: 228 | if len(buyOrders) < GRIDS and len(buyOrders) != 0: 229 | print(f"Can create new buy order(s)") 230 | try: 231 | createOrder("buy",getQuantity(),truncate(Decimal(min(buyOrders.values()))-stepsize)) 232 | except Exception as e: 233 | print(e) 234 | #print(e) 235 | else: 236 | print(f"Not sufficient funds to create buy orders") 237 | 238 | 239 | print(f'Checking if orders are still inside range') 240 | if 1 > 0: 241 | try: 242 | currentSetPrice = truncate(getCurrentPrice()-stepsize) 243 | maxBuyOrder = Decimal(max(buyOrders.values())) 244 | threshold = (maxBuyOrder)*(1+(Decimal(GRIDPERC)/100)) 245 | if (Decimal(currentSetPrice) > Decimal(max(buyOrders.values()))*(1+(Decimal(GRIDPERC)/100))) and (order['status'] == 'open'): 246 | try: 247 | print(f"{dt_string} Cancelling lowest order and bringing it on top") 248 | sendMessage('Cancelling lowest order and bringing it on top') 249 | orderToPop = min(buyOrders, key=buyOrders.get) 250 | print(str(orderToPop)) 251 | exchange.cancel_order (str(orderToPop),symbol=SYMBOL) 252 | try: 253 | order = exchange.fetchOrder(orderToPop, symbol = SYMBOL, params = {}) 254 | filledQuantity = order['filled'] 255 | if filledQuantity > 0: 256 | if exchange.has['createMarketOrder']: 257 | exchange.createOrder(SYMBOL,'market','sell',filledQuantity,{}) 258 | except Exception as e: 259 | sendMessage(str(e)) 260 | buyOrders.pop(orderToPop) 261 | 262 | buyOrders.pop(orderToPop) 263 | 264 | #adding buy order 265 | createOrder("buy",getQuantity(),Decimal(max(buyOrders.values()))*(1+(Decimal(GRIDPERC)/100))) 266 | except ccxt.ExchangeError as e: 267 | 268 | buyOrders.pop(orderToPop) 269 | 270 | except Exception as e: 271 | pass 272 | 273 | except Exception as e: 274 | print(e) 275 | 276 | def dailyUpdate(): 277 | currentBalance = Decimal(getBalance()) 278 | global yesterdayBalance 279 | sendMessage(f"Your daily update \n Current balance: {currentBalance} \n Balance yesterday: {str(yesterdayBalance)} \n Difference: {str(currentBalance-yesterdayBalance)}") 280 | yesterdayBalance = Decimal(currentBalance) 281 | 282 | def startjob(): 283 | schedule.every(1).seconds.do(job) 284 | schedule.every(1).day.do(dailyUpdate) 285 | 286 | 287 | def printLengths(): 288 | print(f"Length buyOrders is {len(buyOrders)} Length buyOrderQuantity is {len(buyOrderQuantity)}, sellorders is {len(sellOrders)} stepprice is {len(stepprice)} ") 289 | 290 | 291 | 292 | 293 | try: 294 | t2 = threading.Thread(target=startjob()) 295 | t2.start() 296 | t1 = threading.Thread(target=main()) 297 | t1.start() 298 | except Exception as e: 299 | print(f"Something went wrong with threading") 300 | print(e) 301 | 302 | while 1: 303 | schedule.run_pending() 304 | time.sleep(0.1) 305 | 306 | 307 | 308 | 309 | -------------------------------------------------------------------------------- /telegrammodule.py: -------------------------------------------------------------------------------- 1 | from telegram import Update, ForceReply, message 2 | from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackContext 3 | from config import TG_TOKEN,TG_ENABLED,TG_CHAT_ID,GRIDS 4 | import threading 5 | import time 6 | import binancedata 7 | 8 | 9 | 10 | def balance(update: Update, context: CallbackContext) -> None: 11 | """Send a message when the command /balance is issued.""" 12 | user = update.effective_user 13 | update.message.reply_text( 14 | f'Your balance is {binancedata.getBalance()} USDT' 15 | ) 16 | 17 | 18 | def start(update: Update, context: CallbackContext) -> None: 19 | """Send a message when the command /start is issued.""" 20 | user = update.effective_user 21 | update.message.reply_markdown_v2( 22 | fr'Hi {user.mention_markdown_v2()}\!', 23 | reply_markup=ForceReply(selective=True), 24 | ) 25 | 26 | def sendMessage(tekst) -> None: 27 | if TG_ENABLED == True: 28 | updater = Updater(token=TG_TOKEN, use_context=True) 29 | updater.bot.send_message(chat_id=TG_CHAT_ID,text=tekst) 30 | 31 | 32 | 33 | def main() -> None: 34 | """Start the bot.""" 35 | if TG_ENABLED == True: 36 | updater = Updater(token=TG_TOKEN, use_context=True) 37 | dispatcher = updater.dispatcher 38 | dispatcher.add_handler(CommandHandler("start", start)) 39 | dispatcher.add_handler(CommandHandler("balance",balance)) 40 | updater.start_polling() 41 | updater.bot.send_message(chat_id=TG_CHAT_ID,text=f'Bot started succesfully! Bot created {GRIDS} buy orders!') 42 | #updater.idle() #commented out for threading 43 | 44 | 45 | 46 | 47 | 48 | --------------------------------------------------------------------------------