├── .gitignore ├── analysis ├── __init__.py └── chart.py ├── signals.db ├── screenshots ├── settings.jpg ├── positions.png ├── mainscreen_top.jpg └── mainscreen_bottom.jpg ├── templates ├── home.html ├── signals.html ├── settings.html ├── wavetrade.html ├── index.html └── quicktrade.html ├── requirements.txt ├── static ├── package.json ├── src │ └── tailwind_src.css ├── js │ ├── waves.js │ └── index.js └── package-lock.json ├── main.py ├── thread_manager.py ├── README.md ├── heiken_ashi.py ├── utils.py ├── signal_data.py ├── app.py ├── signals_loop.py ├── charts.py ├── algo_trader.py ├── async_signals.py ├── signals.py ├── coin_data.py ├── trader.py └── async_algo_trader.py /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | -------------------------------------------------------------------------------- /analysis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /signals.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/signals.db -------------------------------------------------------------------------------- /screenshots/settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/screenshots/settings.jpg -------------------------------------------------------------------------------- /screenshots/positions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/screenshots/positions.png -------------------------------------------------------------------------------- /screenshots/mainscreen_top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/screenshots/mainscreen_top.jpg -------------------------------------------------------------------------------- /screenshots/mainscreen_bottom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nihilok/perpSniper/HEAD/screenshots/mainscreen_bottom.jpg -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0 2 | gunicorn==20.0.4 3 | matplotlib==3.3.4 4 | mplfinance==0.12.7a5 5 | pandas==1.2.2 6 | pandas_ta==0.2.45b 7 | stdiomask==0.0.6 8 | numpy==1.20.1 9 | scipy==1.6.0 10 | Twisted==20.3.0 11 | APScheduler==3.7.0 12 | python-binance==0.7.9 13 | urllib3==1.26.3 14 | requests==2.25.1 15 | aiohttp~=3.7.4 -------------------------------------------------------------------------------- /static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perp_sniper", 3 | "version": "0.1.1", 4 | "description": "trading tool", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tailwind build src/tailwind_src.css -o css/tailwind.css" 8 | }, 9 | "author": "TokenBlack", 10 | "license": "ISC", 11 | "dependencies": { 12 | "autoprefixer": "^10.2.4", 13 | "tailwindcss": "^2.0.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /static/src/tailwind_src.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | 7 | *::-webkit-scrollbar { 8 | width: 12px; /* width of the entire scrollbar */ 9 | } 10 | 11 | *::-webkit-scrollbar-track { 12 | background: black; /* color of the tracking area */ 13 | } 14 | 15 | *::-webkit-scrollbar-thumb { 16 | background-color: gray; /* color of the scroll thumb */ 17 | border-radius: 20px; /* roundness of the scroll thumb */ 18 | border: 3px solid black; /* creates padding around scroll thumb */ 19 | } 20 | 21 | .flex-row-center { 22 | @apply flex justify-center items-center; 23 | } 24 | 25 | .flex-col-center { 26 | @apply flex flex-col justify-center items-center; 27 | } 28 | 29 | .waveBubble { 30 | @apply flex-col-center rounded-full border border-black w-20 h-20 31 | } 32 | 33 | .button { 34 | @apply flex-row-center rounded bg-gray-500 hover:bg-gray-600 text-white cursor-pointer no-underline px-3 py-1; 35 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import atexit 3 | import multiprocessing 4 | 5 | import gunicorn.app.base 6 | from app import app, start_signals, tear_down 7 | from trader import Trader 8 | 9 | 10 | atexit.register(tear_down) 11 | 12 | 13 | def number_of_workers(): 14 | return multiprocessing.cpu_count() 15 | 16 | 17 | class StandaloneApplication(gunicorn.app.base.BaseApplication): 18 | 19 | def __init__(self, app, options=None): 20 | self.options = options or {} 21 | self.application = app 22 | super().__init__() 23 | 24 | def load_config(self): 25 | config = {key: value for key, value in self.options.items() 26 | if key in self.cfg.settings and value is not None} 27 | for key, value in config.items(): 28 | self.cfg.set(key.lower(), value) 29 | 30 | def load(self): 31 | return self.application 32 | 33 | 34 | if __name__ == '__main__': 35 | tr_setup = Trader() 36 | del tr_setup 37 | options = { 38 | 'bind': '%s:%s' % ('0.0.0.0', '8080'), 39 | 'workers': number_of_workers(), 40 | 'worker_class': 'gthread', 41 | 'on_starting': start_signals(), 42 | } 43 | StandaloneApplication(app, options).run() 44 | -------------------------------------------------------------------------------- /templates/signals.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | {% block content %} 3 |
4 |
5 |
6 |

Hot Symbols:

7 |
8 | 9 | 14 |
15 |
16 |

Recent Signals

17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for s in signals %} 28 | 29 | 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 |
TimeSymbolAlert
{{s[0]}}{{s[1]}}{{s[2]}}
36 |
37 |
38 |
39 | 44 | {% endblock %} -------------------------------------------------------------------------------- /thread_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from threading import Thread 4 | 5 | from coin_data import CoinData 6 | from signals_loop import MainLoop, scheduler 7 | from trader import Trader 8 | 9 | 10 | class ThreadManager: 11 | 12 | def __init__(self): 13 | 14 | """Construct all required instances and start threads""" 15 | 16 | self.trader = Trader() 17 | self.data = CoinData() 18 | self.signals = MainLoop(self.data) 19 | self.threads = [] 20 | 21 | self.start_thread(self.jobs) 22 | print('Signals background tasks added') 23 | self.start_thread(self.save_data_thread) 24 | print('Data thread running') 25 | 26 | def save_data_thread(self): 27 | try: 28 | while True: 29 | time.sleep(2) 30 | self.data.save_latest_data() 31 | except KeyboardInterrupt as e: 32 | self.teardown() 33 | os.remove('symbols.db') 34 | raise e 35 | 36 | def teardown(self): 37 | scheduler.remove_all_jobs() 38 | self.stop_threads() 39 | scheduler.shutdown() 40 | self.signals.teardown() 41 | self.data.bsm_tear_down() 42 | 43 | def jobs(self): 44 | jobs = [(self.trader.check_positions_cancel_open_orders, 'interval', 60)] 45 | self.signals.start_jobs(jobs=jobs) 46 | 47 | def start_thread(self, func): 48 | t = Thread(target=func) 49 | t.setDaemon(True) 50 | t.start() 51 | self.threads.append(t) 52 | 53 | def stop_threads(self): 54 | self.data.bsm_tear_down() 55 | for thread in self.threads: 56 | thread.join() 57 | print('Threads stopped') -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # perpSniper v0.2 2 | ### Tools/signals for Binance cryptocurrency derivatives (futures) trading 3 | ##### Flask app with standalone gunicorn server (setup_script.py) 4 | 5 | ## Features: 6 | - automatic stop-loss and trailing take-profit orders set when entering a position 7 | - automatic quantity (percentage of balance) set before opening a position 8 | - live charts (2-second update time) showing entry, take-profit and stop-loss lines as well as RSI, MACD and 50/200 EMA for 1m, 15m, 1h & 4h timeframes 9 | - signal alerts for volume, macd, rsi overbought/sold, rsi divergence, and ema crosses for 15m, 1h & 4h timeframes 10 | 11 | ## Installation: 12 | You will need your Binance api keys. It is recommended to enable IP restriction when setting up keys unless you trust me not to have included a secret script to steal your API keys (I haven't.... or have I??? I haven't but just use the restriction anyway)! 13 | 14 | Clone the repository, cd into the directory and create a virtual environment, 15 | e.g. `python3 -m venv venv` 16 | 17 | Then activate the venv and start the server with: 18 | ``` 19 | source venv/bin/activate 20 | pip install -r requirements.txt 21 | python main.py 22 | ``` 23 | 24 | Use the same final 3 commands to run the server each time and then log on via your browser to the local IP of the server or localhost with pot 8080 (e.g 192.168.0.10:8080 for different PC or smartphone or 127.0.0.1:8080 (localhost) for same PC as server is running from.) 25 | 26 | The server takes a while to start as it has to download the recent symbol data before starting. Also, currently, there is an issue with the background tasks not stopping correctly, after pressing ctrl+c you may need to kill the python3.8 process with ctrl+z. This will be fixed soon. 27 | 28 | ## Screenshots: 29 | ![Main Screen](screenshots/mainscreen_top.jpg) 30 | ![Main Screen](screenshots/mainscreen_bottom.jpg) 31 | ![Settings Screen](screenshots/settings.jpg) 32 | ![Open Positions](screenshots/positions.png) -------------------------------------------------------------------------------- /static/js/waves.js: -------------------------------------------------------------------------------- 1 | var xDown = null; 2 | var yDown = null; 3 | 4 | function getTouches(evt) { 5 | return evt.touches || // browser API 6 | evt.originalEvent.touches; // jQuery 7 | } 8 | 9 | function handleTouchStart(evt) { 10 | const firstTouch = getTouches(evt)[0]; 11 | xDown = firstTouch.clientX; 12 | yDown = firstTouch.clientY; 13 | }; 14 | 15 | function handleTouchMove(evt) { 16 | if ( ! xDown || ! yDown ) { 17 | return; 18 | } 19 | 20 | var xUp = evt.touches[0].clientX; 21 | var yUp = evt.touches[0].clientY; 22 | 23 | var xDiff = xDown - xUp; 24 | var yDiff = yDown - yUp; 25 | 26 | var bubble_id = this.id 27 | 28 | if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) { 29 | if ( xDiff > 0 ) { 30 | $(this).css({ 31 | 'animation': 'slideSwipeDrawerOpen .2s linear', 32 | '-webkit-transform' : 'translateX(' + -6 + 'rem)', 33 | '-moz-transform' : 'translateX(' + -6 + 'rem)', 34 | '-ms-transform' : 'translateX(' + -6 + 'rem)', 35 | '-o-transform' : 'translateX(' + -6 + 'rem)', 36 | 'transform' : 'translateX(' + -6 + 'rem)' 37 | }); 38 | 39 | } else { 40 | $(this).css({ 41 | 'animation': 'slideSwipeDrawerClose .2s linear', 42 | '-webkit-transform' : 'translateX(' + 0 + 'rem)', 43 | '-moz-transform' : 'translateX(' + 0 + 'rem)', 44 | '-ms-transform' : 'translateX(' + 0 + 'rem)', 45 | '-o-transform' : 'translateX(' + 0 + 'rem)', 46 | 'transform' : 'translateX(' + 0 + 'rem)' 47 | }); 48 | } 49 | } 50 | /* reset values */ 51 | xDown = null; 52 | yDown = null; 53 | }; 54 | 55 | function addBubble(colour) { 56 | var bubbleDiv = document.getElementById('waveBubbleParent') 57 | var newBubble = document.createElement('div') 58 | newBubble.classList.add('waveBubble', `bg-${colour}-500`, `hover:bg-${colour}-600`, 'flex-none') 59 | bubbleDiv.appendChild(newBubble) 60 | console.log('done') 61 | } -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | {% block content %} 3 |
4 |
5 | 6 | 7 | 10 | 14 | 15 | 16 | 19 | 23 | 24 | 25 | 28 | 32 | 33 | 34 | 37 | 41 | 42 |
8 | 9 | 11 | 13 |
17 | 18 | 20 | 22 |
26 | 27 | 29 | 31 |
35 | 36 | 38 | 40 |
43 |
44 |
46 | BACK 47 |
48 |
50 | SAVE 51 |
52 |
53 |
54 |
55 | 64 | {% endblock %} -------------------------------------------------------------------------------- /templates/wavetrade.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 8 | 9 | 10 | 89 | 90 | 91 |
92 |
93 | 94 |
95 |
96 |
Add Bubble
97 |
98 |
99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 8 | 9 | 10 |
11 | 12 |
13 | {% if messages %} 14 | {% for message in messages %} 15 |
{{message}} 17 |
18 | {% endfor %} 19 | {% endif %} 20 |
21 | 22 |
23 |
24 |
25 |

Balance: $468.47

26 |
27 |
28 |

16:24:01

29 |
30 |
31 |

PNL: $0.00

32 |
33 |
34 |

PerpSniper v0.2

35 | 36 | 37 |
38 |
39 |

Open Positions:

40 |
41 |
42 | CLOSE ALL 43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
PAIRDIRECTIONQUANTITYPNLROE
58 |
59 |
60 |
61 | {% block content %} 62 |
63 |
65 | Signals 66 |
67 |
69 | Settings 70 |
71 |
73 | Shutdown 74 |
75 |
76 | {% endblock %} 77 |
78 |
79 | 83 | 84 | 94 | 95 | -------------------------------------------------------------------------------- /heiken_ashi.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | from threading import Thread 4 | 5 | from coin_data import CoinData 6 | from trader import Trader, PerpetualTrade 7 | 8 | from signals import Signals 9 | from utils import get_popular_coins 10 | 11 | 12 | class HeikenAshiPerpetualTrader: 13 | 14 | def __init__(self, symbol): 15 | self.tf = '1m' 16 | self.tr = Trader() 17 | self.tr.settings['qty'] = 0.005 18 | self.tr.settings['sl'] = 0.05 19 | self.tr.check_positions_cancel_open_orders() 20 | quantity, approx_price, info = self.tr.calculate_max_qty(symbol) 21 | HA_trend = Signals.get_heiken_ashi_trend(Signals.get_heiken_ashi(CoinData.get_dataframe(symbol, self.tf))) 22 | self.pt = PerpetualTrade(symbol, HA_trend, quantity, 23 | self.tr.settings['tp'], self.tr.settings['sl'], 24 | self.tr.settings['db'], info, self.tr) 25 | print(f'INITIAL TRADE: Going {"LONG" if HA_trend is True else "SHORT" if HA_trend is False else "FLAT"} on {symbol}') 26 | 27 | def subsequent_trades(self, symbol): 28 | HA_trend = Signals.get_heiken_ashi_trend(Signals.get_heiken_ashi(CoinData.get_dataframe(symbol, self.tf))) 29 | # print(symbol, HA_trend) 30 | try: 31 | open_positions = [(position['symbol'], position['direction']) for position in 32 | self.tr.return_open_positions()] 33 | if symbol in {op[0] for op in open_positions}: 34 | log = '' 35 | if HA_trend is True: 36 | if (symbol, 'SHORT') in open_positions: 37 | log = f'Going {"LONG" if HA_trend is True else "SHORT" if HA_trend is False else "FLAT"} on {symbol}' 38 | self.pt.long() 39 | else: 40 | pass 41 | elif HA_trend is False: 42 | if (symbol, 'LONG') in open_positions: 43 | log = f'Going {"LONG" if HA_trend is True else "SHORT" if HA_trend is False else "FLAT"} on {symbol}' 44 | self.pt.short() 45 | else: 46 | log = f'Going {"LONG" if HA_trend is True else "SHORT" if HA_trend is False else "FLAT"} on {symbol}' 47 | self.pt.flat() 48 | if log: 49 | print(log) 50 | else: 51 | quantity, approx_price, info = self.tr.calculate_max_qty(symbol) 52 | if HA_trend is not None: 53 | log = f'REOPENED: Going {"LONG" if HA_trend is True else "SHORT" if HA_trend is False else "FLAT"} on {symbol}' 54 | self.pt = PerpetualTrade(symbol, HA_trend, quantity, 55 | self.tr.settings['tp'], self.tr.settings['sl'], 56 | self.tr.settings['db'], info, self.tr) 57 | print(log) 58 | except Exception as e: 59 | print(e) 60 | 61 | 62 | def mainloop(coin_data_instance): 63 | ha_trades = {} 64 | current_symbols = coin_data_instance.most_volatile_symbols 65 | for symbol, volatility in current_symbols: 66 | ha_trades[symbol] = HeikenAshiPerpetualTrader(symbol) 67 | while True: 68 | for symbol, volatility in current_symbols: 69 | ha_trades[symbol].subsequent_trades(symbol) 70 | now = datetime.now() 71 | if now.minute == 59 and 0 < now.second <= 1: 72 | print('reassessing most volatile coins') 73 | coin_data_instance.most_volatile_symbols = coin_data_instance.return_most_volatile() 74 | for symbol in ha_trades.keys(): 75 | if symbol not in coin_data_instance.most_volatile_symbols: 76 | ha_trades[symbol].tr.close_position(symbol) 77 | 78 | 79 | def save_data(coin_data_instance): 80 | while True: 81 | coin_data_instance.save_latest_data() 82 | time.sleep(5) 83 | 84 | 85 | if __name__ == '__main__': 86 | c = CoinData() 87 | t = Thread(target=save_data, args=(c,)) 88 | t.setDaemon(True) 89 | t.start() 90 | time.sleep(10) 91 | mainloop(c) -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import Thread 3 | 4 | from binance.websockets import BinanceSocketManager 5 | from twisted.internet import reactor 6 | 7 | import trader as tr 8 | 9 | t = tr.Trader() 10 | bsm = BinanceSocketManager(t.client) 11 | 12 | 13 | def return_open_positions(): 14 | positions = t.client.futures_position_information() 15 | position_list = [] 16 | for position in positions: 17 | if float(position['positionAmt']) > 0: 18 | roe = (float(position['unRealizedProfit']) / ( 19 | (float(position['positionAmt']) * float(position['markPrice'])) / int( 20 | position['leverage']))) * 100 21 | direction = 'LONG' 22 | elif float(position['positionAmt']) < 0: 23 | roe = -(float(position['unRealizedProfit']) / ( 24 | (float(position['positionAmt']) * float(position['markPrice'])) / int( 25 | position['leverage']))) * 100 26 | direction = 'SHORT' 27 | else: 28 | continue 29 | position = { 30 | 'symbol': position['symbol'], 31 | 'qty': position['positionAmt'], 32 | 'pnl': float(position['unRealizedProfit']), 33 | 'roe': roe, 34 | 'direction': direction, 35 | } 36 | position_list.append(position) 37 | return position_list 38 | 39 | 40 | def open_positions(): 41 | positions = t.client.futures_position_information() 42 | messages = [] 43 | for position in positions: 44 | if float(position['positionAmt']) > 0: 45 | pnl = (float(position['unRealizedProfit'])/((float(position['positionAmt'])*float(position['markPrice']))/int(position['leverage'])))*100 46 | direction = 'LONG' 47 | elif float(position['positionAmt']) < 0: 48 | pnl = -(float(position['unRealizedProfit'])/((float(position['positionAmt'])*float(position['markPrice']))/int(position['leverage'])))*100 49 | direction = 'SHORT' 50 | else: 51 | continue 52 | message = f'''symbol: {position['symbol']} 53 | positionAmt: {position['positionAmt']} {direction} ({position['leverage']}x) 54 | entryPrice: {str(position['entryPrice'])[:10]} 55 | markPrice: {str(position['markPrice'])[:10]} 56 | unRealizedProfit: {str(position['unRealizedProfit'])[:5]} USDT ({str(pnl)[:5]}% ROE) 57 | ''' 58 | messages.append(message) 59 | return messages 60 | 61 | 62 | local_tickers = {} 63 | 64 | check_tickers = [] 65 | 66 | 67 | def live_vol_data(tickers): 68 | futures_symbols = t.client.futures_exchange_info() 69 | futures_symbols = [symbol['symbol'] for symbol in futures_symbols['symbols']] 70 | futures_symbols = set(futures_symbols) 71 | for symbol in tickers: 72 | if symbol['e'] == '24hrTicker': 73 | if symbol['s'][-4:] == 'USDT' and symbol['s'] in futures_symbols: 74 | local_tickers[symbol['s']] = symbol['q'] 75 | if len(check_tickers) >= 88: 76 | check_tickers.pop(0) 77 | check_tickers.append(local_tickers) 78 | else: 79 | check_tickers.append(local_tickers) 80 | 81 | 82 | def ticker_websocket_loop(): 83 | data = bsm.start_ticker_socket(live_vol_data) 84 | try: 85 | bsm.start() 86 | while len(local_tickers) < 91: 87 | time.sleep(1) 88 | finally: 89 | bsm.stop_socket(data) 90 | 91 | def get_popular_coins(): 92 | ticker_websocket_loop() 93 | tickers = local_tickers 94 | sorted_tickers_list = [ticker[0] for ticker in sorted(tickers.items(), key=lambda ticker: float(ticker[1]))] 95 | print('found ' + str(len(sorted_tickers_list)) + ' symbols') 96 | return list(reversed(sorted_tickers_list)) 97 | 98 | 99 | if __name__ == '__main__': 100 | t = Thread(target=ticker_websocket_loop) 101 | t.setDaemon(True) 102 | t.start() 103 | while not len(check_tickers): 104 | time.sleep(1) 105 | print(get_popular_coins()) 106 | 107 | # ex = {'symbol': 'ETHBTC', 'status': 'TRADING', 'baseAsset': 'ETH', 'baseAssetPrecision': 8, 'quoteAsset': 'BTC', 108 | # 'quotePrecision': 8, 'quoteAssetPrecision': 8, 'baseCommissionPrecision': 8, 'quoteCommissionPrecision': 8, 109 | # 'orderTypes': ['LIMIT', 'LIMIT_MAKER', 'MARKET', 'STOP_LOSS_LIMIT', 'TAKE_PROFIT_LIMIT'], 'icebergAllowed': True, 110 | # 'ocoAllowed': True, 'quoteOrderQtyMarketAllowed': True, 'isSpotTradingAllowed': True, 111 | # 'isMarginTradingAllowed': True, 'filters': [ 112 | # {'filterType': 'PRICE_FILTER', 'minPrice': '0.00000100', 'maxPrice': '100000.00000000', 113 | # 'tickSize': '0.00000100'}, 114 | # {'filterType': 'PERCENT_PRICE', 'multiplierUp': '5', 'multiplierDown': '0.2', 'avgPriceMins': 5}, 115 | # {'filterType': 'LOT_SIZE', 'minQty': '0.00100000', 'maxQty': '100000.00000000', 'stepSize': '0.00100000'}, 116 | # {'filterType': 'MIN_NOTIONAL', 'minNotional': '0.00010000', 'applyToMarket': True, 'avgPriceMins': 5}, 117 | # {'filterType': 'ICEBERG_PARTS', 'limit': 10}, 118 | # {'filterType': 'MARKET_LOT_SIZE', 'minQty': '0.00000000', 'maxQty': '2729.13846842', 'stepSize': '0.00000000'}, 119 | # {'filterType': 'MAX_NUM_ORDERS', 'maxNumOrders': 200}, 120 | # {'filterType': 'MAX_NUM_ALGO_ORDERS', 'maxNumAlgoOrders': 5}], 'permissions': ['SPOT', 'MARGIN']} 121 | -------------------------------------------------------------------------------- /signal_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import sys 4 | from datetime import datetime 5 | 6 | import aiohttp 7 | import nest_asyncio 8 | import sqlite3 9 | import pandas as pd 10 | import pandas_ta as ta 11 | import numpy as np 12 | import scipy.signal 13 | from binance.websockets import BinanceSocketManager 14 | from twisted.internet import reactor 15 | 16 | from trader import Trader 17 | from utils import get_popular_coins 18 | nest_asyncio.apply() 19 | 20 | file_path = os.path.abspath(os.path.dirname(__file__)) 21 | os.chdir(file_path) 22 | 23 | client = Trader().client 24 | loop = asyncio.get_event_loop() 25 | 26 | TIME_FRAMES = ['15m', '1h', '4h'] 27 | 28 | 29 | class SymbolData: 30 | """Start websocket for live klines and get historical klines that don't exist""" 31 | 32 | 33 | class SignalData: 34 | 35 | @classmethod 36 | async def return_dataframes(cls, symbol, event_loop): 37 | """Get complete dataframes for given symbol with signals data for all given timeframes 38 | Main method for initial data preparation""" 39 | return await cls.add_ta_data(await cls.get_original_data(symbol), event_loop) 40 | # return await cls.get_original_data(symbol) 41 | 42 | @classmethod 43 | async def get_original_data(cls, symbol): 44 | """Load price data from the database 45 | This has to happen first""" 46 | conn = sqlite3.connect('symbols.db') 47 | dfs = [] 48 | for tf in TIME_FRAMES: 49 | query = f'SELECT * from {symbol}_{tf}' 50 | df = pd.read_sql_query(query, conn) 51 | dfs.append(df) 52 | conn.close() 53 | return dfs 54 | 55 | @classmethod 56 | async def add_ta_data(cls, dfs, event_loop): 57 | """Create columns for RSI, MACD(m/s/h), EMAs(20/50/200), Heiken Ashi 58 | These can all be run as coroutine tasks""" 59 | coroutines = [[],[],[]] 60 | for i, df in enumerate(dfs): 61 | coroutines[i].append(cls.get_rsi(df)) 62 | coroutines[i].append(cls.get_macd(df)) 63 | coroutines[i].append(cls.get_emas(df)) 64 | coroutines[i].append(cls.get_heiken_ashi(df)) 65 | # run coroutines with event loop and get return VALUES 66 | dfs_15 = event_loop.run_until_complete(asyncio.gather(*coroutines[0])) 67 | dfs_1h = event_loop.run_until_complete(asyncio.gather(*coroutines[1])) 68 | dfs_4h = event_loop.run_until_complete(asyncio.gather(*coroutines[2])) 69 | comp_dfs = [pd.concat([dfs_15[i] for i in range(len(dfs_15))], axis=1), 70 | pd.concat([dfs_1h[i] for i in range(len(dfs_1h))], axis=1), 71 | pd.concat([dfs_4h[i] for i in range(len(dfs_4h))], axis=1)] 72 | # return return values 73 | return comp_dfs 74 | 75 | @classmethod 76 | async def get_rsi(cls, df): 77 | return ta.rsi(df.close, 14) 78 | 79 | @classmethod 80 | async def get_macd(cls, df): 81 | return ta.macd(df.close, 12, 26, 9) 82 | 83 | @classmethod 84 | async def get_emas(cls, df): 85 | if len(df) >= 20: 86 | df['ema_20'] = ta.ema(df.close, 20) 87 | else: 88 | df['ema_20'], df['ema_50'] = ta.ema(df.close, len(df.close) - 3), ta.ema(df.close, len(df.close) - 3) 89 | if len(df) >= 50: 90 | df['ema_20'], df['ema_50'] = ta.ema(df.close, len(df.close) - 3), ta.ema(df.close, len(df.close) - 3) 91 | else: 92 | df['ema_20'] = ta.ema(df.close, 20) 93 | df['ema_50'] = ta.ema(df.close, len(df.close) - 3) 94 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3) 95 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50) 96 | if len(df) >= 200: 97 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50) 98 | df['ema_200'] = ta.ema(df.close, 200) 99 | else: 100 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3) 101 | df = df.tail(88) 102 | return df[['ema_20', 'ema_50', 'ema_200']] 103 | 104 | @classmethod 105 | async def get_heiken_ashi(cls, df): 106 | df['HA_Close'] = (df['open'] + df['high'] + df['low'] + df['close']) / 4 107 | idx = df.index.name 108 | df.reset_index(inplace=True) 109 | 110 | for i in range(0, len(df)): 111 | if i == 0: 112 | df.at[i, 'HA_Open'] = ((df._get_value(i, 'open') + df._get_value(i, 'close')) / 2) 113 | else: 114 | df.at[i, 'HA_Open'] = ((df._get_value(i - 1, 'HA_Open') + df._get_value(i - 1, 'HA_Close')) / 2) 115 | 116 | if idx: 117 | df.set_index(idx, inplace=True) 118 | 119 | df['HA_High'] = df[['HA_Open', 'HA_Close', 'high']].max(axis=1) 120 | df['HA_Low'] = df[['HA_Open', 'HA_Close', 'low']].min(axis=1) 121 | 122 | return df[['HA_Open', 'HA_High', 'HA_Low', 'HA_Close']] 123 | 124 | @classmethod 125 | async def check_df(cls, df, event_loop): 126 | coroutines = [cls.check_rsi(df), cls.check_macd(df), cls.check_heiken_ashi(df)] 127 | results = event_loop.run_until_complete(asyncio.gather(*coroutines)) 128 | return results 129 | 130 | @classmethod 131 | async def check_rsi(cls, df): 132 | # await asyncio.sleep(2) 133 | # print('checked rsi') 134 | return 'RSI' 135 | 136 | @classmethod 137 | async def check_macd(cls, df): 138 | # await asyncio.sleep(1) 139 | # print('checked macd') 140 | return False, True 141 | 142 | @classmethod 143 | async def check_heiken_ashi(cls, df): 144 | # await asyncio.sleep(2) 145 | # print('checked ha') 146 | return 'HA' 147 | 148 | @classmethod 149 | async def main(cls, symbol, event_loop): 150 | # start_time = datetime.now() 151 | dfs = await cls.return_dataframes(symbol, event_loop) 152 | coroutines = [cls.check_df(dfs[i], event_loop) for i in range(len(dfs))] 153 | results = event_loop.run_until_complete(asyncio.gather(*coroutines)) 154 | # print(f'took: ' + str(datetime.now() - start_time)) 155 | return results 156 | 157 | 158 | if __name__ == '__main__': 159 | try: 160 | conn = sqlite3.connect('symbols.db') 161 | curs = conn.cursor() 162 | tabs = {tab[0] for tab in curs.execute("select name from sqlite_master where type = 'table'").fetchall()} 163 | conn.close() 164 | coroutines = [] 165 | start_time = datetime.now() 166 | for s in get_popular_coins(): 167 | if s not in {'XEMUSDT', '1INCHUSDT'}: 168 | coroutines.append(SignalData.main(s, loop)) 169 | data = loop.run_until_complete(asyncio.gather(*coroutines)) 170 | loop.close() 171 | print(data) 172 | except IndexError as e: 173 | print(e) 174 | finally: 175 | print(f'took: ' + str(datetime.now() - start_time)) 176 | 177 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os 4 | from threading import Thread 5 | import logging 6 | from flask import Flask, redirect, url_for, render_template, request, session, jsonify, make_response 7 | import sqlite3 8 | 9 | import trader 10 | from charts import Charts 11 | from thread_manager import ThreadManager 12 | 13 | file_path = os.path.abspath(os.path.dirname(__file__)) 14 | os.chdir(file_path) 15 | 16 | logger = logging.getLogger(__name__) 17 | logging.basicConfig(filename='log.log') 18 | 19 | app = Flask('PerpSniper') 20 | tr = None 21 | thread_manager = None 22 | 23 | 24 | def start_signals(*args): 25 | global tr 26 | global thread_manager 27 | try: 28 | with open('log.log', 'r') as f: 29 | lines = f.readlines() 30 | if len(lines) >= 88: 31 | with open('log.log', 'w') as f: 32 | f.writelines(lines[-88:]) 33 | except FileNotFoundError: 34 | print('No log file to flush') 35 | thread_manager = ThreadManager() 36 | tr = thread_manager.trader 37 | 38 | 39 | def tear_down(): 40 | thread_manager.teardown() 41 | print('app teardown completed') 42 | 43 | 44 | @app.route('/') 45 | def quick_trade(): 46 | return render_template('quicktrade.html', title='PerpSniper v0.2') 47 | 48 | @app.route('/old') 49 | def home(): 50 | return render_template('index.html', title='PerpSniper v0.2') 51 | 52 | 53 | @app.route('/api/signals', methods=['GET']) 54 | def signals_api(): 55 | conn = sqlite3.connect('signals.db') 56 | c = conn.cursor() 57 | qs = c.execute('SELECT * FROM signals') 58 | recent_sigs = list(reversed([alert for alert in qs])) 59 | conn.close() 60 | data = { 61 | 'signals': [], 62 | } 63 | for sig in recent_sigs: 64 | sig_dict = { 65 | 'time': sig[0], 66 | 'symbol': sig[1], 67 | 'alert': sig[2], 68 | } 69 | data['signals'].append(sig_dict) 70 | response = make_response(jsonify(data)) 71 | return response 72 | 73 | 74 | def long_thread(coin): 75 | t = tr.trade(coin, True) 76 | 77 | 78 | @app.route('/api/long', methods=['POST']) 79 | def long_api(): 80 | try: 81 | coin = request.get_json()['coin'] 82 | t = Thread(target=long_thread, args=(coin,)) 83 | t.setDaemon(True) 84 | t.start() 85 | message = f'Opened long on {coin}' 86 | data = {'message': message} 87 | response = make_response(jsonify(data)) 88 | except KeyError: 89 | return 'Failure', 500 90 | return response, 200 91 | 92 | 93 | def short_thread(coin): 94 | t = tr.trade(coin, False) 95 | 96 | 97 | @app.route('/api/short', methods=['POST']) 98 | def short_api(): 99 | try: 100 | coin = request.get_json()['coin'] 101 | t = Thread(target=short_thread, args=(coin,)) 102 | t.setDaemon(True) 103 | t.start() 104 | message = f'Opened short on {coin}' 105 | data = {'message': message} 106 | response = make_response(jsonify(data)) 107 | except Exception as e: 108 | return e, 500 109 | return response, 200 110 | 111 | 112 | @app.route('/wave') 113 | def wave_trade(): 114 | return render_template('wavetrade.html', title='PerpSniper WaveTrade') 115 | 116 | 117 | @app.route('/api/positions') 118 | def positions_api(): 119 | positions = tr.return_open_positions() 120 | data = {'positions':positions} 121 | response = make_response(jsonify(data)) 122 | return response, 200 123 | 124 | 125 | @app.route('/api/account') 126 | def account_api(): 127 | account = tr.get_account_info() 128 | response = make_response(jsonify(account)) 129 | return response, 200 130 | 131 | 132 | @app.route('/settings', methods=['GET', 'POST']) 133 | def settings(): 134 | global tr 135 | if request.method == 'GET': 136 | with open('settings.json', 'r') as f: 137 | settings_dict = json.load(f) 138 | return render_template('settings.html', settings=settings_dict) 139 | else: 140 | tp = float(request.form['TP'])/100 141 | sl = float(request.form['SL'])/100 142 | db = float(request.form['DB']) 143 | qty = float(request.form['QTY'])/100 144 | with open('settings.json', 'r') as f: 145 | settings_dict = json.load(f) 146 | if tp: 147 | settings_dict['tp'] = tp 148 | if sl: 149 | settings_dict['sl'] = sl 150 | if db: 151 | settings_dict['db'] = db 152 | if qty: 153 | settings_dict['qty'] = qty 154 | with open('settings.json', 'w') as f: 155 | json.dump(settings_dict, f, indent=4) 156 | if any({tp, sl, db, qty}): 157 | tr = trader.Trader() 158 | return redirect(url_for('quick_trade')) 159 | 160 | 161 | def shutdown_server(): 162 | func = request.environ.get('werkzeug.server.shutdown') 163 | if func is None: 164 | raise RuntimeError('Not running with the Werkzeug Server') 165 | func() 166 | 167 | @app.route('/shutdown', methods=['POST']) 168 | def shutdown(): 169 | shutdown_server() 170 | return 'Server shutting down...' 171 | 172 | 173 | @app.route('/api/close_old_orders') 174 | def close_old_orders(): 175 | tr.check_positions_cancel_open_orders() 176 | return 'Success', 200 177 | 178 | 179 | @app.route('/api/close_position', methods=['POST']) 180 | def close_position(): 181 | try: 182 | coin = request.get_json()['coin'] 183 | tr.close_position(coin) 184 | return 'Success', 200 185 | except Exception as e: 186 | print(e) 187 | return f'Failure: {e}', 500 188 | 189 | 190 | @app.route('/api/close_all_positions', methods=['POST']) 191 | def close_all_positions(): 192 | try: 193 | tr.close_all_positions() 194 | response = make_response(jsonify({'message': 'Positions closed'})) 195 | return response, 200 196 | except Exception as e: 197 | print(e) 198 | return f'Failure: {e}', 500 199 | 200 | 201 | @app.route('/plot', methods=['POST']) 202 | def build_plot(): 203 | try: 204 | req_json = request.get_json() 205 | symbol = req_json['symbol'] 206 | tf = req_json['interval'] 207 | positions = tr.return_open_positions() 208 | entry = None 209 | direction = None 210 | if positions: 211 | symbols = [p['symbol'] for p in positions] 212 | if symbol in symbols: 213 | entry = positions[symbols.index(symbol)]['entry'] 214 | direction = positions[symbols.index(symbol)]['direction'] 215 | plot_url = Charts(symbol, tf, entry, direction).main_chart() 216 | response = make_response(jsonify({'plot_url': ''.format(plot_url), 217 | 'base64': 'data:image/png;base64,{}'.format(plot_url)})) 218 | return response, 200 219 | # return 'Success', 200 220 | except Exception as error: 221 | print(error) 222 | return 'Failure', 500 223 | 224 | @app.route('/server_time') 225 | def server_time(): 226 | time = tr.return_server_time() 227 | response = make_response(jsonify({'serverTime': datetime.datetime.fromtimestamp(time).strftime('%H:%M:%S')})) 228 | return response, 200 229 | 230 | 231 | 232 | if __name__ == '__main__': 233 | start_signals() 234 | app.run(host='0.0.0.0', port=9000, use_reloader=False, debug=True) 235 | -------------------------------------------------------------------------------- /templates/quicktrade.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{title}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 |
70 | {% if messages %} 71 | {% for message in messages %} 72 |
{{message}} 74 |
75 | {% endfor %} 76 | {% endif %} 77 |
78 | 79 |
80 |
81 |
82 |

Balance: $468.47

83 |
84 |
85 |

16:24:01

86 |
87 |
88 |

PNL: $0.00

89 |
90 |
91 |

PerpSniper v0.2

92 | 93 | 94 |
95 |
96 |

Open Positions:

97 |
98 |
99 | CLOSE ALL 100 |
101 |
102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
PAIRDIRECTIONQUANTITYPNLROE
115 |
116 |
117 | 118 | 119 | 120 |
121 | 122 |
123 | 124 | 125 |
126 |
1m
127 |
15m
128 |
1h
129 |
4h
130 |
131 | 132 | 133 |
134 |

Recent Signals:

135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | {% for s in signals %} 146 | 147 | 148 | 149 | 150 | 151 | {% endfor %} 152 | 153 |
TimeSymbolAlert
{{s[0]}}{{s[1]}}{{s[2]}}
154 |
155 |
156 |
157 | 158 |
159 | 160 |
161 |
162 |
163 | 164 |
165 |
166 | ADD QUICKTRADE PAIR 167 |
168 |
169 |
170 | 171 | 172 | 185 | 186 | 187 |
188 |
190 | Settings 191 |
192 |
193 | 194 | 195 | 196 | 197 | 198 | 199 | 203 | 204 | 236 | 237 | -------------------------------------------------------------------------------- /signals_loop.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | import os 4 | import sqlite3 5 | import time 6 | import logging 7 | 8 | from collections import Counter 9 | 10 | from datetime import datetime, timedelta 11 | 12 | from apscheduler.schedulers.background import BackgroundScheduler 13 | from binance.exceptions import BinanceAPIException 14 | 15 | from signals import Signals 16 | from coin_data import CoinData 17 | 18 | 19 | scheduler = BackgroundScheduler() 20 | 21 | file_path = os.path.abspath(os.path.dirname(__file__)) 22 | os.chdir(file_path) 23 | 24 | # flush log file 25 | try: 26 | with open('log.log', 'r') as f: 27 | lines = f.readlines() 28 | if len(lines) >= 88: 29 | with open('log.log', 'w') as f: 30 | f.writelines(lines[-88:]) 31 | except FileNotFoundError: 32 | pass 33 | 34 | logger = logging.getLogger(__name__) 35 | logging.basicConfig(level=logging.DEBUG, filename='log.log') 36 | 37 | 38 | class MainLoop: 39 | 40 | def __init__(self, coin_data): 41 | 42 | """Check signals for all coins passed from coin_data. Also includes tear down method and background tasks.""" 43 | 44 | os.system('cls' if os.name == 'nt' else 'clear') 45 | print('15m, 1h & 4h signals update every minute to offer an early warning of events occuring on current (' 46 | 'unclosed) candle\'s close. **Expect false positives**\nIf signals are still true they ' 47 | 'are only repeated after 30 minutes\nHint: Look for volume signals to confirm other signals') 48 | self.data_lock = True 49 | self.data = coin_data 50 | self.coins = self.data.symbols 51 | try: 52 | self.conn = self.open_database() 53 | self.flush_db() 54 | self.data_lock = False 55 | except Exception as e: 56 | raise e 57 | 58 | def open_database(self): 59 | path = os.path.abspath(os.path.dirname(__file__)) 60 | file = os.path.join(path, 'signals.db') 61 | self.conn = sqlite3.connect(file) 62 | return self.conn 63 | 64 | def close_database(self): 65 | if self.conn: 66 | self.conn.close() 67 | 68 | def get_qs(self): 69 | self.open_database() 70 | c = self.conn.cursor() 71 | qs = [a for a in c.execute('SELECT * FROM signals')] 72 | c.close() 73 | self.close_database() 74 | return qs 75 | 76 | def flush_db(self): 77 | alert_times = [] 78 | qs = self.get_qs() 79 | for alert in qs: 80 | if self.check_time(alert): 81 | alert_times.append(f'{alert[0]}',) 82 | at = tuple(alert_times) 83 | self.delete_multiple_records(at) 84 | 85 | @staticmethod 86 | def check_time(alert, minutes=30): 87 | full_time = datetime.strptime(datetime.now().strftime('%Y-%m-%d ') + alert[0], 88 | '%Y-%m-%d %H:%M:%S') 89 | if full_time < datetime.now() - timedelta(minutes=minutes) or full_time > datetime.now(): 90 | return True 91 | 92 | def delete_multiple_records(self, idList): 93 | try: 94 | self.open_database() 95 | c = self.conn.cursor() 96 | idList = [(a, ) for a in idList] 97 | sqlite_update_query = f"""DELETE from signals where time = ?""" 98 | c.executemany(sqlite_update_query, idList) 99 | self.conn.commit() 100 | c.close() 101 | 102 | except sqlite3.Error as error: 103 | print("Failed to delete multiple records from sqlite table", error) 104 | finally: 105 | self.close_database() 106 | 107 | def get_popular_coins(self): 108 | self.data = CoinData() 109 | self.coins = self.data.symbols 110 | print(f'''Current Symbols (highest volume): 111 | {', '.join(self.coins)}''') 112 | return self.coins 113 | 114 | def register_alert(self, alert, coin, tf): 115 | try: 116 | self.open_database() 117 | c = self.conn.cursor() 118 | c.execute(f'''INSERT INTO signals VALUES 119 | ("{alert[0]}", "{coin}", "({tf}) {alert[1]}{' ' + alert[2] if len(alert) == 3 else ''}")''') 120 | self.conn.commit() 121 | c.close() 122 | except sqlite3.Error as error: 123 | print("Failed to register alert for " + coin, error) 124 | finally: 125 | self.close_database() 126 | 127 | def check_alert(self, alert, coin, tf): 128 | self.open_database() 129 | c = self.conn.cursor() 130 | qs = c.execute(f'''SELECT * FROM signals WHERE symbol="{coin}" AND alert="({tf}) {alert[1]}{' ' + alert[2] if len(alert) == 3 else ''}"''') 131 | qs = [a for a in qs] 132 | c.close() 133 | self.close_database() 134 | if len(qs): 135 | return True 136 | return False 137 | 138 | def check_hot_coins(self): 139 | self.open_database() 140 | c = self.conn.cursor() 141 | qs = c.execute(f'''SELECT * FROM signals''') 142 | 143 | symbols = [] 144 | hot_coins = [] 145 | for rec in qs: 146 | symbols.append(rec[1]) 147 | C = Counter(symbols) 148 | for item in C.items(): 149 | if item[1] >= 3: 150 | hot_coins.append(item) 151 | c.close() 152 | self.close_database() 153 | return list(sorted(hot_coins, key=lambda x: x[1], reverse=True))[:6] 154 | 155 | def mainloop(self): 156 | bad_coins = [] 157 | for coin in self.coins: 158 | check_time = datetime.now() 159 | try: 160 | signals_15m = Signals(coin, tf='15m') 161 | self.check_signals_object(signals_15m, coin, check_time) 162 | except BinanceAPIException as e: 163 | print(f'Something went wrong with {coin}: {e}') 164 | continue 165 | except IndexError: 166 | bad_coins.append(coin) 167 | continue 168 | try: 169 | signals_1h = Signals(coin, tf='1h') 170 | self.check_signals_object(signals_1h, coin, check_time) 171 | except BinanceAPIException as e: 172 | print(f'Something went wrong with {coin}: {e}') 173 | continue 174 | except IndexError: 175 | pass 176 | try: 177 | signals_4h = Signals(coin) 178 | self.check_signals_object(signals_4h, coin, check_time) 179 | except BinanceAPIException as e: 180 | print(f'Something went wrong with {coin}: {e}') 181 | except IndexError: 182 | pass 183 | self.flush_db() 184 | for coin in bad_coins: 185 | self.coins.remove(coin) 186 | 187 | def check_signals_object(self, signals_obj, coin, check_time): 188 | for k, v in signals_obj.ema_signals_dict.items(): 189 | if v is True: 190 | alert = (check_time.strftime("%H:%M:%S"), k, 'bullish') 191 | if not self.check_alert(alert, coin, signals_obj.tf): 192 | self.register_alert(alert, coin, signals_obj.tf) 193 | elif v is False: 194 | alert = (check_time.strftime("%H:%M:%S"), k, 'bearish') 195 | if not self.check_alert(alert, coin, signals_obj.tf): 196 | self.register_alert(alert, coin, signals_obj.tf) 197 | for k, v in signals_obj.rsi_div_dict.items(): 198 | if v is True: 199 | alert = (check_time.strftime("%H:%M:%S"), k) 200 | if not self.check_alert(alert, coin, signals_obj.tf): 201 | self.register_alert(alert, coin, signals_obj.tf) 202 | for k, v in signals_obj.rsi_ob_os_dict.items(): 203 | if v is True: 204 | alert = (check_time.strftime("%H:%M:%S"), k) 205 | if not self.check_alert(alert, coin, signals_obj.tf): 206 | self.register_alert(alert, coin, signals_obj.tf) 207 | for k, v in signals_obj.macd_dict.items(): 208 | if v is not None: 209 | if v is True: 210 | v = 'up' 211 | else: 212 | v = 'down' 213 | alert = (check_time.strftime("%H:%M:%S"), ' '.join((k, v))) 214 | if not self.check_alert(alert, coin, signals_obj.tf): 215 | self.register_alert(alert, coin, signals_obj.tf) 216 | if signals_obj.vol_signal: 217 | alert = (check_time.strftime("%H:%M:%S"), 'Volume rising') 218 | if not self.check_alert(alert, coin, signals_obj.tf): 219 | self.register_alert(alert, coin, signals_obj.tf) 220 | if signals_obj.vol_candle: 221 | alert = (check_time.strftime("%H:%M:%S"), 'Current candle large volume') 222 | if not self.check_alert(alert, coin, signals_obj.tf): 223 | self.register_alert(alert, coin, signals_obj.tf) 224 | 225 | def start_jobs(self, jobs=None): 226 | """:param jobs: list of tuples (job, trigger, interval)""" 227 | scheduler.start() 228 | scheduler.add_job(self.mainloop, trigger="cron", minute='*/1') 229 | # scheduler.add_job(self.get_popular_coins, trigger="cron", hour='*/1') 230 | if jobs: 231 | for job in jobs: 232 | if job[1] == 'interval': 233 | scheduler.add_job(job[0], trigger=job[1], seconds=job[2]) 234 | elif job[1] == 'cron': 235 | scheduler.add_job(job[0], trigger=job[1], second=job[2]) 236 | 237 | def teardown(self): 238 | self.close_database() 239 | print('Signals loop teardown completed') 240 | 241 | 242 | # if __name__ == '__main__': 243 | # loop = MainLoop() 244 | # try: 245 | # loop.check_hot_coins() 246 | # loop.mainloop() 247 | # loop.start_jobs() 248 | # while True: 249 | # time.sleep(1) 250 | # finally: 251 | # loop.teardown() 252 | -------------------------------------------------------------------------------- /charts.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | import sys 4 | 5 | import matplotlib 6 | 7 | matplotlib.use('agg') 8 | 9 | import mplfinance as mpf 10 | import pandas as pd 11 | import pandas_ta as ta 12 | 13 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas 14 | 15 | from matplotlib import pyplot as plt 16 | 17 | from coin_data import CoinData 18 | 19 | plt.style.use('dark_background') 20 | import numpy as np 21 | import numba as nb 22 | 23 | @nb.jit(fastmath=True, nopython=True) 24 | def calc_rsi( array, deltas, avg_gain, avg_loss, n ): 25 | 26 | # Use Wilder smoothing method 27 | up = lambda x: x if x > 0 else 0 28 | down = lambda x: -x if x < 0 else 0 29 | i = n+1 30 | for d in deltas[n+1:]: 31 | avg_gain = ((avg_gain * (n-1)) + up(d)) / n 32 | avg_loss = ((avg_loss * (n-1)) + down(d)) / n 33 | if avg_loss != 0: 34 | rs = avg_gain / avg_loss 35 | array[i] = 100 - (100 / (1 + rs)) 36 | else: 37 | array[i] = 100 38 | i += 1 39 | 40 | return array 41 | 42 | def get_rsi( array, n = 14 ): 43 | 44 | deltas = np.append([0],np.diff(array)) 45 | 46 | avg_gain = np.sum(deltas[1:n+1].clip(min=0)) / n 47 | avg_loss = -np.sum(deltas[1:n+1].clip(max=0)) / n 48 | 49 | array = np.empty(deltas.shape[0]) 50 | array.fill(np.nan) 51 | 52 | array = calc_rsi( array, deltas, avg_gain, avg_loss, n ) 53 | return array 54 | 55 | class Charts: 56 | 57 | def __init__(self, symbol='BTCUSDT', tf='1h', entry=None, direction=None): 58 | """When initialised, creates chart for given symbol and timeframe 59 | :param symbol: str uppercase eg. 'BTCUSDT' 60 | :param tf: str lowercase eg. '1m', '15m', '1h', '4h' 61 | :param entry: float if open positions 62 | :param direction: str 'LONG', 'SHORT' or None depending on position""" 63 | self.symbol = symbol.upper() 64 | self.tf = tf 65 | self.df = CoinData.get_dataframe(self.symbol, self.tf) 66 | self.df = self.df_ta() 67 | self.entry = entry 68 | self.direction = True if direction == 'LONG' else False 69 | self.tp = 0.03 70 | self.sl = 0.01 71 | 72 | def df_ta(self) -> pd.DataFrame: 73 | df = self.df 74 | # df['rsi'] = ta.rsi(df.close, 14) 75 | df['rsi'] = get_rsi(df.close, 14) 76 | df = pd.concat((df, ta.macd(df.close, 12, 26, 9)), axis=1) 77 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50) 78 | if len(df) >= 288: 79 | df['ema_200'] = ta.ema(df.close, 200) 80 | else: 81 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3) 82 | df = df.tail(88) 83 | return df 84 | 85 | @staticmethod 86 | def get_rsi_timeseries(prices, n=14): 87 | # RSI = 100 - (100 / (1 + RS)) 88 | # where RS = (Wilder-smoothed n-period average of gains / Wilder-smoothed n-period average of -losses) 89 | # Note that losses above should be positive values 90 | # Wilder-smoothing = ((previous smoothed avg * (n-1)) + current value to average) / n 91 | # For the very first "previous smoothed avg" (aka the seed value), we start with a straight average. 92 | # Therefore, our first RSI value will be for the n+2nd period: 93 | # 0: first delta is nan 94 | # 1: 95 | # ... 96 | # n: lookback period for first Wilder smoothing seed value 97 | # n+1: first RSI 98 | 99 | # First, calculate the gain or loss from one price to the next. The first value is nan so replace with 0. 100 | deltas = (prices - prices.shift(1)).fillna(0) 101 | 102 | # Calculate the straight average seed values. 103 | # The first delta is always zero, so we will use a slice of the first n deltas starting at 1, 104 | # and filter only deltas > 0 to get gains and deltas < 0 to get losses 105 | avg_of_gains = deltas[1:n + 1][deltas > 0].sum() / n 106 | avg_of_losses = -deltas[1:n + 1][deltas < 0].sum() / n 107 | 108 | # Set up pd.Series container for RSI values 109 | rsi_series = pd.Series(0.0, deltas.index) 110 | 111 | # Now calculate RSI using the Wilder smoothing method, starting with n+1 delta. 112 | up = lambda x: x if x > 0 else 0 113 | down = lambda x: -x if x < 0 else 0 114 | i = n + 1 115 | for d in deltas[n + 1:]: 116 | avg_of_gains = ((avg_of_gains * (n - 1)) + up(d)) / n 117 | avg_of_losses = ((avg_of_losses * (n - 1)) + down(d)) / n 118 | if avg_of_losses != 0: 119 | rs = avg_of_gains / avg_of_losses 120 | rsi_series[i] = 100 - (100 / (1 + rs)) 121 | else: 122 | rsi_series[i] = 100 123 | i += 1 124 | 125 | return rsi_series 126 | 127 | 128 | def main_chart(self): 129 | fig, axes = plt.subplots(nrows=3, ncols=1, gridspec_kw={'height_ratios': [3, 1, 1]}) 130 | fig.suptitle(f"{self.symbol} {self.tf}", fontsize=16) 131 | ax_r = axes[0].twinx() 132 | mc = mpf.make_marketcolors(up='#00e600', down='#ff0066', 133 | edge={'up': '#00e600', 'down': '#ff0066'}, 134 | wick={'up': '#00e600', 'down': '#ff0066'}, 135 | volume={'up': '#808080', 'down': '#4d4d4d'}, 136 | ohlc='black') 137 | s = mpf.make_mpf_style(marketcolors=mc) 138 | ax_r.set_alpha(0.01) 139 | axes[0].set_zorder(2) 140 | for ax in axes: 141 | ax.set_facecolor((0, 0, 0, 0)) 142 | ax_r.set_zorder(1) 143 | 144 | axes[1].set_ylabel('RSI') 145 | axes[1].margins(x=0, y=0.1) 146 | axes[0].margins(x=0, y=0.05) 147 | axes[2].set_ylabel('MACD') 148 | ax_r.set_ylabel('') 149 | ax_r.yaxis.set_visible(False) 150 | axes[2].margins(0, 0.05) 151 | axes[0].xaxis.set_visible(False) 152 | axes[1].xaxis.set_visible(False) 153 | 154 | axes[0].yaxis.tick_left() 155 | axes[0].yaxis.set_label_position('right') 156 | axes[1].yaxis.set_label_position('right') 157 | axes[2].yaxis.set_label_position('right') 158 | plt.tight_layout() 159 | fig.autofmt_xdate() 160 | self.df.volume = self.df.volume.div(2) 161 | addplot_200 = mpf.make_addplot(self.df['ema_200'], type='line', ax=axes[0], width=1, color='#ff0066') 162 | addplot_50 = mpf.make_addplot(self.df['ema_50'], type='line', ax=axes[0], width=1, color='#00e600') 163 | mpf.plot(self.df, ax=axes[0], type="candle", style=s, volume=ax_r, ylabel='', addplot=[addplot_200, addplot_50]) 164 | max_vol = max({y for index, y in self.df.volume.items()}) 165 | ax_r.axis(ymin=0, ymax=max_vol * 3) 166 | self.df['rsi'].plot(ax=axes[1], legend=False, use_index=True, sharex=axes[0], color='#00e600') 167 | self.df['MACD_12_26_9'].plot(ax=axes[2], legend=False, use_index=True, sharex=axes[0], color='#00e600') 168 | self.df['MACDs_12_26_9'].plot(ax=axes[2], legend=False, use_index=True, sharex=axes[0], color='#ff0066') 169 | axes[2].axhline(0, color='gray', ls='--', linewidth=1) 170 | axes[1].axhline(70, color='gray', ls='--', linewidth=1) 171 | axes[1].axhline(30, color='gray', ls='--', linewidth=1) 172 | if self.entry: 173 | tp = self.entry + self.entry * self.tp if self.direction else self.entry - self.entry * self.tp 174 | sl = self.entry - self.entry * self.sl if self.direction else self.entry + self.entry * self.sl 175 | tp_color = 'red' if self.direction else 'green' 176 | sl_color = 'red' if not self.direction else 'green' 177 | axes[0].axhline(self.entry, color='yellow', ls="--", linewidth=.5) 178 | axes[0].axhline(tp, color=tp_color, ls="--", linewidth=.5) 179 | axes[0].axhline(sl, color=sl_color, ls="--", linewidth=.5) 180 | axes[2].set_xlabel('') 181 | img = io.BytesIO() 182 | FigureCanvas(fig).print_png(img) 183 | plot_url = base64.b64encode(img.getvalue()).decode() 184 | fig.savefig('plot.png', format='png') 185 | plt.close(fig) 186 | return plot_url 187 | 188 | # def plot_rsi_div(self): 189 | # rsi_array = np.array(self.df['rsi'].tail(20).array) 190 | # close_array = np.array(self.df['close'].tail(20).array) 191 | # rsi_peaks, _ = scipy.signal.find_peaks(rsi_array) 192 | # rsi_troughs, _ = scipy.signal.find_peaks(-rsi_array) 193 | # fig, (ax1, ax2) = plt.subplots(2, sharex=True) 194 | # fig.suptitle(f'{self.symbol} RSI Divergence {self.tf}') 195 | # ax1.set_ylabel('Close') 196 | # ax2.set_ylabel('RSI') 197 | # ax2.axhline(70, color='gray', ls='--') 198 | # ax2.axhline(30, color='gray', ls='--') 199 | # ax1.xaxis.set_visible(False) 200 | # ax2.xaxis.set_visible(False) 201 | # ax1.plot(close_array) 202 | # ax2.plot(rsi_array, color='green') 203 | # ax1.plot(rsi_peaks, close_array[rsi_peaks], '.', color="#ff0066") 204 | # ax2.plot(rsi_peaks, rsi_array[rsi_peaks], '.', color="#ff0066") 205 | # ax1.plot(rsi_troughs, close_array[rsi_troughs], '.', color="#00e600") 206 | # ax2.plot(rsi_troughs, rsi_array[rsi_troughs], '.', color="#00e600") 207 | # _, new_close_array, new_rsi_array, indices = self.rsi_divergence() 208 | # if len(close_array) != len(new_close_array): 209 | # ax1.plot(indices, new_close_array, color="#ff0066") 210 | # ax2.plot(indices, new_rsi_array, color="#ff0066") 211 | # img = io.BytesIO() 212 | # fig.savefig(img, format='png') 213 | # img.seek(0) 214 | # plot_url = base64.b64encode(img.getvalue()).decode() 215 | # plt.close() 216 | # return plot_url 217 | 218 | def plot_charts(self): 219 | self.main_chart() 220 | # self.plot_rsi_div() 221 | 222 | 223 | if __name__ == '__main__': 224 | c = Charts('SNXUSDT', '15m') 225 | print('plotting charts') 226 | c.plot_charts() 227 | print('done') 228 | sys.exit() 229 | -------------------------------------------------------------------------------- /algo_trader.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import time 4 | from datetime import datetime, timedelta 5 | 6 | from apscheduler.schedulers.background import BackgroundScheduler 7 | from http.client import RemoteDisconnected 8 | from trader import Trader 9 | from coin_data import CoinData 10 | from signals import Signals 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(logging.DEBUG) 15 | handler = logging.StreamHandler(sys.stdout) 16 | handler.setLevel(logging.DEBUG) 17 | formatter = logging.Formatter('%(asctime)s - [ %(levelname)s ] - %(message)s') 18 | handler.setFormatter(formatter) 19 | logger.addHandler(handler) 20 | 21 | 22 | class AlgoTrader: 23 | 24 | """Check each coin for signals and make trades in certain conditions. 25 | Conditions: 26 | - 15m RSI oversold/overbought and RSI divergence, and 1h ema_50/ema_200 trend 27 | - 15m RSI oversold/overbought in last hour and macd crossing up""" 28 | 29 | data = CoinData() 30 | trader = Trader() 31 | scheduler = BackgroundScheduler() 32 | 33 | def __init__(self): 34 | self.signals_dict = {} 35 | self.trend_markers = {} 36 | self.rsi_markers = {} 37 | self.get_signals() 38 | self.check_emas() 39 | self.event_loop = None 40 | self.recent_alerts = [] 41 | self.trader = Trader() 42 | self.trader.settings['sl'] = 0.005 43 | self.trader.settings['tp'] = 0.015 44 | self.trader.settings['qty'] = 0.01 45 | self.trader.settings['db'] = 0.2 46 | 47 | def get_signals(self): 48 | inadequate_symbols = [] 49 | for symbol in self.data.symbols: 50 | try: 51 | self.signals_dict[symbol] = ( # Signals(symbol, '1m'), 52 | Signals(symbol, '15m'), 53 | Signals(symbol, '1h'), 54 | Signals(symbol, '4h')) 55 | except IndexError: 56 | inadequate_symbols.append(symbol) 57 | continue 58 | for symbol in inadequate_symbols: 59 | self.data.symbols.remove(symbol) 60 | return self.signals_dict 61 | 62 | def check_emas(self): 63 | for symbol in self.data.symbols: 64 | # signals_1m = self.signals_dict[symbol][0] 65 | signals_15m = self.signals_dict[symbol][0] 66 | signals_1h = self.signals_dict[symbol][1] 67 | signals_4h = self.signals_dict[symbol][2] 68 | h4 = True if signals_4h.df.ema_50.iloc[-1] > signals_4h.df.ema_200.iloc[-1] else False 69 | h1 = True if signals_1h.df.ema_50.iloc[-1] > signals_1h.df.ema_200.iloc[-1] else False 70 | m15 = True if signals_15m.df.ema_50.iloc[-1] > signals_15m.df.ema_200.iloc[-1] else False 71 | # m1 = True if signals_1m.df.ema_50.iloc[-1] > signals_1m.df.ema_200.iloc[-1] else False 72 | self.trend_markers[symbol] = (m15, h1, h4) 73 | return self.trend_markers 74 | 75 | def long_condition(self, open_positions, recent_alerts): 76 | for symbol in self.signals_dict.keys(): 77 | if self.trend_markers[symbol][1] and self.trend_markers[symbol][2]: 78 | if self.signals_dict[symbol][0].rsi_div_dict['confirmed bullish divergence']: 79 | alert = f'LONG {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' 80 | if symbol not in open_positions and symbol not in recent_alerts: 81 | self.trader.trade(symbol, True) 82 | with open('buys.txt', 'a') as f: 83 | f.write(alert + '\n') 84 | self.recent_alerts.append(alert) 85 | logger.info(alert) 86 | 87 | def short_condition(self, open_positions, recent_alerts): 88 | for symbol in self.signals_dict.keys(): 89 | if not self.trend_markers[symbol][1] and not self.trend_markers[symbol][2]: 90 | if self.signals_dict[symbol][0].rsi_div_dict['confirmed bearish divergence']: 91 | alert = f'SHORT {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' 92 | if symbol not in open_positions and symbol not in recent_alerts: 93 | self.trader.trade(symbol, False) 94 | with open('buys.txt', 'a') as f: 95 | f.write(alert) 96 | self.recent_alerts.append(alert) 97 | logger.info(alert) 98 | 99 | def purge_alerts(self): 100 | old_alerts = [] 101 | for alert in self.recent_alerts: 102 | split_alert = alert.split(' ') 103 | if datetime.strptime(' '.join(split_alert[3:5]), '%Y-%m-%d %H:%M:%S') < datetime.now() - timedelta(minutes=45): 104 | old_alerts.append(alert) 105 | for alert in old_alerts: 106 | self.recent_alerts.remove(alert) 107 | 108 | def check_rsi_div(self, symbol): 109 | if self.signals_dict[symbol][0].rsi_div_dict['confirmed bearish divergence']: 110 | return False 111 | elif self.signals_dict[symbol][0].rsi_div_dict['confirmed bullish divergence']: 112 | return True 113 | else: 114 | return None 115 | 116 | def check_rsi_ob_os(self, symbol): 117 | if self.signals_dict[symbol][0].rsi_ob_os_dict['overbought']: 118 | return False 119 | elif self.signals_dict[symbol][0].rsi_ob_os_dict['oversold']: 120 | return True 121 | else: 122 | return None 123 | 124 | def check_trend(self, symbol): 125 | if self.trend_markers[symbol][1] and self.trend_markers[symbol][2]: 126 | return True 127 | elif not self.trend_markers[symbol][1] and not self.trend_markers[symbol][2]: 128 | return False 129 | else: 130 | return None 131 | 132 | def rsi_ob_os_trade(self): 133 | for symbol in self.signals_dict.keys(): 134 | if self.check_trend(symbol) is True: 135 | if self.check_rsi_ob_os(symbol) is True: 136 | self.trader.trade(symbol, True) 137 | elif self.check_trend(symbol) is False: 138 | if self.check_rsi_ob_os(symbol) is False: 139 | self.trader.trade(symbol, False) 140 | 141 | def rsi_ob_os_marker(self): 142 | for symbol in self.signals_dict.keys(): 143 | if self.check_trend(symbol) is True: 144 | if self.check_rsi_ob_os(symbol) is True: 145 | self.rsi_markers['symbol'] = (True, datetime.now()) 146 | elif self.check_trend(symbol) is False: 147 | if self.check_rsi_ob_os(symbol) is False: 148 | self.rsi_markers['symbol'] = (False, datetime.now()) 149 | 150 | def purge_rsi_markers(self): 151 | old_keys = [] 152 | for key, value in self.rsi_markers.items(): 153 | if value[1] < datetime.now() - timedelta(hours=1): 154 | old_keys.append(key) 155 | for key in old_keys: 156 | self.rsi_markers.pop(key, None) 157 | 158 | def rsi_div_trade(self, open_positions, recent_alerts): 159 | for symbol in self.signals_dict.keys(): 160 | if symbol not in open_positions and symbol not in recent_alerts: 161 | if self.check_trend(symbol) is True: 162 | if self.check_rsi_div(symbol) is True: 163 | self.trader.trade(symbol, True) 164 | alert = f'LONGED {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' 165 | self.handle_alert(alert) 166 | elif self.check_trend(symbol) is False: 167 | if self.check_rsi_div(symbol) is False: 168 | self.trader.trade(symbol, False) 169 | alert = f'SHORTED {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' 170 | self.handle_alert(alert) 171 | 172 | def handle_alert(self, alert): 173 | self.recent_alerts.append(alert) 174 | logger.info(alert) 175 | 176 | def check_conditions(self): 177 | try: 178 | logger.debug('checking signal data') 179 | start_time = datetime.now() 180 | 181 | # Get new signals data 182 | self.get_signals() 183 | self.check_emas() 184 | self.purge_alerts() 185 | recent_alerts_symbols = [alert.split(' ')[1] for alert in self.recent_alerts] 186 | open_positions = self.trader.check_positions_cancel_open_orders() 187 | 188 | log_statement = 'took: {}'.format(datetime.now() - start_time) 189 | logger.debug(log_statement) 190 | logger.debug('checking trade conditions') 191 | 192 | # Check trade conditions 193 | self.rsi_div_trade(open_positions, recent_alerts_symbols) 194 | 195 | if self.recent_alerts: 196 | recent = ', '.join(self.recent_alerts) 197 | logger.debug(recent) 198 | 199 | total_time = datetime.now() - start_time 200 | log_statement = 'total_time: {}'.format(total_time) 201 | logger.debug(log_statement) 202 | 203 | except Exception as e: 204 | log_statement = f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}: {e}' 205 | logger.warning(log_statement) 206 | 207 | def save_data(self): 208 | self.data.save_latest_data() 209 | 210 | def schedule_tasks(self): 211 | self.scheduler.add_job(self.save_data, trigger='cron', minute='*/1', second="58") 212 | self.scheduler.add_job(self.check_conditions, trigger='cron', minute='*/1') 213 | self.scheduler.start() 214 | 215 | def stop_tasks(self): 216 | self.scheduler.remove_all_jobs() 217 | self.scheduler.shutdown() 218 | 219 | def loop(self): 220 | try: 221 | self.schedule_tasks() 222 | while True: 223 | time.sleep(1) 224 | except KeyboardInterrupt as e: 225 | self.stop_tasks() 226 | sys.exit() 227 | 228 | 229 | if __name__ == '__main__': 230 | at = AlgoTrader() 231 | at.loop() 232 | -------------------------------------------------------------------------------- /async_signals.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from datetime import datetime 4 | 5 | import pandas as pd 6 | import pandas_ta as ta 7 | import numpy as np 8 | import scipy.signal 9 | from trader import Trader 10 | from coin_data import CoinData 11 | 12 | client = Trader().client 13 | file_path = os.path.abspath(os.path.dirname(__file__)) 14 | os.chdir(file_path) 15 | 16 | 17 | class Signals: 18 | 19 | def __init__(self, symbol, tf): 20 | 21 | """Check for signals for given symbol and timeframe""" 22 | 23 | self.symbol = symbol.upper() 24 | self.tf = tf 25 | self.df = CoinData.get_dataframe(symbol, tf) 26 | self.df = self.df_ta() 27 | # self.df = self.get_heiken_ashi() 28 | # self.vol_signal = self.vol_rise_fall() 29 | # self.vol_candle = self.large_vol_candle() 30 | # self.HA_trend = self.get_heiken_ashi_trend(self.get_heiken_ashi(self.df)) 31 | self.rsi_ob_os_dict = { 32 | 'overbought': False, 33 | 'oversold': False, 34 | } 35 | 36 | self.rsi_div_dict = { 37 | 'possible bearish divergence': False, 38 | 'possible bullish divergence': False, 39 | 'confirmed bearish divergence': False, 40 | 'confirmed bullish divergence': False, 41 | } 42 | 43 | self.macd_dict = { 44 | 'MACD cross': None, 45 | 'MACD 0 cross': None, 46 | } 47 | 48 | self.ema_signals_dict = { 49 | 'Price crossing EMA200': None, 50 | 'EMA20 crossing EMA50': None, 51 | 'EMA50 crossing EMA200': None, 52 | } 53 | 54 | async def _async_init(self): 55 | task_0 = asyncio.create_task(self.rsi_overbought_oversold()) 56 | task_1 = asyncio.create_task(self.rsi_divergence()) 57 | task_2 = asyncio.create_task(self.macd_signals()) 58 | task_3 = asyncio.create_task(self.ema_signals()) 59 | await task_0 60 | await task_1 61 | await task_2 62 | await task_3 63 | 64 | def full_check(self): 65 | self.rsi_divergence() 66 | self.ema_signals() 67 | self.macd_signals() 68 | self.rsi_overbought_oversold() 69 | # self.vol_rise_fall() 70 | # self.large_vol_candle() 71 | 72 | def df_ta(self) -> pd.DataFrame: 73 | df = self.df 74 | df['rsi'] = ta.rsi(df.close, 14) 75 | df = pd.concat((df, ta.macd(df.close, 12, 26, 9)), axis=1) 76 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50) 77 | if len(df) >= 288: 78 | df['ema_200'] = ta.ema(df.close, 200) 79 | else: 80 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3) 81 | df = df.tail(88) 82 | return df 83 | 84 | @staticmethod 85 | def get_heiken_ashi(df): 86 | df['HA_Close'] = (df['open'] + df['high'] + df['low'] + df['close']) / 4 87 | idx = df.index.name 88 | df.reset_index(inplace=True) 89 | 90 | for i in range(0, len(df)): 91 | if i == 0: 92 | df.at[i, 'HA_Open'] = ((df._get_value(i, 'open') + df._get_value(i, 'close')) / 2) 93 | else: 94 | df.at[i, 'HA_Open'] = ((df._get_value(i - 1, 'HA_Open') + df._get_value(i - 1, 'HA_Close')) / 2) 95 | 96 | if idx: 97 | df.set_index(idx, inplace=True) 98 | 99 | df['HA_High'] = df[['HA_Open', 'HA_Close', 'high']].max(axis=1) 100 | df['HA_Low'] = df[['HA_Open', 'HA_Close', 'low']].min(axis=1) 101 | 102 | return df 103 | 104 | @staticmethod 105 | def get_heiken_ashi_trend(df): 106 | if df['HA_Close'].iloc[-1] > df['HA_Open'].iloc[-1]: 107 | if df['HA_Close'].iloc[-2] > df['HA_Open'].iloc[-2]: 108 | return True 109 | elif df['HA_Close'].iloc[-1] < df['HA_Open'].iloc[-1]: 110 | if df['HA_Close'].iloc[-2] < df['HA_Open'].iloc[-2]: 111 | return False 112 | else: 113 | return None 114 | 115 | def rsi_divergence(self): 116 | rsi_array = np.array(self.df['rsi'].tail(20).array) 117 | close_array = np.array(self.df['close'].tail(20).array) 118 | rsi_peaks, _ = scipy.signal.find_peaks(rsi_array) 119 | rsi_troughs, _ = scipy.signal.find_peaks(-rsi_array) 120 | original_index = len(close_array) 121 | indices = np.array([]) 122 | 123 | # bearish divergence confirmed: rsi formed lower peak while price formed higher peak 124 | if 70 <= rsi_array[rsi_peaks[-2]] >= rsi_array[rsi_peaks[-1]] >= rsi_array[-2] >= rsi_array[-1]: 125 | if close_array[rsi_peaks[-2]] <= close_array[rsi_peaks[-1]]: 126 | close_array = np.array([close_array[rsi_peaks[-2]], close_array[rsi_peaks[-1]]]) 127 | rsi_array = np.array([rsi_array[rsi_peaks[-2]], rsi_array[rsi_peaks[-1]]]) 128 | indices = np.array([rsi_peaks[-2], rsi_peaks[-1]]) 129 | self.rsi_div_dict['confirmed bearish divergence'] = True 130 | 131 | # possible bearish divergence: rsi forming lower peak while price forming higher peak 132 | elif 70 <= rsi_array[rsi_peaks[-1]] >= rsi_array[-2] > rsi_array[-1]: 133 | if close_array[rsi_peaks[-1]] <= close_array[-1]: 134 | close_array = np.array([close_array[rsi_peaks[-1]], close_array[-1]]) 135 | rsi_array = np.array([rsi_array[rsi_peaks[-1]], rsi_array[-1]]) 136 | indices = np.array([rsi_peaks[-1], original_index]) 137 | self.rsi_div_dict['possible bearish divergence'] = True 138 | 139 | # bullish divergence confirmed: rsi formed higher trough while price formed lower trough 140 | elif 30 >= rsi_array[rsi_troughs[-2]] <= rsi_array[rsi_troughs[-1]] <= rsi_array[-2] <= rsi_array[-1]: 141 | if close_array[rsi_troughs[-2]] >= close_array[rsi_troughs[-1]]: 142 | close_array = np.array([close_array[rsi_troughs[-2]], close_array[rsi_troughs[-1]]]) 143 | rsi_array = np.array([rsi_array[rsi_troughs[-2]], rsi_array[rsi_troughs[-1]]]) 144 | indices = np.array([rsi_troughs[-2], rsi_troughs[-1]]) 145 | self.rsi_div_dict['confirmed bullish divergence'] = True 146 | 147 | # possible bullish divergence: rsi forming higher trough while price forming lower trough 148 | elif 30 >= rsi_array[rsi_troughs[-1]] <= rsi_array[-2] < rsi_array[-1]: 149 | if close_array[rsi_troughs[-1]] >= close_array[-1]: 150 | close_array = np.array([close_array[rsi_troughs[-1]], close_array[-1]]) 151 | rsi_array = np.array([rsi_array[rsi_troughs[-1]], rsi_array[-1]]) 152 | indices = np.array([rsi_troughs[-1], original_index]) 153 | self.rsi_div_dict['possible bullish divergence'] = True 154 | 155 | return self.rsi_div_dict, close_array, rsi_array, indices 156 | 157 | def rsi_overbought_oversold(self, o_s=30, o_b=70): 158 | rsi_array = self.df['rsi'].array 159 | if rsi_array[-3] <= o_s <= rsi_array[-2]: 160 | self.rsi_ob_os_dict['oversold'] = True 161 | elif rsi_array[-3] >= o_b >= rsi_array[-2]: 162 | self.rsi_ob_os_dict['overbought'] = True 163 | return self.rsi_ob_os_dict 164 | 165 | def macd_signals(self): 166 | if self.df['MACD_12_26_9'].array[-2] > self.df['MACDs_12_26_9'].array[-2]: 167 | if self.df['MACD_12_26_9'].array[-3] < self.df['MACDs_12_26_9'].array[-3]: 168 | self.macd_dict['MACD cross'] = True 169 | elif self.df['MACD_12_26_9'].array[-2] < self.df['MACDs_12_26_9'].array[-2]: 170 | if self.df['MACD_12_26_9'].array[-3] > self.df['MACDs_12_26_9'].array[-3]: 171 | self.macd_dict['MACD cross'] = False 172 | if (self.df['MACD_12_26_9'].array[-2], self.df['MACDs_12_26_9'].array[-2]) > (0, 0): 173 | if (self.df['MACD_12_26_9'].array[-3], self.df['MACDs_12_26_9'].array[-3]) <= (0, 0): 174 | self.macd_dict['MACD 0 cross'] = True 175 | elif (self.df['MACD_12_26_9'].array[-2], self.df['MACDs_12_26_9'].array[-2]) < (0, 0): 176 | if (self.df['MACD_12_26_9'].array[-3], self.df['MACDs_12_26_9'].array[-3]) >= (0, 0): 177 | self.macd_dict['MACD 0 cross'] = False 178 | 179 | def ema_signals(self): 180 | ema_200 = self.df['ema_200'].array[-3:] 181 | ema_50 = self.df['ema_50'].array[-3:] 182 | ema_20 = self.df['ema_20'].array[-3:] 183 | price = self.df['close'].array[-3:] 184 | if ema_200[0] > price[0] and ema_200[1] >= price[1] and ema_200[2] < price[2]: 185 | self.ema_signals_dict['Price crossing EMA200'] = True 186 | elif ema_200[0] < price[0] and ema_200[1] <= price[1] and ema_200[2] > price[2]: 187 | self.ema_signals_dict['Price crossing EMA200'] = False 188 | if ema_20[0] > ema_50[0] and ema_20[1] >= ema_50[1] and ema_20[2] < ema_50[2]: 189 | self.ema_signals_dict['EMA20 crossing EMA50'] = False 190 | elif ema_20[0] < ema_50[0] and ema_20[1] <= ema_50[1] and ema_20[2] > ema_50[2]: 191 | self.ema_signals_dict['EMA20 crossing EMA50'] = True 192 | if ema_50[0] > ema_200[0] and ema_50[1] >= ema_200[1] and ema_50[2] < ema_200[2]: 193 | self.ema_signals_dict['EMA50 crossing EMA200'] = False 194 | elif ema_50[0] < ema_200[0] and ema_50[1] <= ema_200[1] and ema_50[2] > ema_200[2]: 195 | self.ema_signals_dict['EMA50 crossing EMA200'] = True 196 | return self.ema_signals_dict 197 | 198 | # def vol_rise_fall(self): 199 | # recent_vol = self.df.volume.tail(3).array 200 | # self.vol_signal = True if recent_vol[0] < recent_vol[1] < recent_vol[2] else False 201 | # return self.vol_signal 202 | # 203 | # def large_vol_candle(self): 204 | # self.vol_candle = True if self.df.volume.array[-1] >= self.df.volume.tail(14).values.mean()*2 else False 205 | # return self.vol_candle 206 | 207 | 208 | async def create_signals_instance(symbol='BTCUSDT', tf='15m'): 209 | s = Signals(symbol, tf) 210 | await s._async_init() 211 | return s 212 | 213 | 214 | if __name__ == '__main__': 215 | x = datetime.now() 216 | df = CoinData.get_dataframe('BTCUSDT', '15m') 217 | print(Signals.get_heiken_ashi(df)) 218 | print(datetime.now() - x) -------------------------------------------------------------------------------- /signals.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | import pandas as pd 5 | import pandas_ta as ta 6 | import numpy as np 7 | import scipy.signal 8 | from trader import Trader 9 | from coin_data import CoinData 10 | 11 | client = Trader().client 12 | file_path = os.path.abspath(os.path.dirname(__file__)) 13 | os.chdir(file_path) 14 | 15 | 16 | class Signals: 17 | 18 | def __init__(self, symbol='BTCUSDT', tf='4h'): 19 | 20 | """Check for signals for given symbol and timeframe""" 21 | 22 | self.symbol = symbol.upper() 23 | self.tf = tf 24 | self.df = CoinData.get_dataframe(symbol, tf) 25 | self.df = self.df_ta() 26 | # self.df = self.get_heiken_ashi() 27 | self.vol_signal = self.vol_rise_fall() 28 | self.vol_candle = self.large_vol_candle() 29 | # self.HA_trend = self.get_heiken_ashi_trend(self.get_heiken_ashi(self.df)) 30 | self.rsi_ob_os_dict = { 31 | 'overbought': False, 32 | 'oversold': False, 33 | } 34 | self.rsi_overbought_oversold() 35 | self.rsi_div_dict = { 36 | 'possible bearish divergence': False, 37 | 'possible bullish divergence': False, 38 | 'confirmed bearish divergence': False, 39 | 'confirmed bullish divergence': False, 40 | } 41 | self.rsi_divergence() 42 | self.macd_dict = { 43 | 'MACD cross': None, 44 | 'MACD 0 cross': None, 45 | } 46 | self.macd_signals() 47 | self.ema_signals_dict = { 48 | 'Price crossing EMA200': None, 49 | 'EMA20 crossing EMA50': None, 50 | 'EMA50 crossing EMA200': None, 51 | } 52 | self.ema_signals() 53 | 54 | def full_check(self): 55 | self.rsi_divergence() 56 | self.ema_signals() 57 | self.macd_signals() 58 | self.rsi_overbought_oversold() 59 | self.vol_rise_fall() 60 | self.large_vol_candle() 61 | 62 | def df_ta(self) -> pd.DataFrame: 63 | df = self.df 64 | df['rsi'] = ta.rsi(df.close, 14) 65 | df = pd.concat((df, ta.macd(df.close, 12, 26, 9)), axis=1) 66 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50) 67 | if len(df) >= 288: 68 | df['ema_200'] = ta.ema(df.close, 200) 69 | else: 70 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3) 71 | df = df.tail(88) 72 | return df 73 | 74 | @staticmethod 75 | def get_heiken_ashi(df): 76 | df['HA_Close'] = (df['open'] + df['high'] + df['low'] + df['close']) / 4 77 | idx = df.index.name 78 | df.reset_index(inplace=True) 79 | 80 | for i in range(0, len(df)): 81 | if i == 0: 82 | df.at[i, 'HA_Open'] = ((df._get_value(i, 'open') + df._get_value(i, 'close')) / 2) 83 | else: 84 | df.at[i, 'HA_Open'] = ((df._get_value(i - 1, 'HA_Open') + df._get_value(i - 1, 'HA_Close')) / 2) 85 | 86 | if idx: 87 | df.set_index(idx, inplace=True) 88 | 89 | df['HA_High'] = df[['HA_Open', 'HA_Close', 'high']].max(axis=1) 90 | df['HA_Low'] = df[['HA_Open', 'HA_Close', 'low']].min(axis=1) 91 | 92 | return df 93 | 94 | @staticmethod 95 | def get_heiken_ashi_trend(df): 96 | if df['HA_Close'].iloc[-1] > df['HA_Open'].iloc[-1]: 97 | if df['HA_Close'].iloc[-2] > df['HA_Open'].iloc[-2] or all([df['HA_Low'].iloc[-2] < df['HA_Open'].iloc[-2], 98 | df['HA_High'].iloc[-2] > df['HA_Close'].iloc[-2]]): 99 | return True 100 | elif df['HA_Close'].iloc[-1] < df['HA_Open'].iloc[-1]: 101 | if df['HA_Close'].iloc[-2] < df['HA_Open'].iloc[-2] or all([df['HA_High'].iloc[-2] > df['HA_Open'].iloc[-2], 102 | df['HA_Low'].iloc[-2] < df['HA_Close'].iloc[-2]]): 103 | return False 104 | else: 105 | return None 106 | 107 | def rsi_divergence(self): 108 | rsi_array = np.array(self.df['rsi'].tail(20).array) 109 | close_array = np.array(self.df['close'].tail(20).array) 110 | rsi_peaks, _ = scipy.signal.find_peaks(rsi_array) 111 | rsi_troughs, _ = scipy.signal.find_peaks(-rsi_array) 112 | original_index = len(close_array) 113 | indices = np.array([]) 114 | 115 | # bearish divergence confirmed: rsi formed lower peak while price formed higher peak 116 | if 70 <= rsi_array[rsi_peaks[-2]] >= rsi_array[rsi_peaks[-1]] >= rsi_array[-2] >= rsi_array[-1]: 117 | if close_array[rsi_peaks[-2]] <= close_array[rsi_peaks[-1]]: 118 | close_array = np.array([close_array[rsi_peaks[-2]], close_array[rsi_peaks[-1]]]) 119 | rsi_array = np.array([rsi_array[rsi_peaks[-2]], rsi_array[rsi_peaks[-1]]]) 120 | indices = np.array([rsi_peaks[-2], rsi_peaks[-1]]) 121 | self.rsi_div_dict['confirmed bearish divergence'] = True 122 | 123 | # possible bearish divergence: rsi forming lower peak while price forming higher peak 124 | elif 70 <= rsi_array[rsi_peaks[-1]] >= rsi_array[-2] > rsi_array[-1]: 125 | if close_array[rsi_peaks[-1]] <= close_array[-1]: 126 | close_array = np.array([close_array[rsi_peaks[-1]], close_array[-1]]) 127 | rsi_array = np.array([rsi_array[rsi_peaks[-1]], rsi_array[-1]]) 128 | indices = np.array([rsi_peaks[-1], original_index]) 129 | self.rsi_div_dict['possible bearish divergence'] = True 130 | 131 | # bullish divergence confirmed: rsi formed higher trough while price formed lower trough 132 | elif 30 >= rsi_array[rsi_troughs[-2]] <= rsi_array[rsi_troughs[-1]] <= rsi_array[-2] <= rsi_array[-1]: 133 | if close_array[rsi_troughs[-2]] >= close_array[rsi_troughs[-1]]: 134 | close_array = np.array([close_array[rsi_troughs[-2]], close_array[rsi_troughs[-1]]]) 135 | rsi_array = np.array([rsi_array[rsi_troughs[-2]], rsi_array[rsi_troughs[-1]]]) 136 | indices = np.array([rsi_troughs[-2], rsi_troughs[-1]]) 137 | self.rsi_div_dict['confirmed bullish divergence'] = True 138 | 139 | # possible bullish divergence: rsi forming higher trough while price forming lower trough 140 | elif 30 >= rsi_array[rsi_troughs[-1]] <= rsi_array[-2] < rsi_array[-1]: 141 | if close_array[rsi_troughs[-1]] >= close_array[-1]: 142 | close_array = np.array([close_array[rsi_troughs[-1]], close_array[-1]]) 143 | rsi_array = np.array([rsi_array[rsi_troughs[-1]], rsi_array[-1]]) 144 | indices = np.array([rsi_troughs[-1], original_index]) 145 | self.rsi_div_dict['possible bullish divergence'] = True 146 | 147 | return self.rsi_div_dict, close_array, rsi_array, indices 148 | 149 | def rsi_overbought_oversold(self, o_s=30, o_b=70): 150 | rsi_array = self.df['rsi'].array 151 | if rsi_array[-3] <= o_s <= rsi_array[-2]: 152 | self.rsi_ob_os_dict['oversold'] = True 153 | elif rsi_array[-3] >= o_b >= rsi_array[-2]: 154 | self.rsi_ob_os_dict['overbought'] = True 155 | return self.rsi_ob_os_dict 156 | 157 | def macd_signals(self): 158 | if self.df['MACD_12_26_9'].array[-2] > self.df['MACDs_12_26_9'].array[-2]: 159 | if self.df['MACD_12_26_9'].array[-3] < self.df['MACDs_12_26_9'].array[-3]: 160 | self.macd_dict['MACD cross'] = True 161 | elif self.df['MACD_12_26_9'].array[-2] < self.df['MACDs_12_26_9'].array[-2]: 162 | if self.df['MACD_12_26_9'].array[-3] > self.df['MACDs_12_26_9'].array[-3]: 163 | self.macd_dict['MACD cross'] = False 164 | if (self.df['MACD_12_26_9'].array[-2], self.df['MACDs_12_26_9'].array[-2]) > (0, 0): 165 | if (self.df['MACD_12_26_9'].array[-3], self.df['MACDs_12_26_9'].array[-3]) <= (0, 0): 166 | self.macd_dict['MACD 0 cross'] = True 167 | elif (self.df['MACD_12_26_9'].array[-2], self.df['MACDs_12_26_9'].array[-2]) < (0, 0): 168 | if (self.df['MACD_12_26_9'].array[-3], self.df['MACDs_12_26_9'].array[-3]) >= (0, 0): 169 | self.macd_dict['MACD 0 cross'] = False 170 | if self.df['MACD_12_26_9'].array[-1] > self.df['MACDs_12_26_9'].array[-1]: 171 | self.macd_dict['MACD up'] = True 172 | else: 173 | self.macd_dict['MACD up'] = False 174 | 175 | def ema_signals(self): 176 | ema_200 = self.df['ema_200'].array[-3:] 177 | ema_50 = self.df['ema_50'].array[-3:] 178 | ema_20 = self.df['ema_20'].array[-3:] 179 | price = self.df['close'].array[-3:] 180 | if ema_200[0] > price[0] and ema_200[1] >= price[1] and ema_200[2] < price[2]: 181 | self.ema_signals_dict['Price crossing EMA200'] = True 182 | elif ema_200[0] < price[0] and ema_200[1] <= price[1] and ema_200[2] > price[2]: 183 | self.ema_signals_dict['Price crossing EMA200'] = False 184 | if ema_20[0] > ema_50[0] and ema_20[1] >= ema_50[1] and ema_20[2] < ema_50[2]: 185 | self.ema_signals_dict['EMA20 crossing EMA50'] = False 186 | elif ema_20[0] < ema_50[0] and ema_20[1] <= ema_50[1] and ema_20[2] > ema_50[2]: 187 | self.ema_signals_dict['EMA20 crossing EMA50'] = True 188 | if ema_50[0] > ema_200[0] and ema_50[1] >= ema_200[1] and ema_50[2] < ema_200[2]: 189 | self.ema_signals_dict['EMA50 crossing EMA200'] = False 190 | elif ema_50[0] < ema_200[0] and ema_50[1] <= ema_200[1] and ema_50[2] > ema_200[2]: 191 | self.ema_signals_dict['EMA50 crossing EMA200'] = True 192 | return self.ema_signals_dict 193 | 194 | def vol_rise_fall(self): 195 | recent_vol = self.df.volume.tail(3).array 196 | self.vol_signal = True if recent_vol[0] < recent_vol[1] < recent_vol[2] else False 197 | return self.vol_signal 198 | 199 | def large_vol_candle(self): 200 | self.vol_candle = True if self.df.volume.array[-1] >= self.df.volume.tail(14).values.mean()*2 else False 201 | return self.vol_candle 202 | 203 | if __name__ == '__main__': 204 | x = datetime.now() 205 | df = CoinData.get_dataframe('BTCUSDT', '15m') 206 | print(Signals.get_heiken_ashi(df)) 207 | print(datetime.now() - x) -------------------------------------------------------------------------------- /coin_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | import time 4 | from datetime import datetime, timedelta 5 | from threading import Thread 6 | 7 | import pandas as pd 8 | import numpy as np 9 | import scipy.signal 10 | from binance.websockets import BinanceSocketManager 11 | from twisted.internet import reactor 12 | 13 | from trader import Trader 14 | from utils import get_popular_coins 15 | 16 | client = Trader().client 17 | file_path = os.path.abspath(os.path.dirname(__file__)) 18 | os.chdir(file_path) 19 | 20 | data_path = os.path.dirname(os.path.abspath(__file__)) 21 | data_path = os.path.join(data_path, 'data') 22 | 23 | 24 | NUMBER_OF_SYMBOLS = 50 25 | 26 | 27 | class CoinData: 28 | def __init__(self): 29 | """Get most popular symbols, download historical data, start live data web socket""" 30 | # os.system('cls' if os.name == 'nt' else 'clear') 31 | print('Getting symbol list') 32 | self.symbols = get_popular_coins() # [:NUMBER_OF_SYMBOLS] 33 | self.bad_symbols = [] 34 | self.intervals = ['1m', '15m', '1h', '4h'] # '1m', 35 | self.latest_klines = {} 36 | self.data_dict = {} 37 | for s in self.symbols: 38 | self.data_dict[s] = {} 39 | self.latest_klines[s] = {} 40 | for interval in self.intervals: 41 | self.data_dict[s][interval] = [] 42 | self.latest_klines[s][interval] = {} 43 | 44 | self.tf_dict = { 45 | '1m': 1, 46 | '15m': 15, 47 | '1h': 60, 48 | '4h': 240, 49 | } 50 | self.bsm = BinanceSocketManager(client) 51 | self.conn_key = self.bsm.start_multiplex_socket(self.get_streams(), self.get_data) 52 | self.shutdown = False 53 | self.t = Thread(target=self.websocket_loop) 54 | self.t.setDaemon(True) 55 | self.t.start() 56 | self.create_database() 57 | self.adjust_symbols() 58 | print('Coin data initialized') 59 | self.most_volatile_symbols = self.return_most_volatile() 60 | 61 | def adjust_symbols(self): 62 | for symbol in self.bad_symbols: 63 | self.symbols.remove(symbol) 64 | 65 | def get_data(self, msg): 66 | static = msg 67 | if not self.latest_klines[static['data']['k']['s']][static['data']['k']['i']]: 68 | self.latest_klines[static['data']['k']['s']][static['data']['k']['i']] = static['data']['k'] 69 | elif datetime.fromtimestamp(static['data']['k']['t']/1000) >= datetime.fromtimestamp(self.latest_klines[static['data']['k']['s']][static['data']['k']['i']]['t']/1000): 70 | self.latest_klines[static['data']['k']['s']][static['data']['k']['i']] = static['data']['k'] 71 | else: 72 | print('______WARNING______') 73 | print('caught irrelevant timestamp: ' + datetime.fromtimestamp(static['data']['k']['t']/1000).strftime('%H:%M:%S')) 74 | print(static['data']['k']['s'] + static['data']['k']['i']) 75 | 76 | @staticmethod 77 | def get_dataframe(symbol, interval): 78 | if not symbol[0].isalpha(): 79 | symbol = symbol[1:] 80 | conn = sqlite3.connect('symbols.db') 81 | try: 82 | df = pd.read_sql_query(f'SELECT * FROM {symbol}_{interval}', conn) 83 | df.date = pd.to_datetime(df.date) 84 | df.set_index('date', inplace=True) 85 | df.rename_axis('date', inplace=True) 86 | finally: 87 | conn.close() 88 | return df 89 | 90 | @staticmethod 91 | def volatility(df): 92 | df = df.tail(16) 93 | close_array = np.array(df['close'].tail(40).array) 94 | peaks, _peaks = scipy.signal.find_peaks(close_array) 95 | troughs, _troughs = scipy.signal.find_peaks(-close_array) 96 | if close_array[peaks][0] < close_array[peaks][-1]: 97 | divisor = max(close_array[peaks]) 98 | else: 99 | divisor = min(close_array[troughs]) 100 | return (max(close_array[peaks]) - min(close_array[troughs])) / divisor 101 | 102 | def return_most_volatile(self, n=10): 103 | volatities = [] 104 | for symbol in self.symbols: 105 | 106 | volatities.append((symbol, self.volatility(self.get_dataframe(symbol, '15m')))) 107 | volatities = sorted(volatities, key=lambda x: x[1], reverse=True) 108 | return volatities[:n] 109 | 110 | def create_database(self): 111 | if os.path.isfile('symbols.db'): 112 | conn = sqlite3.connect('symbols.db') 113 | cursor = conn.cursor() 114 | tabs = {tab[0] for tab in cursor.execute("select name from sqlite_master where type = 'table'").fetchall()} 115 | if tabs: 116 | time = cursor.execute('SELECT MAX(date) FROM BTCUSDT_15m').fetchone()[0] 117 | if time and datetime.strptime(time, '%Y-%m-%d %H:%M:%S') >= datetime.now() - timedelta(minutes=30): 118 | for symbol in self.symbols: 119 | if self.check_symbol(symbol + '_15m') in tabs: 120 | continue 121 | else: 122 | print(f'{symbol} not in tabs') 123 | os.remove('symbols.db') 124 | self.create_database() 125 | break 126 | return 127 | else: 128 | print(f'{time} is too old') 129 | os.remove('symbols.db') 130 | self.create_database() 131 | else: 132 | print('no tables in database') 133 | os.remove('symbols.db') 134 | self.create_database() 135 | else: 136 | print('recreating database from scratch') 137 | conn = sqlite3.connect('symbols.db') 138 | cursor = conn.cursor() 139 | try: 140 | for symbol in self.symbols: 141 | for interval in self.intervals: 142 | safe_symbol = self.check_symbol(symbol) 143 | query = f'CREATE TABLE {safe_symbol}_{interval} ' \ 144 | f'(date datetime, open dec(6, 8), ' \ 145 | f'high dec(6, 8), low dec(6, ' \ 146 | f'8), close dec(' \ 147 | f'6, 8), volume dec(12, 2))' 148 | try: 149 | cursor.execute(query) 150 | except sqlite3.OperationalError as e: 151 | if str(e)[-6:] == 'exists': 152 | continue 153 | else: 154 | raise e 155 | print('created tables for ' + ', '.join(self.symbols)) 156 | self.save_original_data() 157 | finally: 158 | conn.commit() 159 | conn.close() 160 | 161 | def check_symbol(self, symbol): 162 | safe_symbol = symbol 163 | if not symbol[0].isalpha(): 164 | safe_symbol = symbol[1:] 165 | self.check_symbol(safe_symbol) 166 | return safe_symbol 167 | 168 | def save_original_data(self): 169 | print('Downloading historical data') 170 | conn = sqlite3.connect('symbols.db') 171 | cursor = conn.cursor() 172 | try: 173 | for symbol in self.symbols: 174 | for interval in self.intervals: 175 | data = client.futures_klines(symbol=symbol, interval=interval, requests_params={'timeout': 20}) 176 | if len(data) < 200: 177 | safe_symbol = self.check_symbol(symbol) 178 | query = f'DROP TABLE {safe_symbol}_{interval}' 179 | cursor.execute(query) 180 | print(f'Dropped {symbol} (not enough data)') 181 | self.bad_symbols.append(symbol) 182 | break 183 | else: 184 | for kline in data: 185 | row = [datetime.fromtimestamp(kline[0] / 1000), 186 | float(kline[1]), 187 | float(kline[2]), 188 | float(kline[3]), 189 | float(kline[4]), 190 | float(kline[7])] 191 | safe_symbol = self.check_symbol(symbol) 192 | query = f'''INSERT INTO {safe_symbol}_{interval} VALUES 193 | ("{row[0]}", {row[1]}, {row[2]}, {row[3]}, {row[4]}, {row[5]})''' 194 | cursor.execute(query) 195 | finally: 196 | conn.commit() 197 | conn.close() 198 | 199 | def save_latest_data(self): 200 | conn = sqlite3.connect('symbols.db') 201 | cursor = conn.cursor() 202 | try: 203 | for symbol in self.symbols: 204 | for interval in self.intervals: 205 | if self.latest_klines[symbol][interval]: 206 | new_row = [datetime.fromtimestamp(self.latest_klines[symbol][interval]['t'] / 1000), 207 | self.latest_klines[symbol][interval]['o'], 208 | self.latest_klines[symbol][interval]['h'], 209 | self.latest_klines[symbol][interval]['l'], 210 | self.latest_klines[symbol][interval]['c'], 211 | self.latest_klines[symbol][interval]['q']] 212 | safe_symbol = self.check_symbol(symbol) 213 | date_query= f'SELECT MAX(date) FROM {safe_symbol}_{interval}' 214 | old_row_date = cursor.execute(date_query).fetchone()[0] 215 | if str(old_row_date) == str(new_row[0]): 216 | query = f'UPDATE {safe_symbol}_{interval} SET open = {new_row[1]}, high = {new_row[2]}, low = {new_row[3]}, close = {new_row[4]}, volume = {new_row[5]} WHERE date = (SELECT MAX(date) FROM {safe_symbol}_{interval})' 217 | else: 218 | query = f'''INSERT INTO {safe_symbol}_{interval} VALUES 219 | ("{new_row[0]}", {new_row[1]}, {new_row[2]}, {new_row[3]}, {new_row[4]}, {new_row[5]})''' 220 | cursor.execute(query) 221 | finally: 222 | conn.commit() 223 | conn.close() 224 | 225 | def get_streams(self): 226 | streams = [] 227 | for symbol in self.symbols: 228 | for interval in self.intervals: 229 | streams += [f'{symbol.lower()}@kline_{interval.lower()}'] 230 | return streams 231 | 232 | def websocket_loop(self): 233 | try: 234 | self.bsm.start() 235 | while True: 236 | time.sleep(1) 237 | except Exception as e: 238 | print(e) 239 | finally: 240 | self.bsm_tear_down() 241 | 242 | def bsm_tear_down(self): 243 | self.bsm.stop_socket(self.conn_key) 244 | reactor.stop() 245 | print('bsm tear down success') 246 | 247 | 248 | if __name__ == "__main__": 249 | c = CoinData() 250 | while True: 251 | c.save_latest_data() 252 | time.sleep(5) 253 | -------------------------------------------------------------------------------- /analysis/chart.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import datetime 3 | import io 4 | import os 5 | import sys 6 | 7 | import matplotlib 8 | 9 | matplotlib.use('agg') 10 | 11 | import mplfinance as mpf 12 | import pandas as pd 13 | import pandas_ta as ta 14 | 15 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas 16 | 17 | from matplotlib import pyplot as plt 18 | 19 | from coin_data import CoinData 20 | 21 | plt.style.use('dark_background') 22 | import numpy as np 23 | import numba as nb 24 | import csv 25 | 26 | from binance.client import Client 27 | 28 | client = Client(os.getenv('bbot_pub'), os.getenv('bbot_sec')) 29 | 30 | data_dict = {} 31 | 32 | def read_data(): 33 | with open('recent_trades.csv', newline='') as f: 34 | trade_reader = csv.reader(f, delimiter=' ', quotechar='|') 35 | for row in trade_reader: 36 | if row: 37 | # print() 38 | if row[1] not in data_dict.keys(): 39 | data_dict[row[1]] = [[row[0]] + row[2:]] 40 | else: 41 | data_dict[row[1]].append([row[0]] + row[2:]) 42 | return data_dict 43 | 44 | 45 | @nb.jit(fastmath=True, nopython=True) 46 | def calc_rsi( array, deltas, avg_gain, avg_loss, n ): 47 | 48 | # Use Wilder smoothing method 49 | up = lambda x: x if x > 0 else 0 50 | down = lambda x: -x if x < 0 else 0 51 | i = n+1 52 | for d in deltas[n+1:]: 53 | avg_gain = ((avg_gain * (n-1)) + up(d)) / n 54 | avg_loss = ((avg_loss * (n-1)) + down(d)) / n 55 | if avg_loss != 0: 56 | rs = avg_gain / avg_loss 57 | array[i] = 100 - (100 / (1 + rs)) 58 | else: 59 | array[i] = 100 60 | i += 1 61 | 62 | return array 63 | 64 | def get_rsi( array, n = 14 ): 65 | 66 | deltas = np.append([0],np.diff(array)) 67 | 68 | avg_gain = np.sum(deltas[1:n+1].clip(min=0)) / n 69 | avg_loss = -np.sum(deltas[1:n+1].clip(max=0)) / n 70 | 71 | array = np.empty(deltas.shape[0]) 72 | array.fill(np.nan) 73 | 74 | array = calc_rsi( array, deltas, avg_gain, avg_loss, n ) 75 | return array 76 | 77 | class Charts: 78 | 79 | def __init__(self, symbol='BTCUSDT', tf='1h', entry=None, direction=None): 80 | """When initialised, creates chart for given symbol and timeframe 81 | :param symbol: str uppercase eg. 'BTCUSDT' 82 | :param tf: str lowercase eg. '1m', '15m', '1h', '4h' 83 | :param entry: float if open positions 84 | :param direction: str 'LONG', 'SHORT' or None depending on position""" 85 | self.symbol = symbol.upper() 86 | self.tf = tf 87 | self.df = CoinData.get_dataframe(self.symbol, self.tf) 88 | self.df = self.df_ta() 89 | self.entry = entry 90 | self.direction = True if direction == 'LONG' else False 91 | self.tp = 0.03 92 | self.sl = 0.01 93 | 94 | def df_ta(self) -> pd.DataFrame: 95 | df = self.df 96 | # df['rsi'] = ta.rsi(df.close, 14) 97 | df['rsi'] = get_rsi(df.close, 14) 98 | df = pd.concat((df, ta.macd(df.close, 12, 26, 9)), axis=1) 99 | df['ema_20'], df['ema_50'] = ta.ema(df.close, 20), ta.ema(df.close, 50) 100 | if len(df) >= 288: 101 | df['ema_200'] = ta.ema(df.close, 200) 102 | else: 103 | df['ema_200'] = ta.ema(df.close, len(df.close) - 3) 104 | df = df.tail(88) 105 | return df 106 | 107 | def trades_series(self, symbol): 108 | d = read_data() 109 | for key in d.keys(): 110 | if key == symbol: 111 | trades = [{'datetime': datetime.datetime.strptime(d[symbol][i][0], "%Y-%m-%d %H:%M:%S.%f"), 'price': d[symbol][i][2]} for i in range(len(d[symbol]))] 112 | df = pd.DataFrame(trades) 113 | df.set_index('datetime', inplace=True) 114 | df.rename_axis('date', inplace=True) 115 | return df 116 | 117 | @staticmethod 118 | def get_rsi_timeseries(prices, n=14): 119 | # RSI = 100 - (100 / (1 + RS)) 120 | # where RS = (Wilder-smoothed n-period average of gains / Wilder-smoothed n-period average of -losses) 121 | # Note that losses above should be positive values 122 | # Wilder-smoothing = ((previous smoothed avg * (n-1)) + current value to average) / n 123 | # For the very first "previous smoothed avg" (aka the seed value), we start with a straight average. 124 | # Therefore, our first RSI value will be for the n+2nd period: 125 | # 0: first delta is nan 126 | # 1: 127 | # ... 128 | # n: lookback period for first Wilder smoothing seed value 129 | # n+1: first RSI 130 | 131 | # First, calculate the gain or loss from one price to the next. The first value is nan so replace with 0. 132 | deltas = (prices - prices.shift(1)).fillna(0) 133 | 134 | # Calculate the straight average seed values. 135 | # The first delta is always zero, so we will use a slice of the first n deltas starting at 1, 136 | # and filter only deltas > 0 to get gains and deltas < 0 to get losses 137 | avg_of_gains = deltas[1:n + 1][deltas > 0].sum() / n 138 | avg_of_losses = -deltas[1:n + 1][deltas < 0].sum() / n 139 | 140 | # Set up pd.Series container for RSI values 141 | rsi_series = pd.Series(0.0, deltas.index) 142 | 143 | # Now calculate RSI using the Wilder smoothing method, starting with n+1 delta. 144 | up = lambda x: x if x > 0 else 0 145 | down = lambda x: -x if x < 0 else 0 146 | i = n + 1 147 | for d in deltas[n + 1:]: 148 | avg_of_gains = ((avg_of_gains * (n - 1)) + up(d)) / n 149 | avg_of_losses = ((avg_of_losses * (n - 1)) + down(d)) / n 150 | if avg_of_losses != 0: 151 | rs = avg_of_gains / avg_of_losses 152 | rsi_series[i] = 100 - (100 / (1 + rs)) 153 | else: 154 | rsi_series[i] = 100 155 | i += 1 156 | 157 | return rsi_series 158 | 159 | 160 | def main_chart(self): 161 | fig, axes = plt.subplots(nrows=3, ncols=1, gridspec_kw={'height_ratios': [3, 1, 1]}) 162 | fig.suptitle(f"{self.symbol} {self.tf}", fontsize=16) 163 | ax_r = axes[0].twinx() 164 | mc = mpf.make_marketcolors(up='#00e600', down='#ff0066', 165 | edge={'up': '#00e600', 'down': '#ff0066'}, 166 | wick={'up': '#00e600', 'down': '#ff0066'}, 167 | volume={'up': '#808080', 'down': '#4d4d4d'}, 168 | ohlc='black') 169 | s = mpf.make_mpf_style(marketcolors=mc) 170 | ax_r.set_alpha(0.01) 171 | axes[0].set_zorder(2) 172 | for ax in axes: 173 | ax.set_facecolor((0, 0, 0, 0)) 174 | ax_r.set_zorder(1) 175 | 176 | axes[1].set_ylabel('RSI') 177 | axes[1].margins(x=0, y=0.1) 178 | axes[0].margins(x=0, y=0.05) 179 | axes[2].set_ylabel('MACD') 180 | ax_r.set_ylabel('') 181 | ax_r.yaxis.set_visible(False) 182 | axes[2].margins(0, 0.05) 183 | axes[0].xaxis.set_visible(False) 184 | axes[1].xaxis.set_visible(False) 185 | 186 | axes[0].yaxis.tick_left() 187 | axes[0].yaxis.set_label_position('right') 188 | axes[1].yaxis.set_label_position('right') 189 | axes[2].yaxis.set_label_position('right') 190 | plt.tight_layout() 191 | fig.autofmt_xdate() 192 | self.df.volume = self.df.volume.div(2) 193 | addplot_200 = mpf.make_addplot(self.df['ema_200'], type='line', ax=axes[0], width=1, color='#ff0066') 194 | addplot_50 = mpf.make_addplot(self.df['ema_50'], type='line', ax=axes[0], width=1, color='#00e600') 195 | addplot_trades = mpf.make_addplot(self.trades_series(self.symbol), type='scatter', ax=axes[0], width=5, color='#fff') 196 | mpf.plot(self.df, ax=axes[0], type="candle", style=s, volume=ax_r, ylabel='', addplot=[addplot_200, addplot_50]) 197 | max_vol = max({y for index, y in self.df.volume.items()}) 198 | ax_r.axis(ymin=0, ymax=max_vol * 3) 199 | self.df['rsi'].plot(ax=axes[1], legend=False, use_index=True, sharex=axes[0], color='#00e600') 200 | self.df['MACD_12_26_9'].plot(ax=axes[2], legend=False, use_index=True, sharex=axes[0], color='#00e600') 201 | self.df['MACDs_12_26_9'].plot(ax=axes[2], legend=False, use_index=True, sharex=axes[0], color='#ff0066') 202 | axes[2].axhline(0, color='gray', ls='--', linewidth=1) 203 | axes[1].axhline(70, color='gray', ls='--', linewidth=1) 204 | axes[1].axhline(30, color='gray', ls='--', linewidth=1) 205 | if self.entry: 206 | tp = self.entry + self.entry * self.tp if self.direction else self.entry - self.entry * self.tp 207 | sl = self.entry - self.entry * self.sl if self.direction else self.entry + self.entry * self.sl 208 | tp_color = 'red' if self.direction else 'green' 209 | sl_color = 'red' if not self.direction else 'green' 210 | axes[0].axhline(self.entry, color='yellow', ls="--", linewidth=.5) 211 | axes[0].axhline(tp, color=tp_color, ls="--", linewidth=.5) 212 | axes[0].axhline(sl, color=sl_color, ls="--", linewidth=.5) 213 | axes[2].set_xlabel('') 214 | img = io.BytesIO() 215 | FigureCanvas(fig).print_png(img) 216 | plot_url = base64.b64encode(img.getvalue()).decode() 217 | fig.savefig('plot.png', format='png') 218 | plt.close(fig) 219 | return plot_url 220 | 221 | # def plot_rsi_div(self): 222 | # rsi_array = np.array(self.df['rsi'].tail(20).array) 223 | # close_array = np.array(self.df['close'].tail(20).array) 224 | # rsi_peaks, _ = scipy.signal.find_peaks(rsi_array) 225 | # rsi_troughs, _ = scipy.signal.find_peaks(-rsi_array) 226 | # fig, (ax1, ax2) = plt.subplots(2, sharex=True) 227 | # fig.suptitle(f'{self.symbol} RSI Divergence {self.tf}') 228 | # ax1.set_ylabel('Close') 229 | # ax2.set_ylabel('RSI') 230 | # ax2.axhline(70, color='gray', ls='--') 231 | # ax2.axhline(30, color='gray', ls='--') 232 | # ax1.xaxis.set_visible(False) 233 | # ax2.xaxis.set_visible(False) 234 | # ax1.plot(close_array) 235 | # ax2.plot(rsi_array, color='green') 236 | # ax1.plot(rsi_peaks, close_array[rsi_peaks], '.', color="#ff0066") 237 | # ax2.plot(rsi_peaks, rsi_array[rsi_peaks], '.', color="#ff0066") 238 | # ax1.plot(rsi_troughs, close_array[rsi_troughs], '.', color="#00e600") 239 | # ax2.plot(rsi_troughs, rsi_array[rsi_troughs], '.', color="#00e600") 240 | # _, new_close_array, new_rsi_array, indices = self.rsi_divergence() 241 | # if len(close_array) != len(new_close_array): 242 | # ax1.plot(indices, new_close_array, color="#ff0066") 243 | # ax2.plot(indices, new_rsi_array, color="#ff0066") 244 | # img = io.BytesIO() 245 | # fig.savefig(img, format='png') 246 | # img.seek(0) 247 | # plot_url = base64.b64encode(img.getvalue()).decode() 248 | # plt.close() 249 | # return plot_url 250 | 251 | def plot_charts(self): 252 | self.main_chart() 253 | # self.plot_rsi_div() 254 | 255 | 256 | if __name__ == '__main__': 257 | c = Charts('SNXUSDT', '15m') 258 | print('plotting charts') 259 | c.plot_charts() 260 | print('done') 261 | sys.exit() 262 | # print(Charts('SNXUSDT', '15m').trades_series('SNXUSDT')) 263 | -------------------------------------------------------------------------------- /trader.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import http 3 | import json 4 | import logging 5 | import os 6 | import sqlite3 7 | import time 8 | import stdiomask 9 | from datetime import datetime 10 | from math import fabs 11 | from threading import Thread 12 | from binance.client import Client 13 | from binance.exceptions import BinanceAPIException, BinanceOrderException 14 | from binance.websockets import BinanceSocketManager 15 | from http.client import RemoteDisconnected 16 | from urllib3.exceptions import ProtocolError 17 | from requests.exceptions import ConnectionError 18 | 19 | file_path = os.path.abspath(os.path.dirname(__file__)) 20 | os.chdir(file_path) 21 | data_path = os.path.dirname(os.path.abspath(__file__)) 22 | data_path = os.path.join(data_path, 'data') 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def check_symbol(symbol): 28 | safe_symbol = symbol 29 | if not symbol[0].isalpha(): 30 | safe_symbol = symbol[1:] 31 | check_symbol(safe_symbol) 32 | return safe_symbol 33 | 34 | 35 | def setup(): 36 | """Initial setup getting user input""" 37 | # os.system('cls' if os.name == 'nt' else 'clear') 38 | print('Welcome to perpSniper v0.2 *alpha version, not financial advice, use at your own risk!') 39 | print('Initial setup needed....') 40 | print('\n\n\n\n') 41 | settings = {'api_key': input('api_key: '), 'api_secret': stdiomask.getpass('api_secret: '), 42 | 'sl': float(input('stop loss percentage (price %, e.g. 0.5): ')) / 100, 43 | 'tp': float(input('take profit percentage (price %, e.g. 2.5): ')) / 100, 44 | 'db': float(input('trailing stop drawback (price %, e.g. 0.1): ')), 45 | 'qty': float(input('percentage stake (margin balance %, e.g. 5: ')) / 100} 46 | with open('settings.json', 'w') as f: 47 | json.dump(settings, f) 48 | 49 | 50 | def get_settings(): 51 | """Check for and get settings""" 52 | try: 53 | keycheck = ['api_key', 'api_secret', 'sl', 'tp', 'db', 'qty'] 54 | # file_path = os.path.dirname(__file__) 55 | # file = os.path.join(file_path, 'settings.json') 56 | with open('settings.json', 'r') as f: 57 | settings = json.load(f) 58 | for key in keycheck: 59 | if key not in settings.keys(): 60 | raise KeyError 61 | return settings 62 | except (FileNotFoundError, KeyError) as e: 63 | print(e) 64 | setup() 65 | return get_settings() 66 | 67 | 68 | class Trade: 69 | def __init__(self, 70 | symbol: str, 71 | direction: bool, 72 | quantity: float, 73 | tp: float, 74 | sl: float, 75 | db: float, 76 | info: dict, 77 | trader, 78 | perpetual=False): 79 | """ 80 | Create long or short trade. 81 | :param symbol: str denoting symbol pair to be traded, e.g. 'BTCUSDT' 82 | :param direction: bool indicating if trade direction is long (True) or short (False) 83 | :param quantity: float percentage of balance to be traded 84 | :param tp: float take profit activation price 85 | :param sl: float stop loss stop price 86 | :param db: float callback/drawback rate for trailing tp 87 | :param info: dict symbol exchange information such as 'pricePrecision' 88 | :param trader: Trader class object with Binance api client 89 | """ 90 | self.date = datetime.now() 91 | self.symbol = symbol 92 | self.direction = direction 93 | # self.price = float(approx_price) 94 | self.quantity = float(quantity) 95 | self.trader = trader 96 | self.client = trader.client 97 | self.info = info 98 | self.tp = tp 99 | self.sl = sl 100 | self.db = float(db) 101 | self.price_decimals = f"{{:.{self.info['pricePrecision']}f}}" 102 | if not perpetual: 103 | self.trade() 104 | self.price = self.update_entry_price() 105 | self.stop_loss() 106 | self.take_profit() 107 | 108 | def update_entry_price(self): 109 | for position in self.trader.return_open_positions(): 110 | if position['symbol'] == self.symbol: 111 | self.price = position['entry'] 112 | return self.price 113 | time.sleep(0.1) 114 | self.update_entry_price() 115 | 116 | def trade(self): 117 | order_type = 'MARKET' 118 | side = 'BUY' if self.direction else 'SELL' 119 | try: 120 | self.client.futures_create_order( 121 | type=order_type, 122 | side=side, 123 | quantity=self.quantity, 124 | symbol=self.symbol, 125 | ) 126 | except (BinanceAPIException, BinanceOrderException) as e: 127 | raise e 128 | 129 | def take_profit(self): 130 | order_type = 'TRAILING_STOP_MARKET' 131 | side = 'SELL' if self.direction else 'BUY' 132 | if self.direction: 133 | stop_price = float(self.price_decimals.format(self.price + (self.price * self.tp))) 134 | else: 135 | stop_price = float(self.price_decimals.format(self.price - (self.price * self.tp))) 136 | try: 137 | self.client.futures_create_order( 138 | type=order_type, 139 | side=side, 140 | quantity=self.quantity, 141 | reduceOnly=True, 142 | workingType='MARK_PRICE', 143 | symbol=self.symbol, 144 | activationPrice=stop_price, 145 | callbackRate=self.db 146 | ) 147 | except (BinanceAPIException, BinanceOrderException) as e: 148 | raise e 149 | 150 | def stop_loss(self): 151 | order_type = 'STOP_MARKET' 152 | side = 'SELL' if self.direction else 'BUY' 153 | if self.direction: 154 | stop_price = float(self.price_decimals.format(self.price - (self.price * self.sl))) 155 | else: 156 | stop_price = float(self.price_decimals.format(self.price + (self.price * self.sl))) 157 | try: 158 | self.client.futures_create_order( 159 | type=order_type, 160 | side=side, 161 | quantity=self.quantity, 162 | reduceOnly=True, 163 | symbol=self.symbol, 164 | stopPrice=stop_price, 165 | workingType='MARK_PRICE', 166 | ) 167 | except (BinanceAPIException, BinanceOrderException) as e: 168 | raise e 169 | 170 | 171 | class PerpetualTrade(Trade): 172 | 173 | trade_counter = 0 174 | 175 | def __init__(self, *args): 176 | super().__init__(*args, perpetual=True) 177 | if self.direction is True: 178 | self.long() 179 | elif self.direction is False: 180 | self.short() 181 | else: 182 | return 183 | self.update_entry_price() 184 | self.stop_loss() 185 | 186 | def long(self): 187 | if self.direction is True: 188 | self.trade() 189 | elif self.direction is False: 190 | self.reverse_trade() 191 | self.trade_counter += 1 192 | 193 | def short(self): 194 | if self.direction is False: 195 | self.trade() 196 | elif self.direction is True: 197 | self.reverse_trade() 198 | self.trade_counter += 1 199 | 200 | def flat(self): 201 | if self.trade_counter > 1: 202 | self.quantity /= 2 203 | self.trader.close_position(self.symbol) 204 | self.trade_counter = 0 205 | self.direction = None 206 | 207 | def reverse_trade(self): 208 | self.client.futures_cancel_all_open_orders(symbol=self.symbol) 209 | self.direction = False if self.direction else True 210 | if self.trade_counter == 1: 211 | self.quantity *= 2 212 | self.trade() 213 | try: 214 | self.update_entry_price() 215 | self.stop_loss() 216 | except (BinanceAPIException, BinanceOrderException) as e: 217 | raise e 218 | 219 | 220 | class Trader: 221 | def __init__(self): 222 | """ 223 | Set up constants, and keep track of trades 224 | """ 225 | self.threads = [] 226 | self.LEVERAGE = 20 227 | self.config = [] 228 | self.open_trades = {} 229 | self.mark_prices = {} 230 | self.open_positions_local = [] 231 | self.settings = get_settings() 232 | self.client = Client(self.settings['api_key'], self.settings['api_secret'], requests_params={'timeout': 30}) 233 | self.server_time = datetime.fromtimestamp(self.return_server_time()).strftime('%H:%M:%S') 234 | self.bsm_1 = BinanceSocketManager(self.client) 235 | 236 | def start_thread(self, func): 237 | t = Thread(target=func) 238 | t.setDaemon(True) 239 | t.start() 240 | self.threads.append(t) 241 | 242 | def stop_threads(self): 243 | for thread in self.threads: 244 | thread.join() 245 | 246 | def trade(self, symbol, direction): 247 | symbol = symbol.upper() 248 | quantity, approx_price, info = self.calculate_max_qty(symbol) 249 | try: 250 | t = Trade(symbol, 251 | direction, 252 | quantity, 253 | float(self.settings['tp']), 254 | float(self.settings['sl']), 255 | float(self.settings['db']), 256 | info, 257 | self) 258 | self.open_trades[t.date] = t 259 | except (BinanceAPIException, BinanceOrderException) as e: 260 | raise e 261 | 262 | @staticmethod 263 | def get_price(symbol): 264 | conn = sqlite3.connect('symbols.db') 265 | c = conn.cursor() 266 | symbol = check_symbol(symbol) 267 | try: 268 | q = f'SELECT * FROM {symbol}_15m WHERE date = (SELECT MAX(date) FROM {symbol}_15m)' 269 | price = c.execute(q).fetchone()[3] 270 | finally: 271 | conn.close() 272 | return price 273 | 274 | def get_account_info(self): 275 | ac_info = self.client.futures_account() 276 | maintenance = '{:.2f}'.format(float(ac_info['totalMaintMargin'])) 277 | balance = '{:.2f}'.format(float(ac_info['totalWalletBalance'])) 278 | total_pnl = '{:.2f}'.format(float(ac_info['totalUnrealizedProfit'])) 279 | margin_balance = '{:.2f}'.format(float(ac_info['totalMarginBalance'])) 280 | account_dict = { 281 | 'maintenance': maintenance, 282 | 'balance': balance, 283 | 'total_pnl': total_pnl, 284 | 'margin_balance': margin_balance 285 | } 286 | return account_dict 287 | 288 | def get_usdt_balance(self): 289 | return float(self.get_account_info()['balance']) 290 | 291 | def calculate_max_qty(self, symbol): 292 | price = float(self.get_price(symbol)) 293 | usdt_bal = self.get_usdt_balance() 294 | affordable = usdt_bal / price 295 | qty = affordable * float(self.settings['qty']) * self.LEVERAGE 296 | info = [s for s in self.client.futures_exchange_info()['symbols'] if s['symbol'] == symbol][0] 297 | qty_precision = info['quantityPrecision'] 298 | decimals = f'{{:.{qty_precision}f}}' 299 | qty = float(decimals.format(qty)) 300 | return qty, price, info 301 | 302 | def return_open_positions(self): 303 | acc = self.client.futures_account() 304 | positions = acc['positions'] 305 | position_list = [] 306 | for position in positions: 307 | if float(position['positionAmt']) > 0: 308 | roe = (float(position['unrealizedProfit']) / ( 309 | (float(position['positionAmt']) * float(position['entryPrice'])) / int( 310 | position['leverage']))) * 100 311 | direction = 'LONG' 312 | elif float(position['positionAmt']) < 0: 313 | roe = -(float(position['unrealizedProfit']) / ( 314 | (float(position['positionAmt']) * float(position['entryPrice'])) / int( 315 | position['leverage']))) * 100 316 | direction = 'SHORT' 317 | else: 318 | continue 319 | position = { 320 | 'symbol': position['symbol'], 321 | 'qty': position['positionAmt'], 322 | 'entry': float(position['entryPrice']), 323 | 'pnl': float(position['unrealizedProfit']), 324 | 'roe': roe, 325 | 'direction': direction, 326 | } 327 | position_list.append(position) 328 | return position_list 329 | 330 | def positions_set(self): 331 | return set([position['symbol'] for position in self.return_open_positions()]) 332 | 333 | def check_positions_cancel_open_orders(self): 334 | try: 335 | positions_symbols = set([position['symbol'] for position in self.return_open_positions()]) 336 | orders = self.client.futures_get_open_orders() 337 | orders_symbols = set([order['symbol'] for order in orders]) 338 | diff = orders_symbols.difference(positions_symbols) 339 | for s in diff: 340 | self.client.futures_cancel_all_open_orders(symbol=s) 341 | return orders_symbols 342 | except (RemoteDisconnected, ProtocolError, ConnectionError) as e: 343 | error = f'Binance connection error: {e}' 344 | logger.error(error) 345 | time.sleep(1) 346 | self.check_positions_cancel_open_orders() 347 | 348 | def close_position(self, symbol): 349 | for position in self.return_open_positions(): 350 | if position['symbol'] == symbol: 351 | direction = True if position['direction'] == 'LONG' else False 352 | side = 'SELL' if direction else 'BUY' 353 | qty = fabs(float(position['qty'])) 354 | self.client.futures_create_order( 355 | type='MARKET', 356 | reduceOnly=True, 357 | symbol=symbol, 358 | side=side, 359 | quantity=qty 360 | ) 361 | self.check_positions_cancel_open_orders() 362 | break 363 | 364 | def close_all_positions(self): 365 | for position in self.return_open_positions(): 366 | direction = True if position['direction'] == 'LONG' else False 367 | side = 'SELL' if direction else 'BUY' 368 | qty = fabs(float(position['qty'])) 369 | self.client.futures_create_order( 370 | type='MARKET', 371 | reduceOnly=True, 372 | symbol=position['symbol'], 373 | side=side, 374 | quantity=qty 375 | ) 376 | 377 | def return_server_time(self): 378 | time_dict = self.client.get_server_time() 379 | original_server_time = time_dict['serverTime']/1000 380 | return original_server_time 381 | 382 | def count_server_time(self, ost): 383 | while True: 384 | ost += 1 385 | time.sleep(1) 386 | self.server_time = datetime.fromtimestamp(ost).strftime('%H:%M:%S') 387 | 388 | 389 | if __name__ == '__main__': 390 | print(Trader.get_price('BTCUSDT')) 391 | -------------------------------------------------------------------------------- /async_algo_trader.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | import pickle 5 | import sys 6 | import time 7 | from datetime import datetime, timedelta 8 | 9 | import requests 10 | from apscheduler.schedulers.background import BackgroundScheduler 11 | from trader import Trader 12 | from coin_data import CoinData 13 | from signals import Signals 14 | 15 | logger = logging.getLogger(__name__) 16 | logger.setLevel(logging.DEBUG) 17 | handler = logging.StreamHandler(sys.stdout) 18 | handler.setLevel(logging.INFO) 19 | formatter = logging.Formatter('%(asctime)s - [ %(levelname)s ] - %(message)s') 20 | handler.setFormatter(formatter) 21 | logger.addHandler(handler) 22 | 23 | 24 | # Telegram details 25 | BOT_TOKEN = os.getenv('tg_debug_bot_token') 26 | CHANNEL_ID = os.getenv('tg_debug_channel') 27 | 28 | 29 | class AlgoTrader: 30 | 31 | """Check each coin for signals and make trades in certain conditions. 32 | Conditions: 33 | - 15m RSI oversold/overbought and RSI divergence, and 1h ema_50/ema_200 trend. 34 | - 15m RSI oversold/overbought in last hour and macd crossing up/down.""" 35 | 36 | data = CoinData() 37 | trader = Trader() 38 | scheduler = BackgroundScheduler() 39 | 40 | def __init__(self): 41 | self.signals_dict = {} 42 | self.trend_markers = {} 43 | self.rsi_markers = {} 44 | self.event_loop = None 45 | try: 46 | with open('pickle', 'rb') as f: 47 | data = pickle.load(f) 48 | if data['time'] >= datetime.now() - timedelta(minutes=15): 49 | self.recent_alerts = data['recent'] 50 | self.ready_symbols = data['ready'] 51 | print(f'ready symbols loaded: ' + ', '.join(self.ready_symbols)) 52 | else: 53 | raise FileNotFoundError 54 | except (FileNotFoundError, EOFError): 55 | self.recent_alerts = [] 56 | self.ready_symbols = { 57 | 'long': [], 58 | 'short': [] 59 | } 60 | self.trader = Trader() 61 | self.trader.settings['sl'] = 0.005 62 | self.trader.settings['tp'] = 0.01 63 | self.trader.settings['qty'] = 0.01 64 | self.trader.settings['db'] = 0.1 65 | self.event_loop = asyncio.get_event_loop() 66 | 67 | @staticmethod 68 | async def create_signals_instance(symbol, tf): 69 | s = Signals(symbol, tf) 70 | return s 71 | 72 | async def get_signals(self): 73 | logger.debug('Getting signals') 74 | inadequate_symbols = [] 75 | for symbol in self.data.symbols: 76 | try: 77 | m15 = asyncio.create_task(self.create_signals_instance(symbol, '15m')) 78 | h1 = asyncio.create_task(self.create_signals_instance(symbol, '1h')) 79 | h4 = asyncio.create_task(self.create_signals_instance(symbol, '4h')) 80 | self.signals_dict[symbol] = (await m15, await h1, await h4) 81 | except IndexError: 82 | inadequate_symbols.append(symbol) 83 | continue 84 | for symbol in inadequate_symbols: 85 | self.data.symbols.remove(symbol) 86 | return self.signals_dict 87 | 88 | async def record_trend(self): 89 | for symbol in self.data.symbols: 90 | signals_15m = self.signals_dict[symbol][0] 91 | signals_1h = self.signals_dict[symbol][1] 92 | signals_4h = self.signals_dict[symbol][2] 93 | h4 = True if signals_4h.df.ema_50.iloc[-1] > signals_4h.df.ema_200.iloc[-1] else False 94 | h1 = True if signals_1h.df.ema_50.iloc[-1] > signals_1h.df.ema_200.iloc[-1] else False 95 | m15 = True if signals_15m.df.ema_50.iloc[-1] > signals_15m.df.ema_200.iloc[-1] else False 96 | self.trend_markers[symbol] = (m15, h1, h4) 97 | return self.trend_markers 98 | 99 | async def purge_alerts(self): 100 | logger.debug('Purging alerts') 101 | old_alerts = [] 102 | for alert in self.recent_alerts: 103 | split_alert = alert.split(' ') 104 | if datetime.strptime(' '.join(split_alert[3:5]), '%Y-%m-%d %H:%M:%S') < datetime.now() - timedelta(minutes=45): 105 | old_alerts.append(alert) 106 | for alert in old_alerts: 107 | self.recent_alerts.remove(alert) 108 | 109 | def check_rsi_div(self, symbol): 110 | if self.signals_dict[symbol][0].rsi_div_dict['confirmed bearish divergence']: 111 | return False 112 | elif self.signals_dict[symbol][0].rsi_div_dict['confirmed bullish divergence']: 113 | return True 114 | else: 115 | return None 116 | 117 | def check_macd(self, symbol): 118 | if self.signals_dict[symbol][0].macd_dict['MACD cross'] is False or self.signals_dict[symbol][0].macd_dict['MACD 0 cross'] is False: 119 | return False 120 | elif self.signals_dict[symbol][0].macd_dict['MACD cross'] is True or self.signals_dict[symbol][0].macd_dict['MACD 0 cross'] is True: 121 | return True 122 | else: 123 | return None 124 | 125 | def check_rsi_ob_os(self, symbol): 126 | if self.signals_dict[symbol][0].rsi_ob_os_dict['overbought']: 127 | return False 128 | elif self.signals_dict[symbol][0].rsi_ob_os_dict['oversold']: 129 | return True 130 | else: 131 | return None 132 | 133 | def check_4h_trend(self, symbol): 134 | if self.trend_markers[symbol][2] and self.trend_markers[symbol][1]: 135 | return True 136 | elif not self.trend_markers[symbol][2] and not self.trend_markers[symbol][1]: 137 | return False 138 | else: 139 | return None 140 | 141 | async def rsi_ob_os_marker(self, open_positions, recent_alerts): 142 | logger.debug('Checking RSI markers') 143 | for symbol in self.signals_dict.keys(): 144 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts: 145 | if self.check_4h_trend(symbol) is True: 146 | if self.check_rsi_ob_os(symbol) is True: 147 | if self.signals_dict[symbol][0].macd_dict['MACD up']: 148 | self.ready_symbols['long'].append(symbol) 149 | alert = f'LONG {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (RSI oversold signal)' 150 | self.handle_alert(alert) 151 | else: 152 | self.rsi_markers[symbol] = (True, datetime.now()) 153 | elif self.check_4h_trend(symbol) is False: 154 | if self.check_rsi_ob_os(symbol) is False: 155 | if not self.signals_dict[symbol][0].macd_dict['MACD up']: 156 | self.ready_symbols['short'].append(symbol) 157 | alert = f'SHORT {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (RSI overbought signal)' 158 | self.handle_alert(alert) 159 | else: 160 | self.rsi_markers[symbol] = (False, datetime.now()) 161 | 162 | async def purge_rsi_markers(self): 163 | logger.debug('Purging RSI markers') 164 | old_keys = [] 165 | for key, value in self.rsi_markers.items(): 166 | if value[1] < datetime.now() - timedelta(hours=1): 167 | old_keys.append(key) 168 | if old_keys: 169 | for key in old_keys: 170 | self.rsi_markers.pop(key, None) 171 | 172 | async def rsi_div_trade(self, open_positions, recent_alerts): 173 | logger.debug('Checking RSI div') 174 | for symbol in self.signals_dict.keys(): 175 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts: 176 | if self.check_4h_trend(symbol) is True: 177 | if self.check_rsi_div(symbol) is True: 178 | self.ready_symbols['long'].append(symbol) 179 | # self.trader.trade(symbol, True) 180 | alert = f'LONG {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (RSI div signal)' 181 | self.handle_alert(alert) 182 | elif self.check_4h_trend(symbol) is False: 183 | if self.check_rsi_div(symbol) is False: 184 | self.ready_symbols['short'].append(symbol) 185 | # self.trader.trade(symbol, False) 186 | alert = f'SHORT {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (RSI div signal)' 187 | self.handle_alert(alert) 188 | 189 | async def rsi_macd_trade(self, open_positions, recent_alerts): 190 | logger.debug('Checking MACD') 191 | for symbol in self.signals_dict.keys(): 192 | if symbol in self.rsi_markers.keys(): 193 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts: 194 | if self.rsi_markers[symbol][0]: 195 | if self.check_4h_trend(symbol) is True: 196 | if self.check_macd(symbol) is True: 197 | # self.trader.trade(symbol, True) 198 | self.ready_symbols['long'].append(symbol) 199 | alert = f'LONG {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (MACD signal)' 200 | self.handle_alert(alert) 201 | else: 202 | if self.check_4h_trend(symbol) is False: 203 | if self.check_macd(symbol) is False: 204 | # self.trader.trade(symbol, False) 205 | self.ready_symbols['short'].append(symbol) 206 | alert = f'SHORT {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} (MACD signal)' 207 | self.handle_alert(alert) 208 | 209 | async def ha_long(self, open_positions, recent_alerts): 210 | trades = [] 211 | for symbol in self.ready_symbols['long']: 212 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts: 213 | if Signals.get_heiken_ashi_trend(Signals.get_heiken_ashi(CoinData.get_dataframe(symbol, '15m'))) is True: 214 | self.trader.trade(symbol, True) 215 | alert = f'LONGED {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} HEIKEN ASHI FINAL SIGNAL' 216 | trades.append(symbol) 217 | self.handle_alert(alert) 218 | for s in trades: 219 | self.ready_symbols['long'].remove(s) 220 | 221 | async def ha_short(self, open_positions, recent_alerts): 222 | trades = [] 223 | for symbol in self.ready_symbols['short']: 224 | if open_positions is not None and symbol not in open_positions and symbol not in recent_alerts: 225 | if Signals.get_heiken_ashi_trend(Signals.get_heiken_ashi(CoinData.get_dataframe(symbol, '15m'))) is False: 226 | self.trader.trade(symbol, False) 227 | alert = f'SHORTED {symbol} at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} HEIKEN ASHI FINAL SIGNAL' 228 | trades.append(symbol) 229 | self.handle_alert(alert) 230 | for s in trades: 231 | self.ready_symbols['short'].remove(s) 232 | 233 | async def check_heiken_ashi(self, open_positions, recent_alerts_symbols): 234 | logger.debug('Checking final condition (Heiken Ashi)') 235 | long_task = asyncio.create_task(self.ha_long(open_positions, recent_alerts_symbols)) 236 | short_task = asyncio.create_task(self.ha_short(open_positions, recent_alerts_symbols)) 237 | await long_task 238 | await short_task 239 | 240 | def handle_alert(self, alert): 241 | self.recent_alerts.append(alert) 242 | logger.info(alert) 243 | self.send_message(alert) 244 | 245 | def send_message(self, message): 246 | message = 'TRADING BOT ALERT: ' + message 247 | requests.get(f'https://api.telegram.org/bot{BOT_TOKEN}/sendMessage?chat_id={CHANNEL_ID}&text={message}') 248 | 249 | async def get_recent_alerts(self): 250 | return [alert.split(' ')[1] for alert in self.recent_alerts] 251 | 252 | async def get_open_positions(self): 253 | return self.trader.check_positions_cancel_open_orders() 254 | 255 | async def check_conditions(self): 256 | start_time = datetime.now() 257 | logger.debug('Starting check') 258 | # Get new signals data 259 | await self.get_signals() 260 | task0 = asyncio.create_task(self.record_trend()) 261 | task1 = asyncio.create_task(self.get_recent_alerts()) 262 | task2 = asyncio.create_task(self.get_open_positions()) 263 | # co_1 = [self.record_trend(), 264 | # self.get_recent_alerts(), 265 | # self.get_open_positions()] 266 | await task0 267 | recent_alerts_symbols = await task1 268 | open_positions = await task2 269 | 270 | log_statement = 'took: {}'.format(datetime.now() - start_time) 271 | logger.debug(log_statement) 272 | 273 | # Check trade conditions 274 | task_1 = asyncio.create_task(self.rsi_div_trade(open_positions, 275 | recent_alerts_symbols)) 276 | task_3 = asyncio.create_task(self.rsi_ob_os_marker(open_positions, 277 | recent_alerts_symbols)) 278 | task_2 = asyncio.create_task(self.rsi_macd_trade(open_positions, 279 | recent_alerts_symbols)) 280 | await task_1 281 | await task_3 282 | await task_2 283 | 284 | heiken_ashi_check = asyncio.create_task(self.check_heiken_ashi(open_positions, 285 | recent_alerts_symbols)) 286 | task_1 = asyncio.create_task(self.purge_alerts()) 287 | task_2 = asyncio.create_task(self.purge_rsi_markers()) 288 | await heiken_ashi_check 289 | await task_1 290 | await task_2 291 | 292 | if self.recent_alerts: 293 | recent = ', '.join(self.recent_alerts) 294 | logger.debug(recent) 295 | total_time = datetime.now() - start_time 296 | log_statement = 'total_time: {}'.format(total_time) 297 | logger.debug(log_statement) 298 | if self.recent_alerts or self.ready_symbols: 299 | self.debug_statements() 300 | 301 | def save_data(self): 302 | self.data.save_latest_data() 303 | 304 | def schedule_tasks(self): 305 | self.scheduler.add_job(self.save_data, trigger='cron', minute='*/1', second='2') 306 | self.scheduler.start() 307 | 308 | def stop_tasks(self): 309 | self.scheduler.remove_all_jobs() 310 | self.scheduler.shutdown() 311 | 312 | def loop(self): 313 | try: 314 | asyncio.run(self.get_signals()) 315 | self.schedule_tasks() 316 | while datetime.now().second != 3: 317 | time.sleep(1) 318 | log_statement = f'Starting mainloop at {datetime.now().strftime("%H:%M:%S")}' 319 | logger.info(log_statement) 320 | while True: 321 | asyncio.run(self.check_conditions()) 322 | while datetime.now().second != 3: 323 | time.sleep(1) 324 | except KeyboardInterrupt as e: 325 | self.stop_tasks() 326 | data = { 327 | 'recent': self.recent_alerts, 328 | 'ready': self.ready_symbols, 329 | 'time': datetime.now() 330 | } 331 | with open('pickle', 'wb') as f: 332 | pickle.dump(data, f) 333 | sys.exit() 334 | 335 | def debug_statements(self): 336 | log = 'Recent alerts: ' + ', '.join(self.recent_alerts) 337 | logger.debug(log) 338 | log = 'Ready symbols long: ' + ', '.join(self.ready_symbols['long']) 339 | logger.debug(log) 340 | log = 'Ready symbols short: ' + ', '.join(self.ready_symbols['short']) 341 | logger.debug(log) 342 | 343 | 344 | if __name__ == '__main__': 345 | at = AlgoTrader() 346 | at.loop() 347 | -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | function loadUrl(newLocation){ 2 | window.location = newLocation; 3 | return false; 4 | } 5 | 6 | function getRecentAlerts(){ 7 | var table = document.getElementById('recent_alerts_table') 8 | var thead = document.getElementById('recent_alerts') 9 | // var hotCoins = document.getElementById('hot_coins') 10 | fetch(`${window.origin}/api/signals`) 11 | .then(function(response){ 12 | if (response.status !== 200) { 13 | displayMessage(`Bad response from api: ${response.status}`) 14 | return ; 15 | } 16 | response.json().then(function(data){ 17 | var alertRows = document.getElementsByClassName('alert_row') 18 | thead.innerHTML = ''; 19 | data.signals.sort(function(a,b){ 20 | var c = new Date(a.date); 21 | var d = new Date(b.date); 22 | return c-d; 23 | }); 24 | data.signals.reverse(); 25 | for (i = 0; i < data.signals.length; i++) { 26 | var row = thead.insertRow(0); 27 | row.classList.add('alert_row') 28 | var words = data.signals[i].alert.split(' '); 29 | var bullish = ['up', 'bullish', 'oversold'] 30 | var bearish = ['down', 'bearish', 'overbought'] 31 | for (j = 0; j < words.length; j++) { 32 | for (k = 0; k < bullish.length; k++) { 33 | if (words[j] == bullish[k]) { 34 | row.classList.add('text-green-500') 35 | } 36 | } 37 | for (k = 0; k < bearish.length; k++) { 38 | if (words[j] == bearish[k]) { 39 | row.classList.add('text-red-500') 40 | } 41 | } 42 | } 43 | var cell1 = row.insertCell(0); 44 | var cell2 = row.insertCell(1); 45 | var cell3 = row.insertCell(2); 46 | cell1.innerHTML = data.signals[i].time; 47 | cell2.innerHTML = data.signals[i].symbol; 48 | cell2.classList.add('font-bold') 49 | cell2.addEventListener('click', changeChart, false); 50 | cell3.innerHTML = data.signals[i].alert; 51 | } 52 | // hot_coins.innerHTML = '' 53 | // for (i = 0; i < data.hot_coins.length; i++) { 54 | // var entry = document.createElement('li') 55 | // entry.innerHTML = `${data.hot_coins[i][0]} (${data.hot_coins[i][1]} alerts)` 56 | // hotCoins.appendChild(entry) 57 | // } 58 | return ; 59 | }) 60 | }) 61 | } 62 | 63 | 64 | function openLong() { 65 | var coin = document.getElementById('coinInput') 66 | fetch(`${window.origin}/api/long`, { 67 | method: 'post', 68 | headers: { 69 | 'Content-Type': 'application/json' 70 | // 'Content-Type': 'application/x-www-form-urlencoded', 71 | }, 72 | body: JSON.stringify({coin: coinInput.value}), 73 | }) 74 | .then(function(response){ 75 | if (response.status !== 200) { 76 | displayMessage(`Bad response from api: ${response.status}`) 77 | return ; 78 | } 79 | else {displayMessage(`Opened LONG on: ${coin.value}`)} 80 | }) 81 | } 82 | 83 | 84 | function openShort() { 85 | var coin = document.getElementById('coinInput') 86 | fetch(`${window.origin}/api/short`, { 87 | method: 'post', 88 | headers: { 89 | 'Content-Type': 'application/json' 90 | // 'Content-Type': 'application/x-www-form-urlencoded', 91 | }, 92 | body: JSON.stringify({coin: coinInput.value}), 93 | }) 94 | .then(function(response){ 95 | if (response.status !== 200) { 96 | displayMessage(`Bad response from api: ${response.status}`) 97 | return ; 98 | } 99 | else {displayMessage(`Opened SHORT on: ${coin.value}`)} 100 | }) 101 | } 102 | 103 | function openQuickShort(coin) { 104 | fetch(`${window.origin}/api/short`, { 105 | method: 'post', 106 | headers: { 107 | 'Content-Type': 'application/json' 108 | // 'Content-Type': 'application/x-www-form-urlencoded', 109 | }, 110 | body: JSON.stringify({coin: coin}), 111 | }) 112 | .then(function(response){ 113 | if (response.status !== 200) { 114 | displayMessage(`Bad response from api: ${response.status}`) 115 | return ; 116 | } 117 | else { 118 | response.json().then(function(data) { 119 | if (data.message) { 120 | displayMessage(data.message) 121 | } 122 | }) 123 | } 124 | }) 125 | } 126 | 127 | function testMessage() { 128 | displayMessage('Hello World'); 129 | } 130 | 131 | function openQuickLong(coin) { 132 | fetch(`${window.origin}/api/long`, { 133 | method: 'post', 134 | headers: { 135 | 'Content-Type': 'application/json' 136 | // 'Content-Type': 'application/x-www-form-urlencoded', 137 | }, 138 | body: JSON.stringify({coin: coin}), 139 | }) 140 | .then(function(response){ 141 | if (response.status !== 200) { 142 | displayMessage(`Bad response from api: ${response.status}`) 143 | return ; 144 | } 145 | else { 146 | response.json().then(function(data) { 147 | if (data.message) { 148 | displayMessage(data.message) 149 | } 150 | }) 151 | } 152 | }) 153 | } 154 | 155 | function shutDown() { 156 | fetch(`${window.origin}/shutdown`, { 157 | method: 'post'}) 158 | } 159 | 160 | function getPositions() { 161 | fetch(`${window.origin}/api/positions`) 162 | .then(function(response){ 163 | if (response.status !== 200) { 164 | displayMessage(`Bad response from api: ${response.status}`) 165 | return ; 166 | } 167 | response.json().then(function(data){ 168 | var tBody = document.getElementById('tBody') 169 | tBody.innerHTML = '' 170 | for (i = 0; i < data.positions.length; i++) { 171 | var row = tBody.insertRow(0); 172 | // row.classList.add('') 173 | var cell1 = row.insertCell(0); 174 | var cell2 = row.insertCell(1); 175 | var cell3 = row.insertCell(2); 176 | var cell4 = row.insertCell(3); 177 | var cell5 = row.insertCell(4); 178 | cell1.innerHTML = data.positions[i].symbol; 179 | cell1.addEventListener('click', changeChart, false); 180 | cell2.innerHTML = data.positions[i].direction; 181 | cell3.innerHTML = data.positions[i].qty; 182 | cell4.innerHTML = '$' + data.positions[i].pnl.toFixed(2); 183 | cell5.innerHTML = data.positions[i].roe.toFixed(2) +'%'; 184 | } 185 | }) 186 | }) 187 | } 188 | 189 | function getBalance() { 190 | fetch(`${window.origin}/api/account`) 191 | .then(function(response){ 192 | if (response.status !== 200) { 193 | displayMessage(`Bad response from api: ${response.status}`) 194 | return ; 195 | } 196 | response.json().then(function(data){ 197 | var tBody = document.getElementById('accountTBody') 198 | var balance = document.getElementById('balance') 199 | var pnl = document.getElementById('pnl') 200 | balance.innerHTML = 'Balance: $' + parseFloat(data.balance).toFixed(2); 201 | pnl.innerHTML = 'PNL: $' + parseFloat(data.total_pnl).toFixed(2); 202 | // tBody.innerHTML = '' 203 | // var row = tBody.insertRow(0); 204 | // row.classList.add('text-left') 205 | // var cell1 = row.insertCell(0); 206 | // var cell2 = row.insertCell(1); 207 | // var cell3 = row.insertCell(2); 208 | // cell1.innerHTML = '$' + parseFloat(data.balance).toFixed(2); 209 | // cell2.innerHTML = '$' + parseFloat(data.total_pnl).toFixed(2); 210 | // cell3.innerHTML = '$' + parseFloat(data.margin_balance).toFixed(2); 211 | }) 212 | }) 213 | } 214 | 215 | function closeOldOrders(){ 216 | fetch(`${window.origin}/api/positions`) 217 | .then(function(response){ 218 | if (response.status !== 200) { 219 | displayMessage(`Bad response from api: ${response.status}`) 220 | return ; 221 | } 222 | else { 223 | displayMessage('Orders cleared') 224 | } 225 | }) 226 | } 227 | 228 | var xDown = null; 229 | var yDown = null; 230 | 231 | function getTouches(evt) { 232 | return evt.touches || // browser API 233 | evt.originalEvent.touches; // jQuery 234 | } 235 | 236 | function handleTouchStart(evt) { 237 | const firstTouch = getTouches(evt)[0]; 238 | xDown = firstTouch.clientX; 239 | yDown = firstTouch.clientY; 240 | }; 241 | 242 | function handleTouchMove(evt) { 243 | if ( ! xDown || ! yDown ) { 244 | return; 245 | } 246 | 247 | var xUp = evt.touches[0].clientX; 248 | var yUp = evt.touches[0].clientY; 249 | 250 | var xDiff = xDown - xUp; 251 | var yDiff = yDown - yUp; 252 | 253 | var coinName = this.id 254 | var closeId = `${coinName}Close` 255 | 256 | 257 | if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) { 258 | if ( xDiff > 0 ) { 259 | $(this).css({ 260 | 'animation': 'slideSwipeDrawerOpen .2s linear', 261 | '-webkit-transform' : 'translateX(' + -6 + 'rem)', 262 | '-moz-transform' : 'translateX(' + -6 + 'rem)', 263 | '-ms-transform' : 'translateX(' + -6 + 'rem)', 264 | '-o-transform' : 'translateX(' + -6 + 'rem)', 265 | 'transform' : 'translateX(' + -6 + 'rem)' 266 | }); 267 | document.getElementById(closeId).style.display = 'flex' 268 | 269 | } else { 270 | $(this).css({ 271 | 'animation': 'slideSwipeDrawerClose .2s linear', 272 | '-webkit-transform' : 'translateX(' + 0 + 'rem)', 273 | '-moz-transform' : 'translateX(' + 0 + 'rem)', 274 | '-ms-transform' : 'translateX(' + 0 + 'rem)', 275 | '-o-transform' : 'translateX(' + 0 + 'rem)', 276 | 'transform' : 'translateX(' + 0 + 'rem)' 277 | }); 278 | document.getElementById(closeId).style.display = 'none' 279 | } 280 | } 281 | /* reset values */ 282 | xDown = null; 283 | yDown = null; 284 | }; 285 | 286 | function handleRightClick(evt) { 287 | var coinName = this.id 288 | var closeId = `${coinName}Close` 289 | evt.preventDefault() 290 | if (this.getAttribute('data-open')) { 291 | document.getElementById(closeId).style.display = 'none' 292 | $(this).css({ 293 | 'animation': 'slideSwipeDrawerClose .2s linear', 294 | '-webkit-transform' : 'translateX(' + 0 + 'rem)', 295 | '-moz-transform' : 'translateX(' + 0 + 'rem)', 296 | '-ms-transform' : 'translateX(' + 0 + 'rem)', 297 | '-o-transform' : 'translateX(' + 0 + 'rem)', 298 | 'transform' : 'translateX(' + 0 + 'rem)' 299 | }); 300 | this.removeAttribute('data-open') 301 | } 302 | else { 303 | document.getElementById(closeId).style.display = 'flex' 304 | $(this).css({ 305 | 'animation': 'slideSwipeDrawerOpen .2s linear', 306 | '-webkit-transform' : 'translateX(' + -6 + 'rem)', 307 | '-moz-transform' : 'translateX(' + -6 + 'rem)', 308 | '-ms-transform' : 'translateX(' + -6 + 'rem)', 309 | '-o-transform' : 'translateX(' + -6 + 'rem)', 310 | 'transform' : 'translateX(' + -6 + 'rem)' 311 | }); 312 | this.setAttribute('data-open', true) 313 | } 314 | } 315 | 316 | function removeCoin(coin) { 317 | coin.parentNode.parentNode.parentNode.removeChild(coin.parentNode.parentNode); 318 | } 319 | 320 | function closeOpenPositions(coinCloseIcon) { 321 | var coin = coinCloseIcon.parentNode.parentNode.parentNode.id 322 | fetch(`${window.origin}/api/close_all`, { 323 | method: 'post', 324 | headers: { 325 | 'Content-Type': 'application/json' 326 | // 'Content-Type': 'application/x-www-form-urlencoded', 327 | }, 328 | body: JSON.stringify({coin: coin}), 329 | }) 330 | .then(function(response){ 331 | if (response.status !== 200) { 332 | displayMessage(`Bad response from api: ${response.status}`) 333 | return ; 334 | } 335 | else {displayMessage(`Closed position: ${coin}`);} 336 | }) 337 | } 338 | 339 | function closeOpenPosition(coinCloseIcon) { 340 | var coin = coinCloseIcon.parentNode.parentNode.parentNode.id 341 | fetch(`${window.origin}/api/close_position`, { 342 | method: 'post', 343 | headers: { 344 | 'Content-Type': 'application/json' 345 | // 'Content-Type': 'application/x-www-form-urlencoded', 346 | }, 347 | body: JSON.stringify({coin: coin}), 348 | }) 349 | .then(function(response){ 350 | if (response.status !== 200) { 351 | displayMessage(`Bad response from api: ${response.status}`) 352 | return ; 353 | } 354 | else {displayMessage(`Closed position: ${coin}`);} 355 | }) 356 | } 357 | 358 | function apiPostRequest(endpoint, data, responseSuccessFunc) { 359 | fetch(`${window.origin}/${endpoint}`, { 360 | method: 'post', 361 | headers: { 362 | 'Content-Type': 'application/json' 363 | // 'Content-Type': 'application/x-www-form-urlencoded', 364 | }, 365 | body: JSON.stringify(data), 366 | }) 367 | .then(function(response){ 368 | if (response.status !== 200) { 369 | displayMessage(`Bad response from api: ${response.status}`) 370 | return ; 371 | } 372 | else {responseSuccessFunc(response)} 373 | }) 374 | } 375 | 376 | function closeAllPositions() { 377 | var data = {} 378 | apiPostRequest('/api/close_all_positions', data, function(response) {displayMessage('Positions closed')}) 379 | } 380 | 381 | function addCoin() { 382 | var quickTradeDiv = document.getElementById('quickTradeDiv') 383 | var coin = document.getElementById('coinInput') 384 | coin_value = coin.value 385 | coin.value = '' 386 | coin = coin_value.toUpperCase() 387 | coin += 'USDT' 388 | // var quickTradeComponent = document.createElement('div') 389 | var innerDiv = document.createElement('div') 390 | innerDiv.innerHTML = `
LONG
${coin}
SHORT
`; 391 | innerDiv.id = coin 392 | innerDiv.style.opacity = 1 393 | innerDiv.style.animation = 'fadeIn 1s linear' 394 | innerDiv.addEventListener('touchstart', handleTouchStart, false); 395 | innerDiv.addEventListener('touchmove', handleTouchMove, false); 396 | innerDiv.addEventListener('contextmenu', handleRightClick, false); 397 | quickTradeDiv.appendChild(innerDiv) 398 | document.getElementById(`${coin}QuickTradeCoinText`).addEventListener('click', changeChart, false); 399 | // quickTradeDiv.appendChild(quickTradeComponent) 400 | } 401 | 402 | function hideMessages() { 403 | document.getElementById('messageBoxDiv').innerHTML = '' 404 | } 405 | 406 | function displayMessage(message) { 407 | var messageDiv = document.getElementById('messageBoxDiv') 408 | var component = `
${message}
` 409 | messageDiv.innerHTML = component; 410 | setTimeout(hideMessages, 3000); 411 | 412 | } 413 | 414 | function updateChart() { 415 | // console.log('updating chart'); 416 | var coinText = document.getElementById('coinText') 417 | var intervalText = document.getElementById('intervalText').innerHTML 418 | var coin = coinText.innerHTML 419 | // console.log(coin) 420 | fetch(`${window.origin}/plot`, { 421 | method: 'post', 422 | headers: { 423 | 'Content-Type': 'application/json' 424 | // 'Content-Type': 'application/x-www-form-urlencoded', 425 | }, 426 | body: JSON.stringify({ 427 | symbol: coin, 428 | interval: intervalText 429 | }), 430 | }) 431 | .then(function(response){ 432 | if (response.status !== 200) { 433 | displayMessage(`Bad response from chart api: ${response.status}`) 434 | return ; 435 | } 436 | response.json().then(function(data){ 437 | var chartSpace = document.getElementById('chartSpace') 438 | var imgTag = new Image(); 439 | imgTag.onload = function() { 440 | chartSpace.setAttribute('src', data.base64) 441 | } 442 | imgTag.src = data.base64; 443 | }) 444 | }) 445 | } 446 | 447 | function changeChart() { 448 | var symbol = this.innerHTML 449 | var coinText = document.getElementById('coinText') 450 | coinText.innerHTML = symbol 451 | updateChart() 452 | // console.log(symbol) 453 | // apiPostRequest('/plot', data, function(response) {updateChart()}) 454 | } 455 | 456 | var chartInterval = 10000; 457 | var intervalId; 458 | 459 | function getInterval() { 460 | return chartInterval 461 | } 462 | 463 | function startInterval(_interval) { 464 | intervalId = setInterval(function() { 465 | updateChart() 466 | }, _interval); 467 | } 468 | 469 | function selectInterval(btn) { 470 | var intervalText = document.getElementById('intervalText') 471 | var interval = btn.innerHTML 472 | intervalText.innerHTML = interval 473 | switch(interval) { 474 | case '1m': 475 | chartInterval = 2000; 476 | break; 477 | case '15m': 478 | chartInterval = 5000; 479 | break; 480 | case '1h': 481 | chartInterval = 10000; 482 | break; 483 | case '4h': 484 | chartInterval = 10000; 485 | break; 486 | } 487 | // console.log(interval) 488 | // console.log(chartInterval) 489 | clearInterval(intervalId); 490 | startInterval(chartInterval); 491 | updateChart(); 492 | } 493 | 494 | function startTime(clock) { 495 | var today = new Date(); 496 | var h = today.getHours(); 497 | var m = today.getMinutes(); 498 | var s = today.getSeconds(); 499 | m = checkTime(m); 500 | s = checkTime(s); 501 | clock.innerHTML = 502 | h + ":" + m + ":" + s; 503 | var t = setTimeout(function(){startTime(clock)}, 500); 504 | } 505 | function checkTime(i) { 506 | if (i < 10) {i = "0" + i}; // add zero in front of numbers < 10 507 | return i; 508 | } -------------------------------------------------------------------------------- /static/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perp_sniper", 3 | "version": "0.1.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@fullhuman/postcss-purgecss": { 8 | "version": "3.1.3", 9 | "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz", 10 | "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==", 11 | "requires": { 12 | "purgecss": "^3.1.3" 13 | } 14 | }, 15 | "acorn": { 16 | "version": "7.4.1", 17 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", 18 | "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" 19 | }, 20 | "acorn-node": { 21 | "version": "1.8.2", 22 | "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", 23 | "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", 24 | "requires": { 25 | "acorn": "^7.0.0", 26 | "acorn-walk": "^7.0.0", 27 | "xtend": "^4.0.2" 28 | } 29 | }, 30 | "acorn-walk": { 31 | "version": "7.2.0", 32 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", 33 | "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" 34 | }, 35 | "ansi-styles": { 36 | "version": "4.3.0", 37 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 38 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 39 | "requires": { 40 | "color-convert": "^2.0.1" 41 | } 42 | }, 43 | "at-least-node": { 44 | "version": "1.0.0", 45 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", 46 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" 47 | }, 48 | "autoprefixer": { 49 | "version": "10.2.4", 50 | "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.4.tgz", 51 | "integrity": "sha512-DCCdUQiMD+P/as8m3XkeTUkUKuuRqLGcwD0nll7wevhqoJfMRpJlkFd1+MQh1pvupjiQuip42lc/VFvfUTMSKw==", 52 | "requires": { 53 | "browserslist": "^4.16.1", 54 | "caniuse-lite": "^1.0.30001181", 55 | "colorette": "^1.2.1", 56 | "fraction.js": "^4.0.13", 57 | "normalize-range": "^0.1.2", 58 | "postcss-value-parser": "^4.1.0" 59 | } 60 | }, 61 | "balanced-match": { 62 | "version": "1.0.0", 63 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 64 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 65 | }, 66 | "brace-expansion": { 67 | "version": "1.1.11", 68 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 69 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 70 | "requires": { 71 | "balanced-match": "^1.0.0", 72 | "concat-map": "0.0.1" 73 | } 74 | }, 75 | "browserslist": { 76 | "version": "4.16.3", 77 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz", 78 | "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==", 79 | "requires": { 80 | "caniuse-lite": "^1.0.30001181", 81 | "colorette": "^1.2.1", 82 | "electron-to-chromium": "^1.3.649", 83 | "escalade": "^3.1.1", 84 | "node-releases": "^1.1.70" 85 | } 86 | }, 87 | "bytes": { 88 | "version": "3.1.0", 89 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 90 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 91 | }, 92 | "camelcase-css": { 93 | "version": "2.0.1", 94 | "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", 95 | "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" 96 | }, 97 | "caniuse-lite": { 98 | "version": "1.0.30001187", 99 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001187.tgz", 100 | "integrity": "sha512-w7/EP1JRZ9552CyrThUnay2RkZ1DXxKe/Q2swTC4+LElLh9RRYrL1Z+27LlakB8kzY0fSmHw9mc7XYDUKAKWMA==" 101 | }, 102 | "chalk": { 103 | "version": "4.1.0", 104 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", 105 | "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", 106 | "requires": { 107 | "ansi-styles": "^4.1.0", 108 | "supports-color": "^7.1.0" 109 | } 110 | }, 111 | "color": { 112 | "version": "3.1.3", 113 | "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", 114 | "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", 115 | "requires": { 116 | "color-convert": "^1.9.1", 117 | "color-string": "^1.5.4" 118 | }, 119 | "dependencies": { 120 | "color-convert": { 121 | "version": "1.9.3", 122 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 123 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 124 | "requires": { 125 | "color-name": "1.1.3" 126 | } 127 | }, 128 | "color-name": { 129 | "version": "1.1.3", 130 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 131 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 132 | } 133 | } 134 | }, 135 | "color-convert": { 136 | "version": "2.0.1", 137 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 138 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 139 | "requires": { 140 | "color-name": "~1.1.4" 141 | } 142 | }, 143 | "color-name": { 144 | "version": "1.1.4", 145 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 146 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 147 | }, 148 | "color-string": { 149 | "version": "1.5.4", 150 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", 151 | "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", 152 | "requires": { 153 | "color-name": "^1.0.0", 154 | "simple-swizzle": "^0.2.2" 155 | } 156 | }, 157 | "colorette": { 158 | "version": "1.2.1", 159 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", 160 | "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" 161 | }, 162 | "commander": { 163 | "version": "6.2.1", 164 | "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", 165 | "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" 166 | }, 167 | "concat-map": { 168 | "version": "0.0.1", 169 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 170 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 171 | }, 172 | "css-unit-converter": { 173 | "version": "1.1.2", 174 | "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", 175 | "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" 176 | }, 177 | "cssesc": { 178 | "version": "3.0.0", 179 | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", 180 | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" 181 | }, 182 | "defined": { 183 | "version": "1.0.0", 184 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", 185 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" 186 | }, 187 | "detective": { 188 | "version": "5.2.0", 189 | "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", 190 | "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", 191 | "requires": { 192 | "acorn-node": "^1.6.1", 193 | "defined": "^1.0.0", 194 | "minimist": "^1.1.1" 195 | } 196 | }, 197 | "didyoumean": { 198 | "version": "1.2.1", 199 | "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz", 200 | "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=" 201 | }, 202 | "electron-to-chromium": { 203 | "version": "1.3.666", 204 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.666.tgz", 205 | "integrity": "sha512-/mP4HFQ0fKIX4sXltG6kfcoGrfNDZwCIyWbH2SIcVaa9u7Rm0HKjambiHNg5OEruicTl9s1EwbERLwxZwk19aw==" 206 | }, 207 | "escalade": { 208 | "version": "3.1.1", 209 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 210 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" 211 | }, 212 | "escape-string-regexp": { 213 | "version": "1.0.5", 214 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 215 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 216 | }, 217 | "fraction.js": { 218 | "version": "4.0.13", 219 | "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.13.tgz", 220 | "integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA==" 221 | }, 222 | "fs-extra": { 223 | "version": "9.1.0", 224 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", 225 | "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", 226 | "requires": { 227 | "at-least-node": "^1.0.0", 228 | "graceful-fs": "^4.2.0", 229 | "jsonfile": "^6.0.1", 230 | "universalify": "^2.0.0" 231 | } 232 | }, 233 | "fs.realpath": { 234 | "version": "1.0.0", 235 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 236 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 237 | }, 238 | "function-bind": { 239 | "version": "1.1.1", 240 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 241 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 242 | }, 243 | "glob": { 244 | "version": "7.1.6", 245 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 246 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 247 | "requires": { 248 | "fs.realpath": "^1.0.0", 249 | "inflight": "^1.0.4", 250 | "inherits": "2", 251 | "minimatch": "^3.0.4", 252 | "once": "^1.3.0", 253 | "path-is-absolute": "^1.0.0" 254 | } 255 | }, 256 | "graceful-fs": { 257 | "version": "4.2.6", 258 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", 259 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" 260 | }, 261 | "has": { 262 | "version": "1.0.3", 263 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 264 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 265 | "requires": { 266 | "function-bind": "^1.1.1" 267 | } 268 | }, 269 | "has-flag": { 270 | "version": "4.0.0", 271 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 272 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 273 | }, 274 | "html-tags": { 275 | "version": "3.1.0", 276 | "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", 277 | "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==" 278 | }, 279 | "indexes-of": { 280 | "version": "1.0.1", 281 | "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", 282 | "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" 283 | }, 284 | "inflight": { 285 | "version": "1.0.6", 286 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 287 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 288 | "requires": { 289 | "once": "^1.3.0", 290 | "wrappy": "1" 291 | } 292 | }, 293 | "inherits": { 294 | "version": "2.0.4", 295 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 296 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 297 | }, 298 | "is-arrayish": { 299 | "version": "0.3.2", 300 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 301 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 302 | }, 303 | "is-core-module": { 304 | "version": "2.2.0", 305 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", 306 | "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", 307 | "requires": { 308 | "has": "^1.0.3" 309 | } 310 | }, 311 | "jsonfile": { 312 | "version": "6.1.0", 313 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 314 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 315 | "requires": { 316 | "graceful-fs": "^4.1.6", 317 | "universalify": "^2.0.0" 318 | } 319 | }, 320 | "lodash": { 321 | "version": "4.17.20", 322 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 323 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" 324 | }, 325 | "lodash.toarray": { 326 | "version": "4.4.0", 327 | "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", 328 | "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" 329 | }, 330 | "minimatch": { 331 | "version": "3.0.4", 332 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 333 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 334 | "requires": { 335 | "brace-expansion": "^1.1.7" 336 | } 337 | }, 338 | "minimist": { 339 | "version": "1.2.5", 340 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 341 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 342 | }, 343 | "modern-normalize": { 344 | "version": "1.0.0", 345 | "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.0.0.tgz", 346 | "integrity": "sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw==" 347 | }, 348 | "nanoid": { 349 | "version": "3.1.20", 350 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", 351 | "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==" 352 | }, 353 | "node-emoji": { 354 | "version": "1.10.0", 355 | "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", 356 | "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", 357 | "requires": { 358 | "lodash.toarray": "^4.4.0" 359 | } 360 | }, 361 | "node-releases": { 362 | "version": "1.1.70", 363 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz", 364 | "integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==" 365 | }, 366 | "normalize-range": { 367 | "version": "0.1.2", 368 | "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", 369 | "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" 370 | }, 371 | "object-assign": { 372 | "version": "4.1.1", 373 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 374 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 375 | }, 376 | "object-hash": { 377 | "version": "2.1.1", 378 | "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz", 379 | "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==" 380 | }, 381 | "once": { 382 | "version": "1.4.0", 383 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 384 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 385 | "requires": { 386 | "wrappy": "1" 387 | } 388 | }, 389 | "path-is-absolute": { 390 | "version": "1.0.1", 391 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 392 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 393 | }, 394 | "path-parse": { 395 | "version": "1.0.6", 396 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 397 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" 398 | }, 399 | "postcss": { 400 | "version": "8.2.6", 401 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.6.tgz", 402 | "integrity": "sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==", 403 | "requires": { 404 | "colorette": "^1.2.1", 405 | "nanoid": "^3.1.20", 406 | "source-map": "^0.6.1" 407 | } 408 | }, 409 | "postcss-functions": { 410 | "version": "3.0.0", 411 | "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", 412 | "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=", 413 | "requires": { 414 | "glob": "^7.1.2", 415 | "object-assign": "^4.1.1", 416 | "postcss": "^6.0.9", 417 | "postcss-value-parser": "^3.3.0" 418 | }, 419 | "dependencies": { 420 | "ansi-styles": { 421 | "version": "3.2.1", 422 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 423 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 424 | "requires": { 425 | "color-convert": "^1.9.0" 426 | } 427 | }, 428 | "chalk": { 429 | "version": "2.4.2", 430 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 431 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 432 | "requires": { 433 | "ansi-styles": "^3.2.1", 434 | "escape-string-regexp": "^1.0.5", 435 | "supports-color": "^5.3.0" 436 | } 437 | }, 438 | "color-convert": { 439 | "version": "1.9.3", 440 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 441 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 442 | "requires": { 443 | "color-name": "1.1.3" 444 | } 445 | }, 446 | "color-name": { 447 | "version": "1.1.3", 448 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 449 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 450 | }, 451 | "has-flag": { 452 | "version": "3.0.0", 453 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 454 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 455 | }, 456 | "postcss": { 457 | "version": "6.0.23", 458 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", 459 | "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", 460 | "requires": { 461 | "chalk": "^2.4.1", 462 | "source-map": "^0.6.1", 463 | "supports-color": "^5.4.0" 464 | } 465 | }, 466 | "postcss-value-parser": { 467 | "version": "3.3.1", 468 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", 469 | "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" 470 | }, 471 | "supports-color": { 472 | "version": "5.5.0", 473 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 474 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 475 | "requires": { 476 | "has-flag": "^3.0.0" 477 | } 478 | } 479 | } 480 | }, 481 | "postcss-js": { 482 | "version": "3.0.3", 483 | "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", 484 | "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", 485 | "requires": { 486 | "camelcase-css": "^2.0.1", 487 | "postcss": "^8.1.6" 488 | } 489 | }, 490 | "postcss-nested": { 491 | "version": "5.0.3", 492 | "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.3.tgz", 493 | "integrity": "sha512-R2LHPw+u5hFfDgJG748KpGbJyTv7Yr33/2tIMWxquYuHTd9EXu27PYnKi7BxMXLtzKC0a0WVsqHtd7qIluQu/g==", 494 | "requires": { 495 | "postcss-selector-parser": "^6.0.4" 496 | } 497 | }, 498 | "postcss-selector-parser": { 499 | "version": "6.0.4", 500 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", 501 | "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", 502 | "requires": { 503 | "cssesc": "^3.0.0", 504 | "indexes-of": "^1.0.1", 505 | "uniq": "^1.0.1", 506 | "util-deprecate": "^1.0.2" 507 | } 508 | }, 509 | "postcss-value-parser": { 510 | "version": "4.1.0", 511 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", 512 | "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" 513 | }, 514 | "pretty-hrtime": { 515 | "version": "1.0.3", 516 | "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", 517 | "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" 518 | }, 519 | "purgecss": { 520 | "version": "3.1.3", 521 | "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz", 522 | "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==", 523 | "requires": { 524 | "commander": "^6.0.0", 525 | "glob": "^7.0.0", 526 | "postcss": "^8.2.1", 527 | "postcss-selector-parser": "^6.0.2" 528 | } 529 | }, 530 | "reduce-css-calc": { 531 | "version": "2.1.8", 532 | "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", 533 | "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", 534 | "requires": { 535 | "css-unit-converter": "^1.1.1", 536 | "postcss-value-parser": "^3.3.0" 537 | }, 538 | "dependencies": { 539 | "postcss-value-parser": { 540 | "version": "3.3.1", 541 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", 542 | "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" 543 | } 544 | } 545 | }, 546 | "resolve": { 547 | "version": "1.20.0", 548 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 549 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 550 | "requires": { 551 | "is-core-module": "^2.2.0", 552 | "path-parse": "^1.0.6" 553 | } 554 | }, 555 | "simple-swizzle": { 556 | "version": "0.2.2", 557 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 558 | "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", 559 | "requires": { 560 | "is-arrayish": "^0.3.1" 561 | } 562 | }, 563 | "source-map": { 564 | "version": "0.6.1", 565 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 566 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 567 | }, 568 | "supports-color": { 569 | "version": "7.2.0", 570 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 571 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 572 | "requires": { 573 | "has-flag": "^4.0.0" 574 | } 575 | }, 576 | "tailwindcss": { 577 | "version": "2.0.3", 578 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.0.3.tgz", 579 | "integrity": "sha512-s8NEqdLBiVbbdL0a5XwTb8jKmIonOuI4RMENEcKLR61jw6SdKvBss7NWZzwCaD+ZIjlgmesv8tmrjXEp7C0eAQ==", 580 | "requires": { 581 | "@fullhuman/postcss-purgecss": "^3.1.3", 582 | "bytes": "^3.0.0", 583 | "chalk": "^4.1.0", 584 | "color": "^3.1.3", 585 | "detective": "^5.2.0", 586 | "didyoumean": "^1.2.1", 587 | "fs-extra": "^9.1.0", 588 | "html-tags": "^3.1.0", 589 | "lodash": "^4.17.20", 590 | "modern-normalize": "^1.0.0", 591 | "node-emoji": "^1.8.1", 592 | "object-hash": "^2.1.1", 593 | "postcss-functions": "^3", 594 | "postcss-js": "^3.0.3", 595 | "postcss-nested": "^5.0.1", 596 | "postcss-selector-parser": "^6.0.4", 597 | "postcss-value-parser": "^4.1.0", 598 | "pretty-hrtime": "^1.0.3", 599 | "reduce-css-calc": "^2.1.8", 600 | "resolve": "^1.19.0" 601 | } 602 | }, 603 | "uniq": { 604 | "version": "1.0.1", 605 | "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", 606 | "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" 607 | }, 608 | "universalify": { 609 | "version": "2.0.0", 610 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", 611 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" 612 | }, 613 | "util-deprecate": { 614 | "version": "1.0.2", 615 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 616 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 617 | }, 618 | "wrappy": { 619 | "version": "1.0.2", 620 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 621 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 622 | }, 623 | "xtend": { 624 | "version": "4.0.2", 625 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 626 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 627 | } 628 | } 629 | } 630 | --------------------------------------------------------------------------------