├── README.md ├── test.py ├── worker.py ├── LICENSE ├── .gitignore ├── backtest.py ├── main.py └── trader.py /README.md: -------------------------------------------------------------------------------- 1 | # bybit-bot-vb 2 | bybit 양방향 변동성 돌파 전략 봇 3 | 4 | ![bybit_ui](https://user-images.githubusercontent.com/23475470/149863790-3a6dc470-f672-4771-a9a5-a69056f90a9d.gif) 5 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from pybit import HTTP 2 | import pprint 3 | 4 | with open("./bybit.key") as f: 5 | lines = f.readlines() 6 | api_key = lines[0].strip() 7 | api_secret = lines[1].strip() 8 | 9 | session = HTTP( 10 | endpoint="https://api.bybit.com", 11 | api_key=api_key, 12 | api_secret=api_secret, 13 | spot=False 14 | ) 15 | 16 | resp = session.place_active_order( 17 | symbol="XRPUSDT", 18 | side="Buy", 19 | order_type="Market", 20 | qty=46, 21 | time_in_force="GoodTillCancel", 22 | reduce_only=True, 23 | close_on_trigger=False 24 | ) 25 | 26 | pprint.pprint(resp) -------------------------------------------------------------------------------- /worker.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from pybit import HTTP 4 | import time 5 | import sys 6 | import pprint 7 | 8 | class Worker(QThread): 9 | last_price = pyqtSignal(list) 10 | 11 | def __init__(self, symbol): 12 | super().__init__() 13 | self.symbol = symbol 14 | self.session = HTTP("https://api.bybit.com") 15 | 16 | def run(self): 17 | while True: 18 | try: 19 | info = self.session.latest_information_for_symbol( 20 | symbol=self.symbol 21 | ) 22 | 23 | last_price = info['result'][0]['last_price'] 24 | last_price = float(last_price) 25 | self.last_price.emit([self.symbol, last_price]) 26 | time.sleep(1) 27 | except: 28 | time.sleep(10) 29 | 30 | 31 | if __name__ == "__main__": 32 | app = QApplication(sys.argv) 33 | w = Worker("BTCUSDT") 34 | w.start() 35 | app.exec_() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 sharebook-kr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | *.key 131 | .DS_Store 132 | -------------------------------------------------------------------------------- /backtest.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from pybit import HTTP 4 | import datetime 5 | import time 6 | import pandas as pd 7 | import numpy as np 8 | import sys 9 | 10 | 11 | class BackTest(QThread): 12 | params = pyqtSignal(list) 13 | message = pyqtSignal(str) 14 | 15 | def __init__(self, symbol, main=None): 16 | super().__init__() 17 | self.symbol = symbol 18 | self.main = main 19 | self.df = None 20 | self.session = HTTP( 21 | endpoint="https://api.bybit.com", 22 | spot=False 23 | ) 24 | 25 | def fetch_days(self): 26 | now = datetime.datetime.now() 27 | today = datetime.datetime( 28 | year=now.year, 29 | month=now.month, 30 | day=now.day, 31 | hour=0, 32 | minute=0, 33 | second=0 34 | ) 35 | 36 | delta = datetime.timedelta(days=-60) 37 | dt = today + delta 38 | from_time = time.mktime(dt.timetuple()) 39 | 40 | resp = self.session.query_kline( 41 | symbol=self.symbol, 42 | interval="D", 43 | limit=60+1, # today 44 | from_time=from_time 45 | ) 46 | 47 | result = resp['result'] 48 | df = pd.DataFrame(result) 49 | ts = pd.to_datetime(df['open_time'], unit='s') 50 | df.set_index(ts, inplace=True) 51 | return df[['open', 'high', 'low', 'close']] 52 | 53 | @staticmethod 54 | def backtest(df, window, k, direction): 55 | df['ma'] = df['close'].rolling(window=window).mean().shift(1) 56 | df['range'] = (df['high'] - df['low']) * k 57 | 58 | if direction == 1: 59 | df['target'] = df['open'] + df['range'].shift(1) 60 | else: 61 | df['target'] = df['open'] - df['range'].shift(1) 62 | 63 | # 상승장/하락장 64 | df['status'] = df['open'] > df['ma'] 65 | 66 | if direction == 1: 67 | df['수익률'] = np.where((df['high'] > df['target']) & (df['status'] == 1), df['close'] / df['target'], 1) 68 | else: 69 | df['수익률'] = np.where((df['low'] < df['target']) & (df['status'] == 0), df['target'] / df['close'], 1) 70 | 71 | df['누적수익률'] = df['수익률'].cumprod() 72 | return df 73 | 74 | def find_optimal(self, df, direction): 75 | best_return = 0 76 | best_window = 0 77 | best_k = 0 78 | 79 | for window in range(5, 21): 80 | for k in np.arange(0.3, 0.7, 0.01): 81 | df2 = self.backtest(df, window, k, direction) 82 | cur_return = df2['누적수익률'][-2] 83 | 84 | if cur_return > best_return: 85 | best_return = cur_return 86 | best_window = window 87 | best_k = k 88 | 89 | #print(best_window, best_k, best_return) 90 | return (best_window, best_k) 91 | 92 | def run(self): 93 | df = self.fetch_days() 94 | 95 | up_window, up_k = self.find_optimal(df.copy(), 1) 96 | up_df = self.backtest(df.copy(), up_window, up_k, 1) 97 | acc_return = up_df["누적수익률"][-2] 98 | message = f"{self.symbol} 상승장 누적 수익률: {acc_return:.2f}" 99 | self.message.emit(message); 100 | 101 | down_window, down_k = self.find_optimal(df.copy(), 0) 102 | down_df = self.backtest(df.copy(), down_window, down_k, 0) 103 | acc_return = down_df["누적수익률"][-2] 104 | message = f"{self.symbol} 하락장 누적 수익률: {acc_return:.2f}" 105 | self.message.emit(message); 106 | 107 | self.params.emit([ 108 | self.symbol, 109 | up_df.iloc[-1]['target'], 110 | up_window, 111 | up_k, 112 | down_df.iloc[-1]['target'], 113 | down_window, 114 | down_k 115 | ]) 116 | 117 | 118 | if __name__ == "__main__": 119 | app = QApplication(sys.argv) 120 | #coin = BackTest("BTCUSDT") 121 | #coin = BackTest("ETHUSDT") 122 | coin = BackTest("XRPUSDT") 123 | coin.start() 124 | app.exec_() -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | from PyQt5.QtWidgets import * 4 | from PyQt5.QtCore import * 5 | from worker import * 6 | from backtest import * 7 | from trader import * 8 | from PyQt5.QtCore import Qt 9 | import pprint 10 | import time 11 | 12 | 13 | class MyWindow(QMainWindow): 14 | def __init__(self): 15 | super().__init__() 16 | self.setWindowTitle("ByBit Bot v1.0 (양방향 변동성 돌파 전략)") 17 | self.setGeometry(100, 100, 930, 500) 18 | 19 | # variables 20 | self.worker = [] 21 | self.backtest = [] 22 | self.trader = [] 23 | 24 | #self.symbols = ['BTCUSDT', 'ETHUSDT', "XRPUSDT"] 25 | self.symbols = ['BTCUSDT', 'XRPUSDT'] 26 | self.ready = {k:0 for k in self.symbols} 27 | self.positions = {k:[False, False] for k in self.symbols} 28 | self.labels = ["코인", "현재가", "상승목표가", "하락목표가", "상승W", "상승K", "하락W", "하락K", "보유상태"] 29 | self.usdt = 0 30 | 31 | self.label = QLabel("잔고") 32 | self.line_edit = QLineEdit(" ") 33 | hbox = QHBoxLayout() 34 | hbox.addWidget(self.label) 35 | hbox.addWidget(self.line_edit) 36 | hbox.addStretch(2) 37 | 38 | self.table = QTableWidget() 39 | self.table.verticalHeader().setVisible(False) 40 | self.table.setColumnCount(len(self.labels)) 41 | self.table.setRowCount(len(self.symbols)) 42 | self.table.setHorizontalHeaderLabels(self.labels) 43 | 44 | self.text = QPlainTextEdit() 45 | 46 | widget = QWidget() 47 | layout = QVBoxLayout(widget) 48 | layout.addLayout(hbox) 49 | layout.addWidget(self.table) 50 | layout.addWidget(self.text) 51 | self.setCentralWidget(widget) 52 | 53 | self.connect() 54 | self.fetch_balance(init=1) 55 | 56 | # Timer 57 | self.timer = QTimer(self) 58 | self.timer.start(1000) 59 | self.timer.timeout.connect(self.update_ui) 60 | 61 | self.data = { symbol:{k:0 for k in self.labels} for symbol in self.symbols} 62 | self.create_threads() 63 | 64 | def connect(self): 65 | with open("./bybit.key") as f: 66 | lines = f.readlines() 67 | api_key = lines[0].strip() 68 | api_secret = lines[1].strip() 69 | 70 | self.session = HTTP( 71 | endpoint="https://api.bybit.com", 72 | api_key=api_key, 73 | api_secret=api_secret, 74 | spot=False 75 | ) 76 | 77 | def fetch_balance(self, init=0): 78 | try: 79 | balances = self.session.get_wallet_balance() 80 | usdt = balances['result']['USDT']['wallet_balance'] 81 | except: 82 | usdt = self.usdt # set previous usdt if fetch failed 83 | 84 | if init == 1: 85 | now = datetime.datetime.now() 86 | today = now.strftime("%Y-%m-%d") 87 | self.usdt = usdt 88 | self.text.appendPlainText(f"{today} USDT 잔고: {self.usdt}") 89 | 90 | self.line_edit.setText(str(usdt)) 91 | 92 | def create_threads(self): 93 | for symbol in self.symbols: 94 | w = Worker(symbol) 95 | w.last_price.connect(self.update_last_price) 96 | w.start() 97 | self.worker.append(w) 98 | 99 | b = BackTest(symbol, self) 100 | b.params.connect(self.update_params) 101 | b.message.connect(self.update_message) 102 | b.start() 103 | self.backtest.append(b) 104 | 105 | t = Trader(symbol, self) 106 | t.message.connect(self.update_message) 107 | t.start() 108 | self.trader.append(t) 109 | 110 | def update_ui(self): 111 | now = datetime.datetime.now() 112 | local_time_stamp = int(time.mktime(now.timetuple())) 113 | 114 | try: 115 | server_time = self.session.server_time() 116 | except: 117 | self.connect() 118 | server_time = self.session.server_time() 119 | 120 | server_time_stamp = int(float(server_time['time_now'])) 121 | diff_time_stamp = server_time_stamp - local_time_stamp 122 | diff_msg = f" | SERVER: {server_time_stamp} LOCAL: {local_time_stamp} DIFF: {diff_time_stamp}" 123 | 124 | self.statusBar().showMessage(str(now)[:19] + diff_msg) 125 | self.update_table_widget() 126 | self.fetch_balance() 127 | 128 | def update_table_widget(self): 129 | for r, symbol in enumerate(self.symbols): 130 | for c, label in enumerate(self.labels): 131 | data = self.data[symbol][label] 132 | 133 | if label in ["상승목표가", "하락목표가"]: 134 | data = "{:.3f}".format(data) 135 | 136 | if label == "보유상태": 137 | data = f"{self.positions[symbol][0]} | {self.positions[symbol][1]}" 138 | 139 | item = QTableWidgetItem(str(data)) 140 | if c == 0: 141 | item.setTextAlignment(Qt.AlignCenter) 142 | else: 143 | item.setTextAlignment(int(Qt.AlignRight | Qt.AlignVCenter)) 144 | 145 | self.table.setItem(r, c, item) 146 | 147 | @pyqtSlot(list) 148 | def update_last_price(self, info): 149 | symbol, last = info 150 | self.data[symbol]['코인'] = symbol 151 | self.data[symbol]['현재가'] = last 152 | 153 | @pyqtSlot(str) 154 | def update_message(self, text): 155 | self.text.appendPlainText(text) 156 | 157 | @pyqtSlot(list) 158 | def update_params(self, params): 159 | symbol, up_target, up_window, up_k, down_target, down_window, down_k = params 160 | 161 | self.data[symbol]['상승목표가'] = up_target 162 | self.data[symbol]['상승W'] = up_window 163 | self.data[symbol]['상승K'] = "{:.2f}".format(up_k) 164 | 165 | self.data[symbol]['하락목표가'] = down_target 166 | self.data[symbol]['하락W'] = down_window 167 | self.data[symbol]['하락K'] = "{:.2f}".format(down_k) 168 | 169 | self.ready[symbol] = True 170 | #self.text.appendPlainText(f"{symbol} 파라미터 갱신 완료") 171 | 172 | 173 | if __name__ == "__main__": 174 | app = QApplication(sys.argv) 175 | window = MyWindow() 176 | window.show() 177 | app.exec_() -------------------------------------------------------------------------------- /trader.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from pybit import HTTP 4 | import time 5 | import sys 6 | import pprint 7 | import datetime 8 | 9 | 10 | class Trader(QThread): 11 | message = pyqtSignal(str) 12 | 13 | def __init__(self, symbol, main=None): 14 | super().__init__() 15 | 16 | self.symbol = symbol 17 | self.main = main 18 | 19 | self.price_round = { 20 | "BTCUSDT": 1, 21 | "ETHUSDT": 2, 22 | "XRPUSDT": 4 23 | } 24 | 25 | self.quantity_round = { 26 | "BTCUSDT": 3, 27 | "ETHUSDT": 2, 28 | "XRPUSDT": 0 29 | } 30 | 31 | self.usdt = 0 32 | self.long_quantity = 0 33 | self.short_quantity = 0 34 | 35 | self.create_session() 36 | 37 | def create_session(self): 38 | with open("./bybit.key") as f: 39 | lines = f.readlines() 40 | api_key = lines[0].strip() 41 | api_secret = lines[1].strip() 42 | 43 | self.session = HTTP( 44 | endpoint="https://api.bybit.com", 45 | api_key=api_key, 46 | api_secret=api_secret, 47 | spot=False 48 | ) 49 | 50 | def run(self): 51 | while True: 52 | now = datetime.datetime.now() 53 | 54 | if self.main is not None and self.main.ready[self.symbol]: 55 | # 코인별 포지션별 투자 금액 산정 56 | self.usdt = self.main.usdt / 6 57 | 58 | # long position 59 | if self.main.positions[self.symbol][0] == False: 60 | # check the open long condition 61 | self.open_long() 62 | 63 | # short position 64 | if self.main.positions[self.symbol][1] == False: 65 | # check the open short condition 66 | self.open_short() 67 | 68 | # 08:59:00 포지션 정리 69 | if now.hour == 8 and now.minute == 59 and (now.second > 0 and now.second < 10): 70 | self.create_session() 71 | self.close_long() 72 | self.close_short() 73 | self.main.ready[self.symbol] = 0 # 9시 전까지는 매매 않하도록 74 | time.sleep(10) 75 | 76 | # 09:01:00 거래일 파라미터 업데이트 77 | if now.hour == 9 and now.minute == 1 and (now.second > 0 and now.second < 10): 78 | # 잔고 갱신 79 | self.main.fetch_balance(init=1) 80 | 81 | # backtest thread 시작 82 | for b in self.main.backtest: 83 | b.start() 84 | 85 | time.sleep(10) 86 | 87 | time.sleep(1) 88 | 89 | def open_long(self): 90 | """상승장에서 long position open 91 | """ 92 | cur_price = self.main.data[self.symbol]["현재가"] 93 | target_price = self.main.data[self.symbol]["상승목표가"] 94 | ndigits = self.price_round[self.symbol] 95 | 96 | qty_round = self.quantity_round[self.symbol] 97 | order_price = round(target_price, ndigits) 98 | 99 | quantity = (self.usdt / order_price) * 0.95 100 | quantity = round(quantity, qty_round) 101 | 102 | # 장중간에 프로그램을 시작하는 경우 order_price 대비 1% 이내에서만 주문 되도록 103 | if (cur_price >= order_price) and (cur_price < order_price * 1.01): 104 | message = f"{self.symbol} enter long position" 105 | self.message.emit(message) 106 | 107 | # open the position 108 | try: 109 | resp = self.session.place_active_order( 110 | symbol=self.symbol, 111 | side="Buy", 112 | #order_type="Limit", 113 | order_type="Market", 114 | qty=quantity, 115 | #price=order_price, 116 | time_in_force="GoodTillCancel", 117 | reduce_only=False, 118 | close_on_trigger=False 119 | ) 120 | 121 | # update position 122 | self.main.positions[self.symbol][0] = True 123 | 124 | # save the quantity 125 | self.long_quantity = quantity 126 | except: 127 | pass 128 | 129 | def open_short(self): 130 | """하락장에서 short position open 131 | """ 132 | cur_price = self.main.data[self.symbol]["현재가"] 133 | target_price = self.main.data[self.symbol]["하락목표가"] 134 | ndigits = self.price_round[self.symbol] 135 | 136 | qty_round = self.quantity_round[self.symbol] 137 | order_price = round(target_price, ndigits) 138 | 139 | quantity = (self.usdt / order_price) * 0.95 140 | quantity = round(quantity, qty_round) 141 | 142 | # open the position 143 | # 장중간에 프로그램을 시작하는 경우 order_price 대비 1% 이내에서만 주문 되도록 144 | if (cur_price <= order_price) and (cur_price > order_price * 0.99): 145 | message = f"{self.symbol} enter short position" 146 | self.message.emit(message) 147 | 148 | try: 149 | resp = self.session.place_active_order( 150 | symbol=self.symbol, 151 | side="Sell", 152 | #order_type="Limit", 153 | order_type="Market", 154 | qty=quantity, 155 | #price=order_price, 156 | time_in_force="GoodTillCancel", 157 | reduce_only=False, 158 | close_on_trigger=False 159 | ) 160 | 161 | self.main.positions[self.symbol][1] = True 162 | 163 | # save the quantity 164 | self.short_quantity = quantity 165 | except: 166 | pass 167 | 168 | def close_long(self): 169 | """long position close 170 | """ 171 | if self.long_quantity != 0: 172 | try: 173 | resp = self.session.place_active_order( 174 | symbol=self.symbol, 175 | side="Sell", 176 | order_type="Market", 177 | qty=self.long_quantity, 178 | time_in_force="GoodTillCancel", 179 | reduce_only=True, 180 | close_on_trigger=False 181 | ) 182 | except: 183 | pass 184 | 185 | def close_short(self): 186 | """short position close 187 | """ 188 | if self.short_quantity != 0: 189 | try: 190 | resp = self.session.place_active_order( 191 | symbol=self.symbol, 192 | side="Buy", 193 | order_type="Market", 194 | qty=self.short_quantity, 195 | time_in_force="GoodTillCancel", 196 | reduce_only=True, 197 | close_on_trigger=False 198 | ) 199 | except: 200 | pass 201 | 202 | if __name__ == "__main__": 203 | app = QApplication(sys.argv) 204 | w = Trader("BTCUSDT") 205 | w.start() 206 | app.exec_() --------------------------------------------------------------------------------