├── .vscode └── settings.json ├── Strat.png ├── config.py ├── main.py ├── Util.py ├── Strategy.py ├── pines └── simpleema.pine ├── .gitignore ├── README.md └── Bot.py /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "autopep8" 3 | } -------------------------------------------------------------------------------- /Strat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewboyley/crypto-trader/HEAD/Strat.png -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from binance.client import Client 3 | 4 | from dotenv import load_dotenv 5 | load_dotenv() 6 | 7 | 8 | API = os.getenv("API") 9 | SECRET = os.getenv("SECRET") 10 | 11 | markets = ['BTC','ETH','BNB','DOGE','ADA','UNI','LTC'] 12 | tick_interval = Client.KLINE_INTERVAL_30MINUTE 13 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | from config import * 5 | import numpy as np 6 | import pickle 7 | import talib 8 | from binance.client import Client 9 | from config import * 10 | from Bot import Bot 11 | 12 | 13 | print("--- CRYPTO TRADING BOT ---\n") 14 | bot = Bot() 15 | bot.run() 16 | 17 | 18 | # plt.plot(df['Open Time'], df['Close']) 19 | # # plt.plot(df['Open Time'],ema8) 20 | # # plt.plot(df['Open Time'],ema13) 21 | # # plt.plot(df['Open Time'],ema21) 22 | # # plt.plot(df['Open Time'],ema34) 23 | # # plt.plot(df['Open Time'],ema55) 24 | 25 | # longs = df[df['Decision'] == 'Long'] 26 | # exits = df[df['Decision'] == 'Exit Long'] 27 | 28 | 29 | # plt.scatter(longs['Open Time'], longs['Close'], marker='^', c='#00ff00') 30 | # plt.scatter(exits['Open Time'], exits['Close'],marker='v',c='#ff0000') 31 | 32 | # plt.xticks(rotation = 20) 33 | # plt.show() 34 | -------------------------------------------------------------------------------- /Util.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import math 4 | import pickle 5 | 6 | 7 | def binanceToPandas(klines): 8 | klines = np.array(klines) 9 | df = pd.DataFrame(klines.reshape(-1, 12), dtype=float, columns=('Open Time', 10 | 'Open', 11 | 'High', 12 | 'Low', 13 | 'Close', 14 | 'Volume', 15 | 'Close time', 16 | 'Quote asset volume', 17 | 'Number of trades', 18 | 'Taker buy base asset volume', 19 | 'Taker buy quote asset volume', 20 | 'Ignore')) 21 | df['Open Time'] = pd.to_datetime(df['Open Time'], unit='ms') 22 | return df 23 | 24 | 25 | def truncate(number, digits) -> float: 26 | stepper = 10.0 ** digits 27 | return math.trunc(stepper * number) / stepper 28 | 29 | 30 | def savePickle(var, file_name): 31 | outfile = open(file_name, 'wb') 32 | pickle.dump(var, outfile) 33 | 34 | outfile.close() 35 | 36 | 37 | def openPickle(file_name): 38 | outfile = open(file_name, 'rb') 39 | df = pickle.load(outfile) 40 | 41 | outfile.close() 42 | 43 | return df 44 | -------------------------------------------------------------------------------- /Strategy.py: -------------------------------------------------------------------------------- 1 | import talib 2 | 3 | 4 | def strategyDecision(ema8, ema13, ema21, ema34, ema55, rsi, kFast): 5 | ema8 = ema8.iloc[-1] 6 | ema13 = ema13.iloc[-1] 7 | ema21 = ema21.iloc[-1] 8 | ema34 = ema34.iloc[-1] 9 | ema55 = ema55.iloc[-1] 10 | rsi = rsi.iloc[-1] 11 | kFast = kFast.iloc[-1] 12 | 13 | return strategyCalculator(ema8, ema13, ema21, ema34, ema55, rsi, kFast) 14 | 15 | 16 | def strategyCalculator(ema8, ema13, ema21, ema34, ema55, rsi, kFast): 17 | 18 | # MOMENTUM 19 | longEmaCondition = ema8 > ema13 and ema13 > ema21 and ema21 > ema34 and ema34 > ema55 20 | exitLongEmaCondition = ema21 < ema55 21 | 22 | # RSI 23 | longRsiCondition = rsi < 65 24 | exitLongRsiCondition = rsi > 70 25 | 26 | # STOCHASTIC 27 | longStochasticCondition = kFast < 80 28 | exitLongStochasticCondition = kFast > 95 29 | 30 | # STRAT 31 | enterLongCondition = longEmaCondition and longRsiCondition and longStochasticCondition 32 | exitLongCondition = ( 33 | exitLongEmaCondition or exitLongRsiCondition or exitLongStochasticCondition) 34 | 35 | 36 | return (enterLongCondition, exitLongCondition) 37 | 38 | 39 | def calculateIndicators(klines): 40 | ema8 = talib.EMA(klines['Close'], timeperiod=8) 41 | ema13 = talib.EMA(klines['Close'], timeperiod=13) 42 | ema21 = talib.EMA(klines['Close'], timeperiod=21) 43 | ema34 = talib.EMA(klines['Close'], timeperiod=34) 44 | ema55 = talib.EMA(klines['Close'], timeperiod=55) 45 | # ---------- 46 | # OSCILLATORS 47 | # ---------- 48 | 49 | rsi = talib.RSI(klines['Close'], timeperiod=14) 50 | 51 | # ---------- 52 | # STOCHASTIC 53 | # ---------- 54 | 55 | kFast, dFast = talib.STOCHF( 56 | klines['High'], klines['Low'], klines['Close'], fastk_period=14) 57 | 58 | return (ema8, ema13, ema21, ema34, ema55, rsi, kFast) 59 | -------------------------------------------------------------------------------- /pines/simpleema.pine: -------------------------------------------------------------------------------- 1 | //@version=3 2 | strategy(title = "Combined Strategy", default_qty_type = strategy.percent_of_equity, default_qty_value = 100, commission_type=strategy.commission.percent, commission_value = .005, pyramiding = 0, slippage = 3, overlay = true) 3 | 4 | //----------// 5 | // MOMENTUM // 6 | //----------// 7 | ema8 = ema(close, 8) 8 | ema13 = ema(close, 13) 9 | ema21 = ema(close, 21) 10 | ema34 = ema(close, 34) 11 | ema55 = ema(close, 55) 12 | 13 | plot(ema8, color=red, style=line, title="8", linewidth=1) 14 | plot(ema13, color=orange, style=line, title="13", linewidth=1) 15 | plot(ema21, color=yellow, style=line, title="21", linewidth=1) 16 | plot(ema34, color=aqua, style=line, title="34", linewidth=1) 17 | plot(ema55, color=lime, style=line, title="55", linewidth=1) 18 | 19 | longEmaCondition = ema8 > ema13 and ema13 > ema21 and ema21 > ema34 and ema34 > ema55 20 | exitLongEmaCondition = ema13 < ema55 21 | 22 | shortEmaCondition = ema8 < ema13 and ema13 < ema21 and ema21 < ema34 and ema34 < ema55 23 | exitShortEmaCondition = ema13 > ema55 24 | 25 | // ---------- // 26 | // OSCILLATORS // 27 | // ----------- // 28 | rsi = rsi(close, 14) 29 | longRsiCondition = rsi < 70 and rsi > 40 30 | exitLongRsiCondition = rsi > 70 31 | 32 | shortRsiCondition = rsi > 30 and rsi < 60 33 | exitShortRsiCondition = rsi < 30 34 | 35 | // Stochastic 36 | length = 14, smoothK = 3, smoothD = 3 37 | kFast = stoch(close, high, low, 14) 38 | dSlow = sma(kFast, smoothD) 39 | 40 | longStochasticCondition = kFast < 80 41 | exitLongStochasticCondition = kFast > 95 42 | 43 | shortStochasticCondition = kFast > 20 44 | exitShortStochasticCondition = kFast < 5 45 | 46 | //----------// 47 | // STRATEGY // 48 | //----------// 49 | 50 | longCondition = longEmaCondition and longRsiCondition and longStochasticCondition and strategy.position_size == 0 51 | exitLongCondition = (exitLongEmaCondition or exitLongRsiCondition or exitLongStochasticCondition) and strategy.position_size > 0 52 | 53 | if (longCondition) 54 | strategy.entry("LONG", strategy.long) 55 | if (exitLongCondition) 56 | strategy.close("LONG") 57 | 58 | // shortCondition = shortEmaCondition and shortRsiCondition and shortStochasticCondition and strategy.position_size == 0 59 | // exitShortCondition = (exitShortEmaCondition or exitShortRsiCondition or exitShortStochasticCondition) and strategy.position_size < 0 60 | 61 | // if (shortCondition) 62 | // strategy.entry("SHORT", strategy.short) 63 | // if (exitShortCondition) 64 | // strategy.close("SHORT") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pickle 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/vscode,python 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=vscode,python 5 | 6 | ### Python ### 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | pytestdebug.log 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | doc/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | pythonenv* 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | 140 | # pytype static type analyzer 141 | .pytype/ 142 | 143 | # profiling data 144 | .prof 145 | 146 | ### vscode ### 147 | .vscode/* 148 | !.vscode/settings.json 149 | !.vscode/tasks.json 150 | !.vscode/launch.json 151 | !.vscode/extensions.json 152 | *.code-workspace 153 | 154 | # End of https://www.toptal.com/developers/gitignore/api/vscode,python -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptocurrency Trader 2 | 3 | [![forthebadge](https://forthebadge.com/images/badges/works-on-my-machine.svg)](https://forthebadge.com) 4 | [![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) 5 | 6 | This is a fairly simple cryptocurrency trading bot that is meant to be left running and will make you a profit over time. It has about a 65% win rate which means that it may make some losing trades but will still be profitable in the long run. This project is still *EXTREMELY* buggy and so I urge you to use this with care until I can polish it up a bit. 7 | 8 | ![Trading View Strategy](Strat.png) 9 | 10 | ## Installation 11 | 12 | You need to have a Binance spot trading account to use this project (I tried my best to avoid the image validation thing, try use 2FA). 13 | 14 | 1. Generate a Binance API and SECRET key 15 | 16 | **WRITE THESE DOWN IN A SAFE PLACE**. The secret key will not be visible if you come back to the page a second time 17 | 18 | 2. Pull this repository to your local machine (or wherever you want to run the bot) 19 | 20 | ``` 21 | git clone https://github.com/andrewboyley/crypto-trader.git 22 | cd crypto-trader 23 | ``` 24 | 25 | 3. Create a file called `.env` 26 | 27 | This is where you will store you keys. Make sure that this file is secure so that nobody can see it. Create two variables and insert your keys like this: 28 | 29 | ``` 30 | API=YOUR-API-KEY-HERE 31 | SECRET=YOUR-SECRET-KEY-HERE 32 | ``` 33 | 34 | 4. Install dependencies 35 | 36 | - [ ] Write a script and requirements.txt file to do this 37 | 38 | I know, I need to improve this. For now, make sure you do the following. 39 | 40 | 1. Install TA-Lib for your OS 41 | 42 | Follow this instructions on the [TA-Lib website](https://www.ta-lib.org/) to install TA-Lib locally 43 | 44 | 2. Install Python dependencies 45 | 46 | Install the following with a `pip install` command (requirements.txt coming soon) 47 | 48 | - numpy 49 | - matplotlib 50 | - TA-Lib 51 | - python-binance 52 | - python-dotenv 53 | - pandas 54 | 5. Customize your configuration 55 | 56 | Edit the `config.py` to your liking. You can modify the list of coins that the bot will monitor and change the time interval for the klines (although I have found that 30 minutes works best) 57 | 58 | 6. Start the bot 59 | 60 | - [ ] TODO: Make this into a Linux service so that it runs on boot 61 | 62 | ``` 63 | python3 main.py 64 | ``` 65 | 7. Invest into DOGE 66 | 67 | :gem: :hand: :rocket: :new_moon: 68 | 69 | ## Strategy 70 | 71 | The bot has a relatively simple strategy that I have blatently stolen from a Trading View users Pine Script. I highly recommend that you check it out on [Trading View](https://www.tradingview.com/script/QoNKoXwW-Simple-profitable-trading-strategy/). Every 30 seconds, the bot will fetch the klines from Binance and compute the following: 72 | 73 | - Exponential Moving Averages (8, 13, 21, 34 and 55) 74 | - Relative Strength Index 14 75 | - Stochastic 76 | 77 | It will enter long if *all* of the following is true: 78 | 79 | - Each EMA signal being larger than the next (ema8 > ema13, ema13 > ema21 etc) 80 | - RSI < 65 81 | - Stochastic < 80 82 | 83 | It will exit this trade if *any* of the following is true: 84 | 85 | - RSI > 70 86 | - EMA21 < EMA55 87 | - Stochastic > 95 88 | -------------------------------------------------------------------------------- /Bot.py: -------------------------------------------------------------------------------- 1 | from config import API, SECRET, markets, tick_interval 2 | from binance.client import Client 3 | import pandas as pd 4 | import numpy as np 5 | from Util import * 6 | from time import sleep 7 | from Strategy import calculateIndicators, strategyDecision 8 | import math 9 | 10 | 11 | class Bot: 12 | def __init__(self): 13 | print("- Initializing Bot...") 14 | self.client = Client(API, SECRET) 15 | print("- Loaded API keys") 16 | 17 | self.usdt = 0 18 | self.position_val = 0 19 | self.balance = [] 20 | self.bought = {} 21 | self.ticks = {} 22 | self.available_currencies = [] 23 | self.refreshBalance() 24 | print('- Done fetching balance') 25 | self.generateBoughtStatus() 26 | self.generateTicks() 27 | print(f'- Balance: {self.usdt:.2f} USDT') 28 | 29 | def run(self): 30 | print("- Bot is running") 31 | print('\n--------TRADES-------\n') 32 | while True: 33 | try: 34 | for symbol in markets: 35 | symbol = symbol + 'USDT' 36 | klines = self.getKlines(symbol) 37 | 38 | # self.buy(symbol, klines) 39 | # sleep(5) 40 | # klines = self.getKlines(symbol) 41 | # self.sell(symbol, klines) 42 | # return 43 | 44 | ema8, ema13, ema21, ema34, ema55, rsi, kFast = calculateIndicators( 45 | klines) 46 | 47 | enterLong, exitLong = strategyDecision( 48 | ema8, ema13, ema21, ema34, ema55, rsi, kFast) 49 | 50 | 51 | 52 | if self.bought[symbol]: 53 | if exitLong: 54 | self.sell(symbol, klines) 55 | else: 56 | if enterLong: 57 | self.buy(symbol, klines) 58 | 59 | sleep(30) 60 | except Exception as ex: 61 | print(ex) 62 | sleep(10) 63 | 64 | 65 | def generateBoughtStatus(self): 66 | print('- Generating bought/sold statuses...') 67 | for coin in markets: 68 | coin += 'USDT' 69 | symbol_orders = self.client.get_all_orders(symbol=coin, limit=1) 70 | 71 | if len(symbol_orders) > 0 and symbol_orders[0]['side'] == 'BUY' and symbol_orders[0]['status'] == 'FILLED': 72 | print(f'- {coin} is currently holding long') 73 | self.bought[coin] = symbol_orders[0] 74 | else: 75 | self.bought[coin] = None 76 | 77 | def buy(self, symbol, df): 78 | self.refreshBalance() 79 | 80 | if self.usdt > 10: 81 | price = df["Close"][len(df) - 1] 82 | amount = self.usdt/price 83 | 84 | amount = truncate(amount, self.ticks[symbol]) 85 | 86 | print(f'Buying {amount} {symbol} @ {price} USDT...') 87 | 88 | buy_market = self.client.order_market_buy(symbol=symbol, quoteOrderQty=self.usdt) 89 | 90 | self.bought[symbol] = buy_market 91 | 92 | ## TESTING 93 | #buy_market = self.client.create_test_order( 94 | # symbol=symbol, side='BUY', type='MARKET', quoteOrderQty=self.usdt) 95 | 96 | #self.bought[symbol] = { 97 | # 'type': 'BUY', 98 | # 'cummulativeQuoteQty': self.usdt, 99 | # 'executedQty': amount 100 | #} 101 | ### 102 | 103 | else: 104 | if not any(self.bought.values()): 105 | print(f"{symbol} | Not enough USDT to trade (minimum of $10)") 106 | 107 | def sell(self, symbol, df): 108 | self.refreshBalance() 109 | 110 | symbol_balance = 0 111 | for s in self.balance: 112 | if s['asset'] + 'USDT' == symbol: 113 | symbol_balance = s['free'] 114 | 115 | # TESTING 116 | #symbol_balance = self.bought[symbol]['executedQty'] 117 | ### 118 | 119 | price = df["Close"][len(df) - 1] 120 | 121 | if symbol_balance * price > 10: 122 | 123 | amount = truncate(symbol_balance, self.ticks[symbol]) 124 | 125 | print(f'Selling {amount} {symbol} @ {price} USDT...') 126 | sell_market = self.client.order_market_sell(symbol=symbol, quantity=amount) 127 | 128 | # TESTING 129 | #sell_market = self.client.create_test_order( 130 | # symbol=symbol, side='SELL', type='MARKET', quantity=amount) 131 | 132 | #sell_market = { 133 | # 'cummulativeQuoteQty': amount * price 134 | #} 135 | ### 136 | 137 | net = float(sell_market['cummulativeQuoteQty']) - float(self.bought[symbol]['cummulativeQuoteQty']) 138 | print(f'Net profit: {net} USDT\n') 139 | 140 | self.bought[symbol] = None 141 | 142 | else: 143 | if not any(self.bought.values()): 144 | print(f"Not enough {symbol} to trade (minimum of $10)") 145 | 146 | def generateTicks(self): 147 | print('- Generating symbol step sizes...') 148 | try: 149 | ticks = openPickle('Ticks.pickle') 150 | print('- Loading ticks from file') 151 | self.ticks = ticks 152 | except Exception as ex: 153 | print(ex) 154 | for coin in markets: 155 | coin += 'USDT' 156 | self.getSymbolPrecision(coin) 157 | savePickle(self.ticks, 'Ticks.pickle') 158 | 159 | def getSymbolPrecision(self, symbol): 160 | for filt in self.client.get_symbol_info(symbol=symbol)['filters']: 161 | if filt['filterType'] == 'LOT_SIZE': 162 | diff = filt['stepSize'].find('1') - filt['stepSize'].find('.') 163 | self.ticks[symbol] = max(diff, 0) 164 | break 165 | 166 | def getKlines(self, symbol): 167 | raw_klines = self.client.get_klines( 168 | symbol=symbol, interval=tick_interval) 169 | return binanceToPandas(raw_klines) 170 | 171 | def refreshBalance(self): 172 | balance = self.client.get_account()["balances"] 173 | if balance is not None: 174 | self.available_currencies = [] 175 | self.balance = [] 176 | for dict in balance: 177 | dict["free"] = float(dict["free"]) 178 | dict["locked"] = float(dict["locked"]) 179 | 180 | if dict['asset'] == 'USDT': 181 | self.usdt = float(dict["free"]) 182 | elif (dict["free"] > 0.0): 183 | dict["asset"] = dict["asset"] 184 | self.available_currencies.append(dict["asset"]) 185 | self.balance.append(dict) 186 | --------------------------------------------------------------------------------