├── config.py ├── helper.py ├── README.md ├── market.py ├── patterns_trade.py ├── gather_data.py └── candle.py /config.py: -------------------------------------------------------------------------------- 1 | apiKey = '' 2 | apiSecret = '' 3 | 4 | 5 | pairsToTrade = ['ETHUSDT'] -------------------------------------------------------------------------------- /helper.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def stream_kline_to_struct_kline(bar): 4 | klineList = [float(bar['t']), float(bar['o']), float(bar['h']), float(bar['l']), float(bar['c']), float(bar['v'])] 5 | return klineList 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # patternstrade 2 | Analyzes crypto candles over a set time period and then trades based on winning patterns found. 3 | 4 | Heavily customizable. 5 | 6 | Warning: This was not a viable strategy and should not be used without heavy modification if you want to actually use this seriously. 7 | -------------------------------------------------------------------------------- /market.py: -------------------------------------------------------------------------------- 1 | from binance.client import Client 2 | from binance import exceptions 3 | import time 4 | import config 5 | 6 | makerFee = .0008 7 | takerFee = .0008 8 | 9 | 10 | class Market: 11 | def __init__(self): 12 | self.client = Client(api_key=config.apiKey, api_secret=config.apiSecret, tld='us') 13 | 14 | def buy_coin_possible(self, symbol, percentOfEquity): 15 | if len(self.client.get_open_orders()): 16 | print("Order already open") 17 | return False 18 | 19 | symbol = symbol.upper() 20 | 21 | # Get equity amount of our USD 22 | accountValue = float(self.client.get_asset_balance("USDT")["free"]) 23 | if accountValue < 10: 24 | print("Account value error:", accountValue, "USDT") 25 | return False 26 | if accountValue < 500: 27 | print("You lost too much money today", accountValue, "USDT") 28 | return False 29 | 30 | # calculate how much of the symbol that is 31 | moneyToSpend = percentOfEquity * accountValue / 100.0 32 | symbolQuantity = moneyToSpend / float(self.client.get_symbol_ticker(symbol=symbol)["price"]) 33 | 34 | # set the limit buy so we get it at the price we want hopefully 35 | order = None 36 | try: 37 | order = self.client.order_market_buy( 38 | symbol=symbol, 39 | quantity="{:0.0{}f}".format(symbolQuantity, 3), 40 | ) 41 | except exceptions.BinanceAPIException as e: 42 | print(e) 43 | return False 44 | 45 | # wait 2 seconds. usually small orders will go through immediately but if this scales it wouldn't 46 | time.sleep(.5) 47 | 48 | # see if it went through at our price, otherwise cancel it 49 | if order is not None: 50 | for openOrder in self.client.get_open_orders(): 51 | if order["orderId"] == openOrder["orderId"]: 52 | self.client.cancel_order(symbol=symbol, orderId=order["orderId"]) 53 | print("Could not fill", order) 54 | return False 55 | 56 | # calculate the takeprofit and stoploss price 57 | actualBuyPrice = float(order['fills'][0]['price']) 58 | actualBuyQuantity = float(order['fills'][0]['qty']) * .98 59 | endProfitPrice = actualBuyPrice + actualBuyPrice * .0030 60 | stopLossPrice = actualBuyPrice - actualBuyPrice * .0060 61 | try: 62 | order = self.client.order_oco_sell( 63 | symbol=symbol, 64 | quantity="{:0.0{}f}".format(actualBuyQuantity, 3), 65 | price="{:0.0{}f}".format(endProfitPrice, 2), 66 | stopPrice="{:0.0{}f}".format(stopLossPrice + .01, 2), 67 | stopLimitPrice="{:0.0{}f}".format(stopLossPrice, 2), 68 | stopLimitTimeInForce='GTC' 69 | ) 70 | except exceptions.BinanceAPIException as e: 71 | print(e) 72 | return False 73 | 74 | return True 75 | 76 | 77 | def get_symbol_price(self, symbol): 78 | recentTrades = self.client.get_recent_trades(symbol=symbol) 79 | lastTrade = {} 80 | 81 | for trade in recentTrades: 82 | if lastTrade == {} or int(trade["time"]) > int(lastTrade["time"]): 83 | lastTrade = trade 84 | 85 | return float(lastTrade["price"]) 86 | -------------------------------------------------------------------------------- /patterns_trade.py: -------------------------------------------------------------------------------- 1 | import config 2 | import websocket 3 | import json 4 | import time 5 | import gather_data 6 | import candle 7 | import market 8 | import helper 9 | 10 | mk = market.Market() 11 | 12 | print("Trends to look for") 13 | winningTrends = gather_data.get_winning_trends('ETHUSDT') 14 | print("Number of winning trends:", len(winningTrends)) 15 | print('********************************') 16 | print('********************************') 17 | 18 | barMap = {} # map of symbols->list of list of candles 19 | for pairToAdd in config.pairsToTrade: 20 | barMap[pairToAdd.lower()] = [] 21 | 22 | 23 | def process_bars_for_trade(symbol): 24 | if symbol not in barMap.keys(): 25 | return False 26 | if len(barMap[symbol]) == 3: 27 | candleList = (candle.Candle(), candle.Candle()) 28 | 29 | candleList[0].candleStick = barMap[symbol][1] 30 | candleList[0].leadingCandleStick = barMap[symbol][0] 31 | candleList[0].determine_classification() 32 | candleList[0].determine_direction() 33 | candleList[0].determine_volume_change() 34 | 35 | candleList[1].candleStick = barMap[symbol][2] 36 | candleList[1].leadingCandleStick = barMap[symbol][1] 37 | candleList[1].determine_classification() 38 | candleList[1].determine_direction() 39 | candleList[1].determine_volume_change() 40 | 41 | for c in candleList: 42 | print(c.classification, c.direction) 43 | 44 | if candleList in winningTrends: 45 | print('WINNER') 46 | return mk.buy_coin_possible(symbol.upper(), 5) 47 | print('**************************************************') 48 | 49 | return False 50 | 51 | 52 | def on_message(ws, msg): 53 | jsonMsg = json.loads(msg) 54 | 55 | if 'e' in jsonMsg.keys() and jsonMsg['e'] == 'kline': 56 | ticker = str(jsonMsg['s']).lower() 57 | if ticker in barMap.keys(): 58 | newBar = False 59 | 60 | if len(barMap[ticker]) == 0: 61 | barMap[ticker].append(helper.stream_kline_to_struct_kline(jsonMsg['k'])) 62 | return 63 | elif barMap[ticker][-1][candle.TIMESTAMP_INDEX] == jsonMsg['k']['t']: 64 | barMap[ticker][-1] = helper.stream_kline_to_struct_kline(jsonMsg['k']) 65 | elif barMap[ticker][-1][candle.TIMESTAMP_INDEX] != jsonMsg['k']['t']: 66 | barMap[ticker].append(helper.stream_kline_to_struct_kline(jsonMsg['k'])) 67 | newBar = True 68 | 69 | # only keep 3 bars in storage because that's all we need 70 | if len(barMap[ticker]) > 3: 71 | barMap[ticker].pop(0) 72 | 73 | # only look on 3 bars 74 | if len(barMap[ticker]) != 3: 75 | return 76 | 77 | # check for patterns if we don't have a new bar and it's in the last 7 seconds of the 1m bar 78 | if not newBar and int(time.time() * 1000) - int(barMap[ticker][-1][candle.TIMESTAMP_INDEX]) > 50000: 79 | tradeMade = process_bars_for_trade(ticker) 80 | 81 | # if a trade was made, reset everything and cool down for a bit and let our buffers fill up again 82 | if tradeMade: 83 | barMap[ticker] = [] 84 | else: 85 | print(msg) 86 | 87 | 88 | print('Starting stream') 89 | streamString = f'wss://stream.binance.com:9443/ws' 90 | for pairToStream in config.pairsToTrade: 91 | streamString += f'/{(pairToStream.lower())}@kline_1m' 92 | 93 | ws = websocket.WebSocketApp(streamString, on_message=on_message) 94 | ws.run_forever() 95 | -------------------------------------------------------------------------------- /gather_data.py: -------------------------------------------------------------------------------- 1 | from binance.client import Client 2 | from binance import enums 3 | import config 4 | import candle 5 | 6 | 7 | def get_winning_trends(symbol): 8 | client = Client(config.apiKey, config.apiSecret, tld='us') 9 | 10 | winningTrends = {} # Stores the determined trends from first pass, list of lists of candles (length of numOfCandlesToLookFor) 11 | totalTrends = {} # Stores total trends amount, winning and losing 12 | 13 | # Get Historical Klines 14 | oneMinData = client.get_historical_klines(symbol=symbol, interval=enums.KLINE_INTERVAL_1MINUTE, start_str='1609542363000', 15 | klines_type=enums.HistoricalKlinesType.SPOT) 16 | 17 | # Look for areas where price increases in a way we want it to 18 | for x in range(len(oneMinData)): 19 | if x < 10: 20 | continue 21 | if x == len(oneMinData) - 10: 22 | break 23 | 24 | barsToCheck = [oneMinData[x], oneMinData[x + 1], oneMinData[x + 2], oneMinData[x + 3], oneMinData[x + 4], oneMinData[x + 5], 25 | oneMinData[x + 6], oneMinData[x + 7], oneMinData[x + 8], oneMinData[x + 9], oneMinData[x + 10]] 26 | buyPrice = float(barsToCheck[0][candle.CLOSE_INDEX]) 27 | takeProfitPrice = buyPrice + buyPrice * .0030 28 | stopLossPrice = buyPrice - buyPrice * .0060 29 | takeProfitHit = False 30 | stopLossHit = False 31 | for z in range(len(barsToCheck)): 32 | if z == 0: 33 | continue 34 | if float(barsToCheck[z][candle.LOW_INDEX]) <= stopLossPrice: 35 | stopLossHit = True 36 | break 37 | if float(barsToCheck[z][candle.HIGH_INDEX]) >= takeProfitPrice: 38 | takeProfitHit = True 39 | break 40 | 41 | # If no target was hit, then make it a loss 42 | if not takeProfitHit and not stopLossHit: 43 | stopLossHit = True 44 | 45 | # Add to the list of candles 46 | if takeProfitHit: 47 | candleList = [candle.Candle(), candle.Candle()] 48 | 49 | candleList[0].candleStick = oneMinData[x-1] 50 | candleList[0].leadingCandleStick = oneMinData[x-2] 51 | candleList[0].determine_classification() 52 | candleList[0].determine_direction() 53 | candleList[0].determine_volume_change() 54 | 55 | candleList[1].candleStick = oneMinData[x] 56 | candleList[1].leadingCandleStick = oneMinData[x-1] 57 | candleList[1].determine_classification() 58 | candleList[1].determine_direction() 59 | candleList[1].determine_volume_change() 60 | 61 | # if candleList[0].classification == candle.Classification.unclassified or \ 62 | # candleList[1].classification == candle.Classification.unclassified or \ 63 | # continue 64 | 65 | if tuple(candleList) in winningTrends.keys(): 66 | winningTrends[tuple(candleList)] += 1 67 | else: 68 | winningTrends[tuple(candleList)] = 1 69 | 70 | filteredWinningTrends = [] 71 | print(len(winningTrends.keys())) 72 | 73 | for pt in winningTrends.keys(): 74 | totalTrends[tuple(pt)] = 0 75 | 76 | # calculate total trends for all bars 77 | for y in range(len(oneMinData)): 78 | if y + 2 >= len(oneMinData): 79 | break 80 | 81 | tempBarList = [candle.Candle(), candle.Candle()] 82 | tempBarList[0].candleStick = oneMinData[y] 83 | tempBarList[0].leadingCandleStick = oneMinData[y - 1] 84 | tempBarList[0].determine_classification() 85 | tempBarList[0].determine_direction() 86 | tempBarList[0].determine_volume_change() 87 | 88 | tempBarList[1].candleStick = oneMinData[y + 1] 89 | tempBarList[1].leadingCandleStick = oneMinData[y] 90 | tempBarList[1].determine_classification() 91 | tempBarList[1].determine_direction() 92 | tempBarList[1].determine_volume_change() 93 | 94 | if tuple(tempBarList) in totalTrends.keys(): 95 | totalTrends[tuple(tempBarList)] += 1 96 | 97 | # compare and return winners 98 | for trend in winningTrends.keys(): 99 | numWinningOccurrences = winningTrends[trend] 100 | numTotalOccurrences = totalTrends[trend] 101 | print(numWinningOccurrences, numTotalOccurrences) 102 | 103 | if float(numWinningOccurrences) >= (float(numTotalOccurrences) * .50): 104 | # for t in trend: 105 | # print(t.classification, t.direction, t.volumeChange) 106 | # print('**************************************************************************') 107 | filteredWinningTrends.append(trend) 108 | 109 | return filteredWinningTrends 110 | -------------------------------------------------------------------------------- /candle.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | # For indexing in candlesticks 4 | TIMESTAMP_INDEX = 0 5 | OPEN_INDEX = 1 6 | HIGH_INDEX = 2 7 | LOW_INDEX = 3 8 | CLOSE_INDEX = 4 9 | VOLUME_INDEX = 5 10 | 11 | 12 | # Models for types of candlesticks 13 | class Classification(enum.Enum): 14 | unclassified = 0 15 | big = 1 16 | small = 2 17 | doji = 3 18 | dragonfly_doji = 4 19 | gravestone_doji = 5 20 | hanging_man = 6 21 | hammer = 7 22 | inverted_hammer = 8 23 | spinning_top = 9 24 | shaven_head = 10 25 | shaven_bottom = 11 26 | marubozu = 12 27 | 28 | 29 | # Defined by open price -> close price. stagnant is less than .02% change 30 | class Direction(enum.Enum): 31 | bullish = 0 32 | bearish = 1 33 | stagnant = 2 34 | 35 | 36 | # Change in volume from previous bar. stagnant is less than 2% change 37 | class DeltaVolume(enum.Enum): 38 | increasing = 0 39 | decreasing = 1 40 | stagnant = 2 41 | 42 | 43 | # Combines all classifications into one class 44 | class Candle: 45 | def __init__(self): 46 | self.leadingCandleStick = {} 47 | self.candleStick = {} 48 | self.classification = Classification.unclassified 49 | self.direction = Direction.stagnant 50 | self.volumeChange = DeltaVolume.stagnant 51 | 52 | def __eq__(self, other): 53 | # Do not compare candlesticks 54 | return self.classification == other.classification 55 | 56 | def __hash__(self): 57 | return hash(self.classification) 58 | 59 | def determine_classification(self): 60 | highSolid = float(self.candleStick[CLOSE_INDEX]) 61 | lowSolid = float(self.candleStick[OPEN_INDEX]) 62 | if float(self.candleStick[OPEN_INDEX]) > float(self.candleStick[CLOSE_INDEX]): 63 | highSolid = float(self.candleStick[OPEN_INDEX]) 64 | lowSolid = float(self.candleStick[CLOSE_INDEX]) 65 | 66 | openCloseSize = abs(float(self.candleStick[OPEN_INDEX]) - float(self.candleStick[CLOSE_INDEX])) 67 | highLowSize = abs(float(self.candleStick[HIGH_INDEX]) - float(self.candleStick[LOW_INDEX])) 68 | topWickSize = abs(float(self.candleStick[HIGH_INDEX]) - highSolid) 69 | bottomWickSize = abs(lowSolid - float(self.candleStick[LOW_INDEX])) 70 | 71 | # Make sure bar is substantial 72 | if highLowSize < highSolid * .004: 73 | self.classification = Classification.small 74 | 75 | # Big 76 | if topWickSize > 0 and bottomWickSize > 0: 77 | if topWickSize < openCloseSize * .15 and bottomWickSize < openCloseSize * .15: 78 | self.classification = Classification.big 79 | return 80 | 81 | # Dojis 82 | if openCloseSize < highLowSize * .15: 83 | if topWickSize > openCloseSize * 5 and bottomWickSize > openCloseSize * 5: 84 | self.classification = Classification.doji 85 | return 86 | if bottomWickSize > highLowSize * .70: 87 | self.classification = Classification.dragonfly_doji 88 | return 89 | if topWickSize > highLowSize * .70: 90 | self.classification = Classification.gravestone_doji 91 | return 92 | 93 | # Hanging Man 94 | if topWickSize == 0 and bottomWickSize > 0: 95 | if openCloseSize <= highLowSize * .40: 96 | if openCloseSize >= highLowSize * .20: 97 | self.classification = Classification.hanging_man 98 | return 99 | 100 | # Hammer 101 | if topWickSize < highLowSize * .1: 102 | if openCloseSize <= highLowSize * .40: 103 | self.classification = Classification.hammer 104 | return 105 | 106 | # Inverted Hammer 107 | if bottomWickSize < highLowSize * .1: 108 | if openCloseSize <= highLowSize * .40: 109 | self.classification = Classification.hammer 110 | return 111 | 112 | # Spinning Top 113 | if topWickSize > highLowSize * .15: 114 | if bottomWickSize > highLowSize * .15: 115 | self.classification = Classification.spinning_top 116 | return 117 | 118 | # Shaven Head 119 | if topWickSize == 0: 120 | if highLowSize * .70 > openCloseSize > highLowSize * .30: 121 | self.classification = Classification.shaven_head 122 | return 123 | 124 | # Shaven Bottom 125 | if bottomWickSize == 0: 126 | if highLowSize * .70 > openCloseSize > highLowSize * .30: 127 | self.classification = Classification.shaven_bottom 128 | return 129 | 130 | # Marubozu 131 | if bottomWickSize == 0 and topWickSize == 0: 132 | self.classification = Classification.marubozu 133 | return 134 | 135 | self.classification = Classification.unclassified 136 | return 137 | 138 | def determine_direction(self): 139 | if (abs(float(self.candleStick[OPEN_INDEX]) - float(self.candleStick[CLOSE_INDEX])) / float(self.candleStick[OPEN_INDEX])) <= .0002: 140 | self.direction = Direction.stagnant 141 | elif float(self.candleStick[OPEN_INDEX]) > float(self.candleStick[CLOSE_INDEX]): 142 | self.direction = Direction.bearish 143 | else: 144 | self.direction = Direction.bullish 145 | 146 | def determine_volume_change(self): 147 | if float(self.candleStick[VOLUME_INDEX]) < .01: 148 | self.volumeChange = DeltaVolume.decreasing 149 | elif (abs(float(self.candleStick[VOLUME_INDEX]) - float(self.leadingCandleStick[VOLUME_INDEX])) / float(self.candleStick[VOLUME_INDEX])) <= .05: 150 | self.volumeChange = DeltaVolume.stagnant 151 | elif float(self.candleStick[VOLUME_INDEX]) > float(self.leadingCandleStick[VOLUME_INDEX]): 152 | self.volumeChange = DeltaVolume.increasing 153 | else: 154 | self.volumeChange = DeltaVolume.decreasing 155 | 156 | --------------------------------------------------------------------------------