├── .gitignore ├── requirements.txt ├── keywords.yml ├── auth ├── auth.example.yml ├── binance_auth.py └── reddit_auth.py ├── store_order.py ├── config.yml ├── README.md ├── LICENSE ├── trade_client.py └── reddit_crypto_trader.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore creds file 2 | auth/auth.yml 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nltk==3.5 2 | praw==7.3.0 3 | PyYAML==5.4.1 4 | python-binance==1.0.9 -------------------------------------------------------------------------------- /keywords.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Add Keywords like so 3 | # use the coin symbol for the list 4 | BTC: 5 | - bitccoin 6 | - BTC 7 | - btc 8 | - BITCOIN 9 | - Bitcoin 10 | ETH: 11 | - Ethereum 12 | - Eth 13 | - ETH 14 | -------------------------------------------------------------------------------- /auth/auth.example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # CLIENT DETAILS 3 | client_id: "14_CHAR_APP_ID" 4 | client_secret: "27_CHAR_APP_SECRET" 5 | user_agent: "NAME_OF_APP" 6 | password: "REDDIT_PASS" 7 | username: "REDDIT_USERNAME" 8 | binance_api: "BINANCE_API_KEY" 9 | binance_secret: "BINANCE_SECRET" 10 | -------------------------------------------------------------------------------- /auth/binance_auth.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | from binance.client import Client 4 | from binance.exceptions import BinanceAPIException 5 | 6 | 7 | def load_binance_creds(file): 8 | with open(file) as file: 9 | auth = yaml.load(file, Loader=yaml.FullLoader) 10 | 11 | return Client(auth['binance_api'], auth['binance_secret']) 12 | -------------------------------------------------------------------------------- /store_order.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def store_order(file, order): 5 | """ 6 | Save order into local json file 7 | """ 8 | with open(file, 'w') as f: 9 | json.dump(order, f, indent=4) 10 | 11 | def load_order(file): 12 | """ 13 | Update Json file 14 | """ 15 | with open(file, "r+") as f: 16 | return json.load(f) 17 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Subs to analyse 3 | SUBREDDITS: 4 | - Cryptocurrency 5 | - crypto_currency 6 | - cryptocurrencies 7 | - worldnews 8 | # Number of posts to analyse for each sub 9 | NUMBER_OF_POSTS: 10 10 | # Sort by 11 | SORT_BY: hot 12 | TRADE_OPTIONS: 13 | # In your pairing coin 14 | QUANTITY: 15 15 | # BTCUSDT will be bought for example 16 | PAIRING: USDT 17 | # How often to check for posts and run the script 18 | # in minutes 19 | RUN_EVERY: 10 20 | TEST: True 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This is a cryptocurrency trading bot that analyses Reddit sentiment and places trades on Binance based on reddit post and comment sentiment.** 2 | The bot features several customisation options allowing you to target any relevant subreddit as well as a defined set of keywords to look out for. 3 | 4 | In addition to the library requirements you will also need a Binance and a Reddit account. 5 | 6 | For a detailed guide on how to set everything up please see my blog post for the [reddit crypto trading bot](https://www.cryptomaton.org/2021/06/27/cryptocurrency-binance-trading-bot-that-analyses-reddit-sentiment/) 7 | -------------------------------------------------------------------------------- /auth/reddit_auth.py: -------------------------------------------------------------------------------- 1 | import praw 2 | import yaml 3 | 4 | def load_creds(file): 5 | with open(file) as file: 6 | auth = yaml.load(file, Loader=yaml.FullLoader) 7 | 8 | return praw.Reddit( 9 | client_id=auth['client_id'], 10 | client_secret=auth['client_secret'], 11 | user_agent=auth['user_agent'], 12 | password = auth['password'], 13 | username = auth['username'] 14 | ) 15 | def load_config(file): 16 | with open(file) as file: 17 | return yaml.load(file, Loader=yaml.FullLoader) 18 | 19 | def load_keywords(file): 20 | with open(file, 'r',) as f: 21 | return yaml.load(f, Loader=yaml.FullLoader) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andrei 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 | -------------------------------------------------------------------------------- /trade_client.py: -------------------------------------------------------------------------------- 1 | from auth.binance_auth import * 2 | 3 | client = load_binance_creds('auth/auth.yml') 4 | 5 | def get_price(coin): 6 | return client.get_ticker(symbol=coin)['lastPrice'] 7 | 8 | 9 | def convert_volume(coin, quantity, last_price): 10 | """Converts the volume given in QUANTITY from USDT to the each coin's volume""" 11 | 12 | try: 13 | info = client.get_symbol_info(coin) 14 | step_size = info['filters'][2]['stepSize'] 15 | lot_size = {coin:step_size.index('1') - 1} 16 | 17 | if lot_size[coin] < 0: 18 | lot_size[coin] = 0 19 | 20 | except: 21 | pass 22 | 23 | # calculate the volume in coin from QUANTITY in USDT (default) 24 | volume = float(quantity / float(last_price)) 25 | 26 | # define the volume with the correct step size 27 | if coin not in lot_size: 28 | volume = float('{:.1f}'.format(volume)) 29 | 30 | else: 31 | # if lot size has 0 decimal points, make the volume an integer 32 | if lot_size[coin] == 0: 33 | volume = int(volume) 34 | else: 35 | volume = float('{:.{}f}'.format(volume, lot_size[coin])) 36 | 37 | return volume 38 | 39 | 40 | def create_order(coin, quantity): 41 | """ 42 | Creates simple buy order and returns the order 43 | """ 44 | return client.create_order( 45 | symbol = coin, 46 | side = 'BUY', 47 | type = 'MARKET', 48 | quantity = quantity 49 | ) 50 | -------------------------------------------------------------------------------- /reddit_crypto_trader.py: -------------------------------------------------------------------------------- 1 | from auth.reddit_auth import * 2 | from trade_client import * 3 | from store_order import * 4 | 5 | from datetime import datetime, time 6 | import time 7 | 8 | import json 9 | import os.path 10 | 11 | import nltk 12 | nltk.download() 13 | from nltk.sentiment import SentimentIntensityAnalyzer 14 | 15 | reddit = load_creds('auth/auth.yml') 16 | config = load_config('config.yml') 17 | keywords = load_keywords('keywords.yml') 18 | 19 | print(f'logged in as {reddit.user.me()}') 20 | 21 | def get_post(): 22 | """ 23 | Returns relevant posts based the user configuration 24 | """ 25 | posts = {} 26 | for sub in config['SUBREDDITS']: 27 | subreddit = reddit.subreddit(sub) 28 | relevant_posts = getattr(subreddit, config['SORT_BY'])(limit=config['NUMBER_OF_POSTS']) 29 | for post in relevant_posts: 30 | if not post.stickied: 31 | posts[post.id] = {"title": post.title, 32 | "subreddit": sub, 33 | "body": post.selftext, 34 | 35 | } 36 | return posts 37 | 38 | 39 | def store_posts(data): 40 | """ 41 | Stores relevant posts and associated data in a local json file 42 | """ 43 | with open('reddit_posts.json', 'w') as file: 44 | json.dump(data, file) 45 | 46 | 47 | def load_posts(file): 48 | """ 49 | Loads saved reddit posts 50 | """ 51 | with open(file, 'r') as f: 52 | return json.load(f) 53 | 54 | 55 | def compare_posts(fetched, stored): 56 | """ 57 | Checks if there are new posts 58 | """ 59 | i=0 60 | for post in fetched: 61 | if not fetched[post] in [stored[item] for item in stored]: 62 | i+=1 63 | 64 | return i 65 | 66 | 67 | def find_keywords(posts, keywords): 68 | """ 69 | Checks if there are any keywords int he posts we pulled 70 | Bit of a mess but it works 71 | """ 72 | key_posts = {} 73 | 74 | for post in posts: 75 | for key in keywords: 76 | for item in keywords[key]: 77 | if item in posts[post]['title'] or item in posts[post]['body']: 78 | key_posts[post] = posts[post] 79 | key_posts[post]['coin'] = key 80 | 81 | return key_posts 82 | 83 | 84 | def analyse_posts(posts): 85 | """ 86 | analyses the sentiment of each post with a keyword 87 | """ 88 | sia = SentimentIntensityAnalyzer() 89 | sentiment = {} 90 | for post in posts: 91 | if posts[post]['coin'] not in sentiment: 92 | sentiment[posts[post]['coin']] = [] 93 | 94 | sentiment[posts[post]['coin']].append(sia.polarity_scores(posts[post]['title'])) 95 | sentiment[posts[post]['coin']].append(sia.polarity_scores(posts[post]['body'])) 96 | 97 | return sentiment 98 | 99 | 100 | def get_avg_sentiment(sentiment): 101 | """ 102 | Compiles and returnes the average sentiment 103 | of all titles and bodies of our query 104 | """ 105 | average = {} 106 | 107 | for coin in sentiment: 108 | # sum up all compound readings from each title & body associated with the 109 | # coin we detected in keywords 110 | average[coin] = sum([item['compound'] for item in sentiment[coin]]) 111 | 112 | # get the mean compound sentiment if it's not 0 113 | if average[coin] != 0: 114 | average[coin] = average[coin] / len(sentiment[coin]) 115 | 116 | return average 117 | 118 | 119 | def get_price(coin, pairing): 120 | return client.get_ticker(symbol=coin+pairing)['lastPrice'] 121 | 122 | 123 | if __name__ == '__main__': 124 | i = 0 125 | while True: 126 | i +=1 127 | print(f'iteration {i}') 128 | # get the posts from reddit 129 | posts = get_post() 130 | 131 | # check if the order file exists and load the current orders 132 | if os.path.isfile('order.json'): 133 | order = load_order('order.json') 134 | else: 135 | order = {} 136 | 137 | # check if the reddit posts files exist and load them 138 | if os.path.isfile('reddit_posts.json'): 139 | saved_posts = load_posts('reddit_posts.json') 140 | 141 | # this will return the number of new posts we found on reddit 142 | # compared to the ones stored 143 | new_posts = compare_posts(posts, saved_posts) 144 | 145 | if new_posts > 0 or i == 2: 146 | print("New posts detected, fetching new posts...") 147 | 148 | # store the posts if they are new 149 | store_posts(posts) 150 | # find posts with matching keywords 151 | key_posts = find_keywords(posts, keywords) 152 | # determine the sentiment for each post 153 | sentiment = analyse_posts(key_posts) 154 | # return the compoundavg sentiment, grouped by symbol 155 | analyzed_coins = get_avg_sentiment(sentiment) 156 | 157 | print(f'Found matching keywords with the following sentiments: {analyzed_coins}') 158 | 159 | for coin in analyzed_coins: 160 | 161 | # prepare to buy if the sentiment of each coin is greater than 0 162 | # and the coin hasn't been bought already 163 | if analyzed_coins[coin] > 0 and coin not in order: 164 | print(f'{coin} sentiment is positive: {analyzed_coins[coin]}, preparing to buy...') 165 | 166 | price = get_price(coin, config['TRADE_OPTIONS']['PAIRING']) 167 | volume = convert_volume(coin+config['TRADE_OPTIONS']['PAIRING'], config['TRADE_OPTIONS']['QUANTITY'],price) 168 | 169 | try: 170 | # Run a test trade if true 171 | if config['TRADE_OPTIONS']['TEST']: 172 | order[coin] = { 173 | 'symbol':coin+config['TRADE_OPTIONS']['PAIRING'], 174 | 'price':price, 175 | 'volume':volume, 176 | 'time':datetime.timestamp(datetime.now()) 177 | } 178 | 179 | print('PLACING TEST ORDER') 180 | else: 181 | order[coin] = create_order(coin+config['TRADE_OPTIONS']['PAIRING'], volume) 182 | 183 | except Exception as e: 184 | print(e) 185 | 186 | else: 187 | print(f'Order created with {volume} on {coin}') 188 | 189 | store_order('order.json', order) 190 | else: 191 | print(f'Sentiment for {coin} is negative or {coin} is currently in portfolio') 192 | 193 | time.sleep(config['TRADE_OPTIONS']['RUN_EVERY']*60) 194 | else: 195 | print("Running first iteration, fetching posts...") 196 | store_posts(posts) 197 | --------------------------------------------------------------------------------