From bdb8dadb33c1f422b170e754709b38554819cfb7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 1 Mar 2018 10:34:29 +0200 Subject: [PATCH 001/187] Fix pep8 issues in storage.py - fix artifact name --- appveyor.yml | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..48bf91915 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,65 @@ +version: 1.0.{build} + +image: Visual Studio 2015 + +environment: + matrix: + # Python 3.6 - 32-bit + #- PYTHON: "C:\\Python36" + # Python 3.6 - 64-bit + - PYTHON: "C:\\Python36-x64" + +#---------------------------------# +# build # +#---------------------------------# + +build: off + +configuration: Release + +install: + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - "python --version" + - "pip install pyinstaller click-datetime PyQt5" + - "python setup.py install" + +after_test: + - "pyinstaller app.spec" + - "pyinstaller cli.spec" + - '7z a DEXBot-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-cli.exe' + +# @TODO: Run tests.. +test_script: + - "echo tests..." + +artifacts: + - path: DEXBot-win64.zip + name: DEXBot-win64.zip + +#---------------------------------# +# deployment # +#---------------------------------# + +shallow_clone: false + +clone_depth: 1 + +deploy: + - provider: GitHub + artifact: DEXBot-win64.zip + draft: false + prerelease: false + force_update: true + auth_token: + secure: DNkVbBiLYkrWapXn2ioCY3Qbkn5jlsNNyx2G7gOR86OCEqEnOWSxvMQp/Yoq4jJ3 + on: + appveyor_repo_tag: true # deploy on tag push only + +#---------------------------------# +# notifications # +#---------------------------------# + +notifications: + # Slack + #- provider: Slack + # incoming_webhook: http://dexbotdevelop.slack.com \ No newline at end of file From 150fdc7e2fef12be4916a90883136677b0fa38c1 Mon Sep 17 00:00:00 2001 From: sergio Date: Tue, 20 Feb 2018 23:49:49 +0100 Subject: [PATCH 002/187] - PyInstaller Build CLI + GUI - Added Travis / Appveyor CI - Added hidden import for cbc mode - Added built-in strategies as hidden imports --- .travis.yml | 35 +++++++++++++++++++++++++++++++++++ README.md | 8 ++++++++ app.spec | 30 +++++++++++++++++++++++++----- cli.spec | 44 ++++++++++++++++++++++++++++++++++++++++++++ hooks/hook-Crypto.py | 1 + 5 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 .travis.yml create mode 100644 cli.spec diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..64982742d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: true +matrix: + include: + - os: linux + language: python + python: '3.6' + - os: osx + language: generic + before_install: + - brew update + - brew install python3 + - virtualenv venv -p python3 + - source venv/bin/activate +install: + - python --version + - pip install click-datetime PyQt5 pyinstaller + - python setup.py install +script: + - echo "@TODO - Running tests..." + - pyinstaller --distpath dist/$TRAVIS_OS_NAME app.spec + - pyinstaller --distpath dist/$TRAVIS_OS_NAME cli.spec +before_deploy: + - git config --local user.name "Travis" + - git config --local user.email "travis@travis-ci.org" + - git tag "$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)" + - tar -czvf dist/DEXBot-$TRAVIS_OS_NAME.tar.gz dist/$TRAVIS_OS_NAME +deploy: + provider: releases + skip_cleanup: true + api_key: + secure: railzqElBKAEt0f1o1Z0sqaD4FZ3WkwhCxMJYTo1TmwkmS0r3f57n5LHOGd43MY/ZYbQhyoxZ0DE5Q+16+gSLVn2j5pLTMjBbBn4iqntgFFf4L6hHPsv67yFHSUz9l/T/2TKRCMbQzWTnvE8wQVFVnoSVc7yWgNIBoEVhA1vcPd17OUeCgZQcNN3YvGpJQhEOhoW+wByiskweV6tTTxtOmX3aQdJQPD6Tt/pbKdN4hHInJmrZjvUnH/qpQEgcgsOFiZE7er+zFIXxPdFV/XV2GqlUMob0rAmun6sGDpqZwTqSOmxZunquQCpmzMLqlW9lB3UUDsZs18amwDniq2rBEBUL6MEsgxEfYbmxMungNd7v3CV5zeq6QZen50Cwmhs8nhr3UIxUJzk5FfQCekFS4pxWvl+/QPW+oxKPTNa3oe4egzuDTTqY2XocuAVEeSHs72OFWqD0LYnHkV7APGQTDaJhcGvLSe0YcBX1R1KVz1JlTapdlZIJ4Ba3MOqgBaAybptkJ0CzaX0mcDh1R8CwcM2NHaFr4w2HFhgJSLtb3AbguOj5O+tdXvOlPq9n8ColH7Cuzch3t+ZnpSg3Q5qR5WqcVSP0AyfqKoNmSEA1exiqJp295vJnWeLkzlAcT0cMTsLAlQ6rSrR9IlUfmamuZuY+ypCxUQuunGdE0/s4J4= + file: dist/*.tar.gz + file_glob: true + on: + tags: true \ No newline at end of file diff --git a/README.md b/README.md index e3fae6630..25f9860b9 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,14 @@ Trading Bot for the BitShares Decentralized Exchange (DEX). +## Build status + +master: +[![Build Status](https://travis-ci.org/svdev/DEXBot.svg?branch=master)](https://travis-ci.org/svdev/DEXBot) + +develop: +[![Build Status](https://travis-ci.org/svdev/DEXBot.svg?branch=develop)](https://travis-ci.org/svdev/DEXBot) + **Warning**: This is highly experimental code! Use at your OWN risk! ## Installation diff --git a/app.spec b/app.spec index d4e6d7c91..a3968ae3f 100644 --- a/app.spec +++ b/app.spec @@ -1,12 +1,25 @@ # -*- mode: python -*- +import os, sys block_cipher = None +hiddenimports_strategies = [ + 'dexbot', + 'dexbot.strategies', + 'dexbot.strategies.echo', + 'dexbot.strategies.simple', + 'dexbot.strategies.storagedemo', + 'dexbot.strategies.walls', +] + +hiddenimports_packaging = [ + 'packaging', 'packaging.version', 'packaging.specifiers', 'packaging.requirements' +] a = Analysis(['app.py'], binaries=[], - datas=[('config.yml', '.')], - hiddenimports=[], + datas=[], + hiddenimports=hiddenimports_packaging + hiddenimports_strategies, hookspath=['hooks'], runtime_hooks=['hooks/rthook-Crypto.py'], excludes=[], @@ -22,9 +35,16 @@ exe = EXE(pyz, a.binaries, a.zipfiles, a.datas, - name='DEXBot', - debug=False, + name=os.path.join('dist', 'DEXBot-gui' + ('.exe' if sys.platform == 'win32' else '')), + debug=True, strip=False, + icon=None, upx=True, runtime_tmpdir=None, - console=False) + console=True) + +if sys.platform == 'darwin': + app = BUNDLE(exe, + name='DEXBot-gui.app', + icon=None) + diff --git a/cli.spec b/cli.spec new file mode 100644 index 000000000..18daef423 --- /dev/null +++ b/cli.spec @@ -0,0 +1,44 @@ +# -*- mode: python -*- + +import os, sys +block_cipher = None + + +hiddenimports_strategies = [ + 'dexbot', + 'dexbot.strategies', + 'dexbot.strategies.echo', + 'dexbot.strategies.follow_orders', + 'dexbot.strategies.simple', + 'dexbot.strategies.storagedemo', + 'dexbot.strategies.walls', +] + +hiddenimports_packaging = [ + 'packaging', 'packaging.version', 'packaging.specifiers', 'packaging.requirements' +] + +a = Analysis(['cli.py'], + binaries=[], + datas=[], + hiddenimports=hiddenimports_packaging + hiddenimports_strategies, + hookspath=['hooks'], + runtime_hooks=['hooks/rthook-Crypto.py'], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name=os.path.join('dist', 'DEXBot-cli' + ('.exe' if sys.platform == 'win32' else '')), + debug=True, + strip=False, + upx=True, + runtime_tmpdir=None, + console=True ) diff --git a/hooks/hook-Crypto.py b/hooks/hook-Crypto.py index 5048d1c78..380a46c8f 100644 --- a/hooks/hook-Crypto.py +++ b/hooks/hook-Crypto.py @@ -4,6 +4,7 @@ 'Crypto.Cipher._chacha20', 'Crypto.Cipher._raw_aes', 'Crypto.Cipher._raw_ecb', + 'Crypto.Cipher._raw_cbc', 'Crypto.Hash._SHA256', 'Crypto.Util._cpuid', 'Crypto.Util._strxor', From 862fa4b8e90dca07c1ebcdc03f393bbda6c4faf1 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 1 Mar 2018 21:21:27 +0100 Subject: [PATCH 003/187] - added travis slack notification encrypted token. Generated using the travis command line tool: $ travis encrypt "dexbotdevelop:$TOKEN" --add notifications.slack where TOKEN is the auth token for the slack app --- .travis.yml | 5 ++++- appveyor.yml | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 64982742d..713ba3a0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,4 +32,7 @@ deploy: file: dist/*.tar.gz file_glob: true on: - tags: true \ No newline at end of file + tags: true +notifications: + slack: + secure: E8FenKw66AwWBvMZawROqlYaLa7L+wHy2SflZK1EKzmFEH8ocD9DbIaVKPoLaHV/rXVMth+1TGqKS0qgRS6ra2Yqx6zCXgcpIy007QTuneyUeggYlH2jfDo4sm4LrgF+1AwK0O63vZIuz8R/BQi/MkwbQYzXjzFedcW9VzAMiNHGgBhjgRng7ND11D1zkCi3vJIpLQcC2rQmZrkoTyrgL+Pcw6PzeaVaohhKIWomKjx72ILmJlw2IDIzFjOhXADcADHKP3NPPqDKvuancoiCU1d3maEzQLIrAqEPm7XQCC+iPi6rAWdsbJuYzElOXR1KRcj/YCpWG7jVQ403qmRIi9bNbEZVd4Whl5YhYNJgzQK9eC5Fg0s14H4fiOJnJPZJYxc0rmc0uPN8mnvksGorowa5vAAaFmp6KN6ZCoQAela/oj0Ro5a44y0rBkY6xoKtTurHEcntFGpENQl8ggNrZM3rJNRykrOhQgZJUrOh2JQCBbd8OT5PMGE7lSn/GcvKzQkeuaKdPKDcTesMLXKIoEXmbj/Pf1RTV6JujW8bmNNoa1Ayf6bhXnaDqrbIweW6AEjzfEMx5oVcf9poXdLxzZl9lnyufvYacXwlz37Ti5E4IBnMs1s3GF7Luo/SziLXYpTpSYeqhiEQiDmPTeQaaj/J+QwQ6deSvyz+njGtFjg= diff --git a/appveyor.yml b/appveyor.yml index 48bf91915..2ca73cc46 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -60,6 +60,7 @@ deploy: #---------------------------------# notifications: - # Slack - #- provider: Slack - # incoming_webhook: http://dexbotdevelop.slack.com \ No newline at end of file + - provider: Slack + auth_token: + secure: Vm+uFzL141tuVvoyqN9lBZ6gKXqtascImAycAcsfgoFd1nG7JyxkHjax01TMn0IMyu28qdfOpEiqpfMsxKcmLkrbZAVatGKDFHN3FbU5sZw= + channel: '#ci' From e11d7d1c7bdd66bd163d555c3bd7c6ab934a98d6 Mon Sep 17 00:00:00 2001 From: sergio Date: Tue, 6 Mar 2018 14:28:26 +0100 Subject: [PATCH 004/187] - install python3 for osx --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 713ba3a0b..6ca6b56c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ matrix: - os: osx language: generic before_install: - - brew update + - python --version - brew install python3 - - virtualenv venv -p python3 - - source venv/bin/activate + - alias python=python3 + - python --version install: - python --version - pip install click-datetime PyQt5 pyinstaller From 1e98b66d204c74036a32f4e43b5f0eb3abf1926e Mon Sep 17 00:00:00 2001 From: sergio Date: Tue, 6 Mar 2018 15:08:34 +0100 Subject: [PATCH 005/187] - Added pyqt5 + pyqt-distutils + click-datetime via pip (non existent in PyPi) to solve #17 - Added 'package' make target to build executables --- .travis.yml | 8 ++------ Makefile | 12 +++++++++--- README.md | 18 ++++++++++-------- appveyor.yml | 2 +- requirements.txt | 4 ++++ setup.py | 3 --- 6 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml index 6ca6b56c8..0b4635f82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,13 +8,9 @@ matrix: language: generic before_install: - python --version - - brew install python3 - - alias python=python3 - - python --version + - brew upgrade python install: - - python --version - - pip install click-datetime PyQt5 pyinstaller - - python setup.py install + - make install script: - echo "@TODO - Running tests..." - pyinstaller --distpath dist/$TRAVIS_OS_NAME app.spec diff --git a/Makefile b/Makefile index b9760ea75..c722043cf 100644 --- a/Makefile +++ b/Makefile @@ -12,11 +12,13 @@ clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + +pip: + pip install -r requirements.txt lint: flake8 dexbot/ -build: +build: pip python3 setup.py build install: build @@ -29,10 +31,14 @@ git: git push --all git push --tags -check: +check: pip python3 setup.py check -dist: +package: build + pyinstaller app.spec + pyinstaller cli.spec + +dist: pip python3 setup.py sdist upload -r pypi python3 setup.py bdist_wheel upload diff --git a/README.md b/README.md index 25f9860b9..27afcb096 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,22 @@ Trading Bot for the BitShares Decentralized Exchange ## Build status master: -[![Build Status](https://travis-ci.org/svdev/DEXBot.svg?branch=master)](https://travis-ci.org/svdev/DEXBot) +[![Build Status](https://travis-ci.org/Codaone/DEXBot.svg?branch=master)](https://travis-ci.org/Codaone/DEXBot) -develop: -[![Build Status](https://travis-ci.org/svdev/DEXBot.svg?branch=develop)](https://travis-ci.org/svdev/DEXBot) **Warning**: This is highly experimental code! Use at your OWN risk! ## Installation - git clone https://github.com/codaone/dexbot - cd dexbot - python3 setup.py install - # or - python3 setup.py install --user +Python 3.4+ + pip are required. + + $ git clone https://github.com/codaone/dexbot + $ cd dexbot + $ make install + +or + + $ make install-user ## Configuration diff --git a/appveyor.yml b/appveyor.yml index 2ca73cc46..dd3f3a940 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,7 +20,7 @@ configuration: Release install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python --version" - - "pip install pyinstaller click-datetime PyQt5" + - "pip install pyinstaller click-datetime PyQt5 pyqt-distutils" - "python setup.py install" after_test: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..fc6d4f96a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pyqt5==5.10 +pyqt-distutils==0.7.3 +click-datetime==0.2 +pyinstaller==3.3.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 1402d20a9..2ebecdb7f 100755 --- a/setup.py +++ b/setup.py @@ -52,11 +52,8 @@ def run(self): "bitshares==0.1.11.beta", "uptick>=0.1.4", "click", - "click-datetime", "sqlalchemy", "appdirs", - "pyqt5", - 'pyqt-distutils', "ruamel.yaml" ], dependency_links=[ From 370795d3e5c86b794312e6450090d6a89df7b9a3 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 8 Mar 2018 21:56:17 +1100 Subject: [PATCH 006/187] a very basic test suite --- dexbot/test.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100755 dexbot/test.py diff --git a/dexbot/test.py b/dexbot/test.py new file mode 100755 index 000000000..eff757d09 --- /dev/null +++ b/dexbot/test.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 + +from bitshares.bitshares import BitShares +import unittest +import time +import threading +import logging +from dexbot.bot import BotInfrastructure + + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(levelname)s %(message)s' + ) + + +TEST_CONFIG={ + 'node':'wss://node.testnet.bitshares.eu', + 'bots':{ + 'echo': + { + 'account':'aud.bot.test4', + 'market':'TESTUSD:TEST', + 'module':'dexbot.strategies.echo' + }, + 'follow_orders': + { + 'account':'aud.bot.test4', + 'market':'TESTUSD:TEST', + 'module':'dexbot.strategies.follow_orders', + 'spread':5, + 'reset':True, + 'staggers':2, + 'wall':5, + 'staggerspread':5, + 'min':0, + 'max':100000, + 'start':50 + }}} + +KEYS=['5JV32w3BgPgHV1VoELuDQxvt1gdfuXHo2Rm8TrEn6SQwSsLjnH8'] + + + +class TestDexbot(unittest.TestCase): + + def test_dexbot(self): + btsi = BitShares(node=TEST_CONFIG['node'], keys=KEYS) + bi = BotInfrastructure(config=TEST_CONFIG, bitshares_instance=btsi) + def wait_then_stop(): + time.sleep(20) + bi.do_next_tick(bi.stop) + t = threading.Thread(target=wait_then_stop) + t.start() + bi.run() + t.join() + +if __name__=='__main__': + unittest.main() From fb8739e8fe2f52ae398146161cb90e4f3d5e736d Mon Sep 17 00:00:00 2001 From: Nikolay Date: Thu, 8 Mar 2018 17:02:35 +0200 Subject: [PATCH 007/187] Add dynamic calculation of center price --- dexbot/basestrategy.py | 23 ++++++++++++++++++++++- dexbot/strategies/simple.py | 25 ++++++++++++++++++------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index f2c9d79bf..93763867a 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -123,7 +123,28 @@ def __init__( 'account': self.bot['account'], 'market': self.bot['market'], 'is_disabled': lambda: self.disabled}) - + + @property + def calculate_center_price(self): + ticker = self.market.ticker() + highest_bid = ticker.get("highestBid") + lowest_ask = ticker.get("lowestAsk") + if highest_bid is None or highest_bid == 0.0: + self.log.critical( + "Cannot estimate center price, there is no highest bid." + ) + self.disabled = True + return None + if lowest_ask is None or lowest_ask == 0.0: + self.log.critical( + "Cannot estimate center price, there is no highest bid." + ) + self.disabled = True + return None + else: + center_price = (highest_bid['price'] + lowest_ask['price']) / 2 + return center_price + @property def orders(self): """ Return the bot's open accounts in the current market diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index 535d245df..50430a1b7 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -28,22 +28,31 @@ def __init__(self, *args, **kwargs): # Counter for blocks self.counter = Counter() - self.price = self.bot.get("target", {}).get("center_price", 0) - target = self.bot.get("target", {}) - self.buy_price = self.price * (1 - (target["spread"] / 2) / 100) - self.sell_price = self.price * (1 + (target["spread"] / 2) / 100) + self.target = self.bot.get("target", {}) + self.center_price = None + self.buy_price = None + self.sell_price = None + self.calculate_order_prices() + self.initial_balance = self['initial_balance'] or 0 self.bot_name = kwargs.get('name') self.view = kwargs.get('view') + def calculate_order_prices(self): + self.center_price = self.calculate_center_price + self.buy_price = self.center_price * (1 - (self.target["spread"] / 2) / 100) + self.sell_price = self.center_price * (1 + (self.target["spread"] / 2) / 100) + def error(self, *args, **kwargs): self.disabled = True self.log.info(self.execute()) def init_strategy(self): # Target - target = self.bot.get("target", {}) - amount = target['amount'] / 2 + amount = self.target['amount'] / 2 + + # Recalculate buy and sell order prices + self.calculate_order_prices() # Buy Side if float(self.balance(self.market["base"])) < self.buy_price * amount: @@ -93,7 +102,9 @@ def update_orders(self, new_sell_order, new_buy_order): sell_order = self['sell_order'] buy_order = self['buy_order'] - sell_price = self.sell_price + # Recalculate buy and sell order prices + self.calculate_order_prices() + buy_price = self.buy_price sold_amount = 0 From 545ce94a0c467237dc465dea580945b19bfd90e4 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Fri, 9 Mar 2018 03:51:00 +1100 Subject: [PATCH 008/187] Move to subdir. PEP8ify. Longer var names --- dexbot/test.py | 59 ----------------------------------------- dexbot/tests/test.py | 62 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 59 deletions(-) delete mode 100755 dexbot/test.py create mode 100755 dexbot/tests/test.py diff --git a/dexbot/test.py b/dexbot/test.py deleted file mode 100755 index eff757d09..000000000 --- a/dexbot/test.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/python3 - -from bitshares.bitshares import BitShares -import unittest -import time -import threading -import logging -from dexbot.bot import BotInfrastructure - - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s %(levelname)s %(message)s' - ) - - -TEST_CONFIG={ - 'node':'wss://node.testnet.bitshares.eu', - 'bots':{ - 'echo': - { - 'account':'aud.bot.test4', - 'market':'TESTUSD:TEST', - 'module':'dexbot.strategies.echo' - }, - 'follow_orders': - { - 'account':'aud.bot.test4', - 'market':'TESTUSD:TEST', - 'module':'dexbot.strategies.follow_orders', - 'spread':5, - 'reset':True, - 'staggers':2, - 'wall':5, - 'staggerspread':5, - 'min':0, - 'max':100000, - 'start':50 - }}} - -KEYS=['5JV32w3BgPgHV1VoELuDQxvt1gdfuXHo2Rm8TrEn6SQwSsLjnH8'] - - - -class TestDexbot(unittest.TestCase): - - def test_dexbot(self): - btsi = BitShares(node=TEST_CONFIG['node'], keys=KEYS) - bi = BotInfrastructure(config=TEST_CONFIG, bitshares_instance=btsi) - def wait_then_stop(): - time.sleep(20) - bi.do_next_tick(bi.stop) - t = threading.Thread(target=wait_then_stop) - t.start() - bi.run() - t.join() - -if __name__=='__main__': - unittest.main() diff --git a/dexbot/tests/test.py b/dexbot/tests/test.py new file mode 100755 index 000000000..e1d7ce7eb --- /dev/null +++ b/dexbot/tests/test.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 + +from bitshares.bitshares import BitShares +import unittest +import time +import threading +import logging +from dexbot.bot import BotInfrastructure + + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(levelname)s %(message)s' +) + + +TEST_CONFIG = { + 'node': 'wss://node.testnet.bitshares.eu', + 'bots': { + 'echo': + { + 'account': 'aud.bot.test4', + 'market': 'TESTUSD:TEST', + 'module': 'dexbot.strategies.echo' + }, + 'follow_orders': + { + 'account': 'aud.bot.test4', + 'market': 'TESTUSD:TEST', + 'module': 'dexbot.strategies.follow_orders', + 'spread': 5, + 'reset': True, + 'staggers': 2, + 'wall': 5, + 'staggerspread': 5, + 'min': 0, + 'max': 100000, + 'start': 50 + }}} + +KEYS = ['5JV32w3BgPgHV1VoELuDQxvt1gdfuXHo2Rm8TrEn6SQwSsLjnH8'] + + +class TestDexbot(unittest.TestCase): + + def test_dexbot(self): + bitshares_instance = BitShares(node=TEST_CONFIG['node'], keys=KEYS) + bot_infrastructure = BotInfrastructure(config=TEST_CONFIG, + bitshares_instance=bitshares_instance) + + def wait_then_stop(): + time.sleep(20) + bitshares_instance.do_next_tick(bitshares_instance.stop) + + stopper = threading.Thread(target=wait_then_stop) + stopper.start() + bot_infrastructure.run() + stopper.join() + + +if __name__ == '__main__': + unittest.main() From 9299c672b6b9399006d5c23c4af8bcfb86bb623b Mon Sep 17 00:00:00 2001 From: Nikolay Date: Thu, 8 Mar 2018 19:50:36 +0200 Subject: [PATCH 009/187] Add checkbox for automatic center price --- dexbot/controllers/create_bot_controller.py | 4 ++++ dexbot/strategies/simple.py | 9 +++++++-- dexbot/views/create_bot.py | 9 +++++++++ dexbot/views/edit_bot.py | 19 ++++++++++++++++++- dexbot/views/ui/create_bot_window.ui | 17 +++++++++++++++-- dexbot/views/ui/edit_bot_window.ui | 16 +++++++++++++--- 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_bot_controller.py index 5854413a8..3f0bd44f9 100644 --- a/dexbot/controllers/create_bot_controller.py +++ b/dexbot/controllers/create_bot_controller.py @@ -134,6 +134,10 @@ def get_target_amount(bot_data): def get_target_center_price(bot_data): return bot_data['target']['center_price'] + @staticmethod + def get_target_center_price_automatic(bot_data): + return bot_data['target']['center_price_automatic'] + @staticmethod def get_target_spread(bot_data): return bot_data['target']['spread'] diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index 50430a1b7..47ad8b2e7 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -29,7 +29,11 @@ def __init__(self, *args, **kwargs): self.counter = Counter() self.target = self.bot.get("target", {}) - self.center_price = None + self.is_center_price_automatic = self.target["center_price_automatic"] + if self.is_center_price_automatic: + self.center_price = None + else: + self.center_price = self.target["center_price"] self.buy_price = None self.sell_price = None self.calculate_order_prices() @@ -39,7 +43,8 @@ def __init__(self, *args, **kwargs): self.view = kwargs.get('view') def calculate_order_prices(self): - self.center_price = self.calculate_center_price + if self.is_center_price_automatic: + self.center_price = self.calculate_center_price self.buy_price = self.center_price * (1 - (self.target["spread"] / 2) / 100) self.sell_price = self.center_price * (1 + (self.target["spread"] / 2) / 100) diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 2c88541f4..8e951f03e 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -22,6 +22,14 @@ def __init__(self, controller): self.ui.save_button.clicked.connect(self.handle_save) self.ui.cancel_button.clicked.connect(self.reject) + self.ui.center_price_automatic_checkbox.stateChanged.connect(self.onchange_center_price_automatic_checkbox) + + def onchange_center_price_automatic_checkbox(self): + checkbox = self.ui.center_price_automatic_checkbox + if checkbox.isChecked(): + self.ui.center_price_input.setDisabled(True) + else: + self.ui.center_price_input.setDisabled(False) def validate_bot_name(self): bot_name = self.ui.bot_name_input.text() @@ -82,6 +90,7 @@ def handle_save(self): target = { 'amount': float(ui.amount_input.text()), 'center_price': float(ui.center_price_input.text()), + 'center_price_automatic': bool(ui.center_price_automatic_checkbox.isChecked()), 'spread': spread } diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index 3246c30d7..f89af87b8 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -21,10 +21,26 @@ def __init__(self, controller, botname, config): self.account_name.setText(self.controller.get_account(bot_data)) self.amount_input.setValue(self.controller.get_target_amount(bot_data)) self.center_price_input.setValue(self.controller.get_target_center_price(bot_data)) - self.spread_input.setValue(self.controller.get_target_spread(bot_data)) + center_price_automatic = self.controller.get_target_center_price_automatic(bot_data) + if center_price_automatic: + self.center_price_input.setEnabled(False) + self.center_price_automatic_checkbox.setChecked(True) + else: + self.center_price_input.setEnabled(True) + self.center_price_automatic_checkbox.setChecked(False) + + self.spread_input.setValue(self.controller.get_target_spread(bot_data)) self.save_button.clicked.connect(self.handle_save) self.cancel_button.clicked.connect(self.reject) + self.center_price_automatic_checkbox.stateChanged.connect(self.onchange_center_price_automatic_checkbox) + + def onchange_center_price_automatic_checkbox(self): + checkbox = self.center_price_automatic_checkbox + if checkbox.isChecked(): + self.center_price_input.setDisabled(True) + else: + self.center_price_input.setDisabled(False) def validate_bot_name(self): old_bot_name = self.bot_name @@ -78,6 +94,7 @@ def handle_save(self): target = { 'amount': float(self.amount_input.text()), 'center_price': float(self.center_price_input.text()), + 'center_price_automatic': bool(self.center_price_automatic_checkbox.isChecked()), 'spread': spread } diff --git a/dexbot/views/ui/create_bot_window.ui b/dexbot/views/ui/create_bot_window.ui index 1d2bc26bc..321bddaa4 100644 --- a/dexbot/views/ui/create_bot_window.ui +++ b/dexbot/views/ui/create_bot_window.ui @@ -269,6 +269,9 @@ + + false + 0 @@ -301,7 +304,7 @@ - + @@ -329,7 +332,7 @@ - + @@ -357,6 +360,16 @@ + + + + Calculate center price automatically + + + true + + + diff --git a/dexbot/views/ui/edit_bot_window.ui b/dexbot/views/ui/edit_bot_window.ui index b986bf84a..ea598ddc3 100644 --- a/dexbot/views/ui/edit_bot_window.ui +++ b/dexbot/views/ui/edit_bot_window.ui @@ -7,7 +7,7 @@ 0 0 400 - 430 + 458 @@ -286,6 +286,9 @@ + + false + 0 @@ -318,7 +321,7 @@ - + @@ -346,7 +349,7 @@ - + @@ -374,6 +377,13 @@ + + + + Calculate center price automatically + + + From b7ae7973a444b8119a9d6505b446d91a2668892c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 9 Mar 2018 08:41:00 +0200 Subject: [PATCH 010/187] Change encrypted travis api key --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0b4635f82..03d862290 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ deploy: provider: releases skip_cleanup: true api_key: - secure: railzqElBKAEt0f1o1Z0sqaD4FZ3WkwhCxMJYTo1TmwkmS0r3f57n5LHOGd43MY/ZYbQhyoxZ0DE5Q+16+gSLVn2j5pLTMjBbBn4iqntgFFf4L6hHPsv67yFHSUz9l/T/2TKRCMbQzWTnvE8wQVFVnoSVc7yWgNIBoEVhA1vcPd17OUeCgZQcNN3YvGpJQhEOhoW+wByiskweV6tTTxtOmX3aQdJQPD6Tt/pbKdN4hHInJmrZjvUnH/qpQEgcgsOFiZE7er+zFIXxPdFV/XV2GqlUMob0rAmun6sGDpqZwTqSOmxZunquQCpmzMLqlW9lB3UUDsZs18amwDniq2rBEBUL6MEsgxEfYbmxMungNd7v3CV5zeq6QZen50Cwmhs8nhr3UIxUJzk5FfQCekFS4pxWvl+/QPW+oxKPTNa3oe4egzuDTTqY2XocuAVEeSHs72OFWqD0LYnHkV7APGQTDaJhcGvLSe0YcBX1R1KVz1JlTapdlZIJ4Ba3MOqgBaAybptkJ0CzaX0mcDh1R8CwcM2NHaFr4w2HFhgJSLtb3AbguOj5O+tdXvOlPq9n8ColH7Cuzch3t+ZnpSg3Q5qR5WqcVSP0AyfqKoNmSEA1exiqJp295vJnWeLkzlAcT0cMTsLAlQ6rSrR9IlUfmamuZuY+ypCxUQuunGdE0/s4J4= + secure: YHAPA2G3qu7at2hMu4AplXH/niI1ChlgldJVetaKO92iDQiyOk5VqFfhV1ec+nYdX8rtniwfD7YJr2nG2x1ATwKw4MyFcJEXqaOUmKWTeZ/Q3PnQQsa+2BnN4Rfz1aynpsKHDYS9gCU/YTqymujE8bdlxW1WtpYOqOSDkspGxZGZTiUKQ7/qhrjB3Dywm/KF9WEoba/X7tbhmSuU8sL45gBGY008TXZRWqAPM42qa/aBIrG/cIA865VlCUltPC6vzskcWI5q1UtYh6g2CiXJghcpFEO2rWWXmS1A+5nQp6ptJigjRgnhyFHmHb27lRM8aRGRDTeyJvlNuoyIvNj/FxhLXZvomgTyGyzTIl67WIXcxWMKx6KqqrqGyiooRMeFpDEYobZL/FY9whi3M+gUwsofAVQ6oL4a1L185egaXlMKGbM5GYB4OxVLsVtL2c0pJjvNIkCGGDzaqNpdo+vZflB4iCwvw548rWJsqsHnP1XMo28ZU86hibD7V0x+JW2BJEI0lMvOkRBslOhYBafIsbZakO4Zf4d+5b2dd8/xY/wTbuxwgDuBOmpqoByVYeCBah4bbnb8JS6eze+vUyxaI1XLAdQXbLQ788Agr2jdHGuy1wI8io9g5vtzS5oOyq8YFBM1tVKM2Mtw5nkSsTbPJsZg8m/kkre6qiXJl2gPQTE= file: dist/*.tar.gz file_glob: true on: From 710dce08e81b44f0bdffd83df34ffdf9be091f70 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Fri, 9 Mar 2018 09:04:25 +0200 Subject: [PATCH 011/187] Change naming, automatic to dynamic --- dexbot/basestrategy.py | 2 +- dexbot/strategies/simple.py | 21 ++++++++++----------- dexbot/views/create_bot.py | 8 ++++---- dexbot/views/edit_bot.py | 16 ++++++++-------- dexbot/views/ui/create_bot_window.ui | 6 +++--- dexbot/views/ui/edit_bot_window.ui | 4 ++-- 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 93763867a..c2fee240e 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -137,7 +137,7 @@ def calculate_center_price(self): return None if lowest_ask is None or lowest_ask == 0.0: self.log.critical( - "Cannot estimate center price, there is no highest bid." + "Cannot estimate center price, there is no lowest ask." ) self.disabled = True return None diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index 47ad8b2e7..7896854ee 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -29,11 +29,12 @@ def __init__(self, *args, **kwargs): self.counter = Counter() self.target = self.bot.get("target", {}) - self.is_center_price_automatic = self.target["center_price_automatic"] - if self.is_center_price_automatic: + self.is_center_price_dynamic = self.target["center_price_dynamic"] + if self.is_center_price_dynamic: self.center_price = None else: self.center_price = self.target["center_price"] + self.buy_price = None self.sell_price = None self.calculate_order_prices() @@ -43,8 +44,9 @@ def __init__(self, *args, **kwargs): self.view = kwargs.get('view') def calculate_order_prices(self): - if self.is_center_price_automatic: + if self.is_center_price_dynamic: self.center_price = self.calculate_center_price + self.buy_price = self.center_price * (1 - (self.target["spread"] / 2) / 100) self.sell_price = self.center_price * (1 + (self.target["spread"] / 2) / 100) @@ -53,7 +55,6 @@ def error(self, *args, **kwargs): self.log.info(self.execute()) def init_strategy(self): - # Target amount = self.target['amount'] / 2 # Recalculate buy and sell order prices @@ -110,8 +111,6 @@ def update_orders(self, new_sell_order, new_buy_order): # Recalculate buy and sell order prices self.calculate_order_prices() - buy_price = self.buy_price - sold_amount = 0 if new_sell_order and new_sell_order['base']['amount'] < sell_order['base']['amount']: # Some of the sell order was sold @@ -137,7 +136,7 @@ def update_orders(self, new_sell_order, new_buy_order): new_buy_amount = buy_order_amount - bought_amount + sold_amount if float(self.balance(self.market["base"])) < new_buy_amount: self.log.critical( - 'Insufficient buy balance, needed {} {}'.format(buy_price * new_buy_amount, + 'Insufficient buy balance, needed {} {}'.format(self.buy_price * new_buy_amount, self.market['base']['symbol']) ) self.disabled = True @@ -147,14 +146,14 @@ def update_orders(self, new_sell_order, new_buy_order): self.cancel(buy_order) buy_transaction = self.market.buy( - buy_price, + self.buy_price, Amount(amount=new_buy_amount, asset=self.market["quote"]), account=self.account, returnOrderId="head" ) buy_order = self.get_order(buy_transaction['orderid']) self.log.info( - 'Placed a buy order for {} {} @ {}'.format(new_buy_amount, self.market["quote"], buy_price) + 'Placed a buy order for {} {} @ {}'.format(new_buy_amount, self.market["quote"], self.buy_price) ) if buy_order: self['buy_order'] = buy_order @@ -180,14 +179,14 @@ def update_orders(self, new_sell_order, new_buy_order): self.cancel(sell_order) sell_transaction = self.market.sell( - sell_price, + self.sell_price, Amount(amount=new_sell_amount, asset=self.market["quote"]), account=self.account, returnOrderId="head" ) sell_order = self.get_order(sell_transaction['orderid']) self.log.info( - 'Placed a sell order for {} {} @ {}'.format(new_sell_amount, self.market["quote"], buy_price) + 'Placed a sell order for {} {} @ {}'.format(new_sell_amount, self.market["quote"], self.buy_price) ) if sell_order: self['sell_order'] = sell_order diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 8e951f03e..57145a89c 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -22,10 +22,10 @@ def __init__(self, controller): self.ui.save_button.clicked.connect(self.handle_save) self.ui.cancel_button.clicked.connect(self.reject) - self.ui.center_price_automatic_checkbox.stateChanged.connect(self.onchange_center_price_automatic_checkbox) + self.ui.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) - def onchange_center_price_automatic_checkbox(self): - checkbox = self.ui.center_price_automatic_checkbox + def onchange_center_price_dynamic_checkbox(self): + checkbox = self.ui.center_price_dynamic_checkbox if checkbox.isChecked(): self.ui.center_price_input.setDisabled(True) else: @@ -90,7 +90,7 @@ def handle_save(self): target = { 'amount': float(ui.amount_input.text()), 'center_price': float(ui.center_price_input.text()), - 'center_price_automatic': bool(ui.center_price_automatic_checkbox.isChecked()), + 'center_price_dynamic': bool(ui.center_price_dynamic_checkbox.isChecked()), 'spread': spread } diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index f89af87b8..404bcfa78 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -22,21 +22,21 @@ def __init__(self, controller, botname, config): self.amount_input.setValue(self.controller.get_target_amount(bot_data)) self.center_price_input.setValue(self.controller.get_target_center_price(bot_data)) - center_price_automatic = self.controller.get_target_center_price_automatic(bot_data) - if center_price_automatic: + center_price_dynamic = self.controller.get_target_center_price_dynamic(bot_data) + if center_price_dynamic: self.center_price_input.setEnabled(False) - self.center_price_automatic_checkbox.setChecked(True) + self.center_price_dynamic_checkbox.setChecked(True) else: self.center_price_input.setEnabled(True) - self.center_price_automatic_checkbox.setChecked(False) + self.center_price_dynamic_checkbox.setChecked(False) self.spread_input.setValue(self.controller.get_target_spread(bot_data)) self.save_button.clicked.connect(self.handle_save) self.cancel_button.clicked.connect(self.reject) - self.center_price_automatic_checkbox.stateChanged.connect(self.onchange_center_price_automatic_checkbox) + self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) - def onchange_center_price_automatic_checkbox(self): - checkbox = self.center_price_automatic_checkbox + def onchange_center_price_dynamic_checkbox(self): + checkbox = self.center_price_dynamic_checkbox if checkbox.isChecked(): self.center_price_input.setDisabled(True) else: @@ -94,7 +94,7 @@ def handle_save(self): target = { 'amount': float(self.amount_input.text()), 'center_price': float(self.center_price_input.text()), - 'center_price_automatic': bool(self.center_price_automatic_checkbox.isChecked()), + 'center_price_dynamic': bool(self.center_price_dynamic_checkbox.isChecked()), 'spread': spread } diff --git a/dexbot/views/ui/create_bot_window.ui b/dexbot/views/ui/create_bot_window.ui index 321bddaa4..8f8d0e824 100644 --- a/dexbot/views/ui/create_bot_window.ui +++ b/dexbot/views/ui/create_bot_window.ui @@ -7,7 +7,7 @@ 0 0 418 - 474 + 507 @@ -361,9 +361,9 @@ - + - Calculate center price automatically + Calculate center price dynamically true diff --git a/dexbot/views/ui/edit_bot_window.ui b/dexbot/views/ui/edit_bot_window.ui index ea598ddc3..6a682d7f6 100644 --- a/dexbot/views/ui/edit_bot_window.ui +++ b/dexbot/views/ui/edit_bot_window.ui @@ -378,9 +378,9 @@ - + - Calculate center price automatically + Calculate center price dynamically From 66c3a141963bab030f1801d2a644e15f46723f49 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 9 Mar 2018 09:33:54 +0200 Subject: [PATCH 012/187] Change encrypted auth token for appveyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index dd3f3a940..aee246a9d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -51,7 +51,7 @@ deploy: prerelease: false force_update: true auth_token: - secure: DNkVbBiLYkrWapXn2ioCY3Qbkn5jlsNNyx2G7gOR86OCEqEnOWSxvMQp/Yoq4jJ3 + secure: 9qvwlVUHFBV4GwMz1Gu2HSnqU8Ex2nv5dsY4mVNCurrb+6ULIoHPgbvJPWTo3qV6 on: appveyor_repo_tag: true # deploy on tag push only From 1e2980c6caf638737bda0238e28e42232a6b1b94 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 9 Mar 2018 14:56:07 +0200 Subject: [PATCH 013/187] Add install instructions without make --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27afcb096..fcb80d9c9 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,28 @@ master: ## Installation -Python 3.4+ + pip are required. +Python 3.4+ + pip are required. With make: $ git clone https://github.com/codaone/dexbot $ cd dexbot $ make install or - + $ make install-user +Without make: + + $ git clone https://github.com/codaone/dexbot + $ cd dexbot + $ pip install -r requirements.txt + $ python setup.py install + +or + + $ pip install -r --user requirements.txt + $ python setup.py install --user + ## Configuration Configuration happens in `config.yml` From 98373d9e80ecad6c623696e4d8305556b1359ad3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Mar 2018 08:55:53 +0200 Subject: [PATCH 014/187] Change wording in the readme --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fcb80d9c9..823fe4905 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # DEXBot -Trading Bot for the BitShares Decentralized Exchange -(DEX). +Trading Bot for the BitShares Decentralized Exchange (DEX). ## Build status @@ -13,7 +12,7 @@ master: ## Installation -Python 3.4+ + pip are required. With make: +Python 3.4+ & pip are required. With make: $ git clone https://github.com/codaone/dexbot $ cd dexbot From 3f59eb9825ad9d622aeb21bffe23fa9296d8a0cf Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Mar 2018 09:18:51 +0200 Subject: [PATCH 015/187] Change building language to python 3.6 for osx in .travis.yml --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 03d862290..39085f182 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,8 @@ matrix: language: python python: '3.6' - os: osx - language: generic + language: python + python: '3.6' before_install: - python --version - brew upgrade python From 9fe79c6637a7f4fcc116826561c764bdf0fe7456 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Mar 2018 09:32:28 +0200 Subject: [PATCH 016/187] Remove osx building from .travis.yml Travis currently doesn't support python on mac. See https://github.com/travis-ci/travis-ci/issues/2312 --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 39085f182..573e72f04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,6 @@ matrix: - os: linux language: python python: '3.6' - - os: osx - language: python - python: '3.6' before_install: - python --version - brew upgrade python From afdd4f107ff937ee3f4e0300fe0a750c17e0137c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Mar 2018 09:44:32 +0200 Subject: [PATCH 017/187] Remove brew line from .travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 573e72f04..d106a9a6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ matrix: python: '3.6' before_install: - python --version - - brew upgrade python install: - make install script: From 56ebd8e879db2fde9f71e05290ea1157fd344f7b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Mar 2018 09:56:00 +0200 Subject: [PATCH 018/187] Change email notification notifications in travis to false --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d106a9a6f..4475b306c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,5 +27,6 @@ deploy: on: tags: true notifications: + email: false slack: secure: E8FenKw66AwWBvMZawROqlYaLa7L+wHy2SflZK1EKzmFEH8ocD9DbIaVKPoLaHV/rXVMth+1TGqKS0qgRS6ra2Yqx6zCXgcpIy007QTuneyUeggYlH2jfDo4sm4LrgF+1AwK0O63vZIuz8R/BQi/MkwbQYzXjzFedcW9VzAMiNHGgBhjgRng7ND11D1zkCi3vJIpLQcC2rQmZrkoTyrgL+Pcw6PzeaVaohhKIWomKjx72ILmJlw2IDIzFjOhXADcADHKP3NPPqDKvuancoiCU1d3maEzQLIrAqEPm7XQCC+iPi6rAWdsbJuYzElOXR1KRcj/YCpWG7jVQ403qmRIi9bNbEZVd4Whl5YhYNJgzQK9eC5Fg0s14H4fiOJnJPZJYxc0rmc0uPN8mnvksGorowa5vAAaFmp6KN6ZCoQAela/oj0Ro5a44y0rBkY6xoKtTurHEcntFGpENQl8ggNrZM3rJNRykrOhQgZJUrOh2JQCBbd8OT5PMGE7lSn/GcvKzQkeuaKdPKDcTesMLXKIoEXmbj/Pf1RTV6JujW8bmNNoa1Ayf6bhXnaDqrbIweW6AEjzfEMx5oVcf9poXdLxzZl9lnyufvYacXwlz37Ti5E4IBnMs1s3GF7Luo/SziLXYpTpSYeqhiEQiDmPTeQaaj/J+QwQ6deSvyz+njGtFjg= From b775037866ad5bce205db1ae05822edd96a746ab Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Mar 2018 12:50:23 +0200 Subject: [PATCH 019/187] Change slack api key in .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4475b306c..89fd4c78d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,4 +29,4 @@ deploy: notifications: email: false slack: - secure: E8FenKw66AwWBvMZawROqlYaLa7L+wHy2SflZK1EKzmFEH8ocD9DbIaVKPoLaHV/rXVMth+1TGqKS0qgRS6ra2Yqx6zCXgcpIy007QTuneyUeggYlH2jfDo4sm4LrgF+1AwK0O63vZIuz8R/BQi/MkwbQYzXjzFedcW9VzAMiNHGgBhjgRng7ND11D1zkCi3vJIpLQcC2rQmZrkoTyrgL+Pcw6PzeaVaohhKIWomKjx72ILmJlw2IDIzFjOhXADcADHKP3NPPqDKvuancoiCU1d3maEzQLIrAqEPm7XQCC+iPi6rAWdsbJuYzElOXR1KRcj/YCpWG7jVQ403qmRIi9bNbEZVd4Whl5YhYNJgzQK9eC5Fg0s14H4fiOJnJPZJYxc0rmc0uPN8mnvksGorowa5vAAaFmp6KN6ZCoQAela/oj0Ro5a44y0rBkY6xoKtTurHEcntFGpENQl8ggNrZM3rJNRykrOhQgZJUrOh2JQCBbd8OT5PMGE7lSn/GcvKzQkeuaKdPKDcTesMLXKIoEXmbj/Pf1RTV6JujW8bmNNoa1Ayf6bhXnaDqrbIweW6AEjzfEMx5oVcf9poXdLxzZl9lnyufvYacXwlz37Ti5E4IBnMs1s3GF7Luo/SziLXYpTpSYeqhiEQiDmPTeQaaj/J+QwQ6deSvyz+njGtFjg= + secure: iQwBqvwq0HmEODoWI52pnNi2trfZ4ly5/fDPmkr6Ez8z9rm5XQ3CBLtpH7JpNdkyen5r+dVTczJDIOTBLpXwe/AzwFKLqZc/0pkXnxzNSENnm++/G6uqS0u5fMdYSoR4fJC1zjzEj2ly11OdS+wX3y9//hD13U96u3iO6T/7EXU2VYt82wekziJXzyfK4JeJMs1L5M2Oz7ZBwiHeAZ/3ZNjKE+9TX7S/mlmG+bNiQhv/wSin2AnsB1recgFjp17ZHq4cW+K77TDnRlPZ0bVsOhGYUtMlW9llidXZbunLj3qITIDl7dufowBG95PTHh+L2KDcPv7UCxlN02kXWuz3nL47UwD7BZcLMJ0RLYk4g+qNBrytgrmhH82gdmenzCQ4PgHI/U1/8hgiEyGlBZWUTXrso5EF3VBRUhCtu8dG/F+rdGHSfK1mZQyDPe6my9E888TvfcWWCpVNammAZicrGWU9nY3Rqn7DFodBL896iFPs1DJD5fTF1th6hHEyRSuKZC80irFZRoxccDPuDYVIfPExJH328tFeh75WOuzQt4QCBFiOsiFDlCYhnQ8tNw/MWntPQHwY8PkUlvpvelPCgfh73ihXtMD61/6Hq+lOijkGFhEzgpqmzL4mSUd/EQRJHLE9lAVvRGdrzlaIV6f4CirJkZSAgf4LuYDl2JMZ3kE= From 0c433841ed9d3c8f2b37d6830a222af656920395 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Mar 2018 13:17:37 +0200 Subject: [PATCH 020/187] Change slack api token for appveyor.yml --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index aee246a9d..098652b14 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -62,5 +62,5 @@ deploy: notifications: - provider: Slack auth_token: - secure: Vm+uFzL141tuVvoyqN9lBZ6gKXqtascImAycAcsfgoFd1nG7JyxkHjax01TMn0IMyu28qdfOpEiqpfMsxKcmLkrbZAVatGKDFHN3FbU5sZw= + secure: G9OMj9l2s3+lX8cRiNXXhuQJpnnjcBc0cqP8gzkdKVWqGA8vBTOIPGxD/536VKpeBH/5dJFQWT+vmnGS+XciaCg4hh5s6hDpnvePq2+uEYE= channel: '#ci' From 28b7544953e4161734457ed854bd2f6b4a31779e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Mar 2018 13:55:49 +0200 Subject: [PATCH 021/187] Change dexbot version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2ebecdb7f..f0cf6b4d3 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from pyqt_distutils.build_ui import build_ui -VERSION = '0.1.0' +VERSION = '0.1.2' class InstallCommand(install): From 1b3a7e655d3535fe5a1568abd5ff9ce348cc1989 Mon Sep 17 00:00:00 2001 From: sergio Date: Mon, 12 Mar 2018 15:09:40 +0100 Subject: [PATCH 022/187] - fix for travis osx build: https://travis-ci.org/Codaone/DEXBot/jobs/351290237 - Build using makefile for appveyor - set travis token as environment variables - deploy to github release for master branch commits --- .travis.yml | 32 +++++++++++++++++++++++--------- Makefile | 2 +- appveyor.yml | 22 ++++++++++++++++------ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 89fd4c78d..450a780e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,11 @@ matrix: - os: linux language: python python: '3.6' + - os: osx + language: generic before_install: - python --version + - brew upgrade python install: - make install script: @@ -16,16 +19,27 @@ before_deploy: - git config --local user.name "Travis" - git config --local user.email "travis@travis-ci.org" - git tag "$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)" - - tar -czvf dist/DEXBot-$TRAVIS_OS_NAME.tar.gz dist/$TRAVIS_OS_NAME + - tar -czvf dist/DEXBot-$TRAVIS_OS_NAME.tar.gz dist/$TRAVIS_OS_NAME/* deploy: - provider: releases - skip_cleanup: true - api_key: - secure: YHAPA2G3qu7at2hMu4AplXH/niI1ChlgldJVetaKO92iDQiyOk5VqFfhV1ec+nYdX8rtniwfD7YJr2nG2x1ATwKw4MyFcJEXqaOUmKWTeZ/Q3PnQQsa+2BnN4Rfz1aynpsKHDYS9gCU/YTqymujE8bdlxW1WtpYOqOSDkspGxZGZTiUKQ7/qhrjB3Dywm/KF9WEoba/X7tbhmSuU8sL45gBGY008TXZRWqAPM42qa/aBIrG/cIA865VlCUltPC6vzskcWI5q1UtYh6g2CiXJghcpFEO2rWWXmS1A+5nQp6ptJigjRgnhyFHmHb27lRM8aRGRDTeyJvlNuoyIvNj/FxhLXZvomgTyGyzTIl67WIXcxWMKx6KqqrqGyiooRMeFpDEYobZL/FY9whi3M+gUwsofAVQ6oL4a1L185egaXlMKGbM5GYB4OxVLsVtL2c0pJjvNIkCGGDzaqNpdo+vZflB4iCwvw548rWJsqsHnP1XMo28ZU86hibD7V0x+JW2BJEI0lMvOkRBslOhYBafIsbZakO4Zf4d+5b2dd8/xY/wTbuxwgDuBOmpqoByVYeCBah4bbnb8JS6eze+vUyxaI1XLAdQXbLQ788Agr2jdHGuy1wI8io9g5vtzS5oOyq8YFBM1tVKM2Mtw5nkSsTbPJsZg8m/kkre6qiXJl2gPQTE= - file: dist/*.tar.gz - file_glob: true - on: - tags: true + - provider: releases + skip_cleanup: true + api_key: + secure: YHAPA2G3qu7at2hMu4AplXH/niI1ChlgldJVetaKO92iDQiyOk5VqFfhV1ec+nYdX8rtniwfD7YJr2nG2x1ATwKw4MyFcJEXqaOUmKWTeZ/Q3PnQQsa+2BnN4Rfz1aynpsKHDYS9gCU/YTqymujE8bdlxW1WtpYOqOSDkspGxZGZTiUKQ7/qhrjB3Dywm/KF9WEoba/X7tbhmSuU8sL45gBGY008TXZRWqAPM42qa/aBIrG/cIA865VlCUltPC6vzskcWI5q1UtYh6g2CiXJghcpFEO2rWWXmS1A+5nQp6ptJigjRgnhyFHmHb27lRM8aRGRDTeyJvlNuoyIvNj/FxhLXZvomgTyGyzTIl67WIXcxWMKx6KqqrqGyiooRMeFpDEYobZL/FY9whi3M+gUwsofAVQ6oL4a1L185egaXlMKGbM5GYB4OxVLsVtL2c0pJjvNIkCGGDzaqNpdo+vZflB4iCwvw548rWJsqsHnP1XMo28ZU86hibD7V0x+JW2BJEI0lMvOkRBslOhYBafIsbZakO4Zf4d+5b2dd8/xY/wTbuxwgDuBOmpqoByVYeCBah4bbnb8JS6eze+vUyxaI1XLAdQXbLQ788Agr2jdHGuy1wI8io9g5vtzS5oOyq8YFBM1tVKM2Mtw5nkSsTbPJsZg8m/kkre6qiXJl2gPQTE= + file: dist/*.tar.gz + file_glob: true + on: + tags: true + - provider: releases + prerelease: true + overwrite: true + name: latest + api_key: + secure: YHAPA2G3qu7at2hMu4AplXH/niI1ChlgldJVetaKO92iDQiyOk5VqFfhV1ec+nYdX8rtniwfD7YJr2nG2x1ATwKw4MyFcJEXqaOUmKWTeZ/Q3PnQQsa+2BnN4Rfz1aynpsKHDYS9gCU/YTqymujE8bdlxW1WtpYOqOSDkspGxZGZTiUKQ7/qhrjB3Dywm/KF9WEoba/X7tbhmSuU8sL45gBGY008TXZRWqAPM42qa/aBIrG/cIA865VlCUltPC6vzskcWI5q1UtYh6g2CiXJghcpFEO2rWWXmS1A+5nQp6ptJigjRgnhyFHmHb27lRM8aRGRDTeyJvlNuoyIvNj/FxhLXZvomgTyGyzTIl67WIXcxWMKx6KqqrqGyiooRMeFpDEYobZL/FY9whi3M+gUwsofAVQ6oL4a1L185egaXlMKGbM5GYB4OxVLsVtL2c0pJjvNIkCGGDzaqNpdo+vZflB4iCwvw548rWJsqsHnP1XMo28ZU86hibD7V0x+JW2BJEI0lMvOkRBslOhYBafIsbZakO4Zf4d+5b2dd8/xY/wTbuxwgDuBOmpqoByVYeCBah4bbnb8JS6eze+vUyxaI1XLAdQXbLQ788Agr2jdHGuy1wI8io9g5vtzS5oOyq8YFBM1tVKM2Mtw5nkSsTbPJsZg8m/kkre6qiXJl2gPQTE= + file: dist/*.tar.gz + file_glob: true + skip_cleanup: true + on: + branch: master notifications: email: false slack: diff --git a/Makefile b/Makefile index c722043cf..3a9277531 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ clean-pyc: find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + pip: - pip install -r requirements.txt + python3 -m pip install -r requirements.txt lint: flake8 dexbot/ diff --git a/appveyor.yml b/appveyor.yml index 098652b14..7550b4251 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,14 +18,14 @@ build: off configuration: Release install: - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - "python --version" - - "pip install pyinstaller click-datetime PyQt5 pyqt-distutils" - - "python setup.py install" + - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%;C:\MinGW\bin + - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe + - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe + - python --version + - make install after_test: - - "pyinstaller app.spec" - - "pyinstaller cli.spec" + - make package - '7z a DEXBot-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-cli.exe' # @TODO: Run tests.. @@ -54,6 +54,16 @@ deploy: secure: 9qvwlVUHFBV4GwMz1Gu2HSnqU8Ex2nv5dsY4mVNCurrb+6ULIoHPgbvJPWTo3qV6 on: appveyor_repo_tag: true # deploy on tag push only + - provider: GitHub + name: latest + artifact: DEXBot-win64.zip + draft: false + prerelease: true + force_update: true + auth_token: + secure: 9qvwlVUHFBV4GwMz1Gu2HSnqU8Ex2nv5dsY4mVNCurrb+6ULIoHPgbvJPWTo3qV6 + on: + branch: master # deploy on master only #---------------------------------# # notifications # From 0d35d4d923e4c621fcd4f7c41ea4d820ff7f1021 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 13 Mar 2018 09:24:35 +0200 Subject: [PATCH 023/187] Change appveyor.yml version number --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 7550b4251..d58fa7941 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 1.0.{build} +version: 0.1.{build} image: Visual Studio 2015 From 9da046de640da514671e9c0c36380f83af4241f1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 13 Mar 2018 14:03:14 +0200 Subject: [PATCH 024/187] Remove github pre-releases for now --- .travis.yml | 11 ----------- appveyor.yml | 10 ---------- 2 files changed, 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 450a780e9..1f11fe239 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,17 +29,6 @@ deploy: file_glob: true on: tags: true - - provider: releases - prerelease: true - overwrite: true - name: latest - api_key: - secure: YHAPA2G3qu7at2hMu4AplXH/niI1ChlgldJVetaKO92iDQiyOk5VqFfhV1ec+nYdX8rtniwfD7YJr2nG2x1ATwKw4MyFcJEXqaOUmKWTeZ/Q3PnQQsa+2BnN4Rfz1aynpsKHDYS9gCU/YTqymujE8bdlxW1WtpYOqOSDkspGxZGZTiUKQ7/qhrjB3Dywm/KF9WEoba/X7tbhmSuU8sL45gBGY008TXZRWqAPM42qa/aBIrG/cIA865VlCUltPC6vzskcWI5q1UtYh6g2CiXJghcpFEO2rWWXmS1A+5nQp6ptJigjRgnhyFHmHb27lRM8aRGRDTeyJvlNuoyIvNj/FxhLXZvomgTyGyzTIl67WIXcxWMKx6KqqrqGyiooRMeFpDEYobZL/FY9whi3M+gUwsofAVQ6oL4a1L185egaXlMKGbM5GYB4OxVLsVtL2c0pJjvNIkCGGDzaqNpdo+vZflB4iCwvw548rWJsqsHnP1XMo28ZU86hibD7V0x+JW2BJEI0lMvOkRBslOhYBafIsbZakO4Zf4d+5b2dd8/xY/wTbuxwgDuBOmpqoByVYeCBah4bbnb8JS6eze+vUyxaI1XLAdQXbLQ788Agr2jdHGuy1wI8io9g5vtzS5oOyq8YFBM1tVKM2Mtw5nkSsTbPJsZg8m/kkre6qiXJl2gPQTE= - file: dist/*.tar.gz - file_glob: true - skip_cleanup: true - on: - branch: master notifications: email: false slack: diff --git a/appveyor.yml b/appveyor.yml index d58fa7941..178fca97c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -54,16 +54,6 @@ deploy: secure: 9qvwlVUHFBV4GwMz1Gu2HSnqU8Ex2nv5dsY4mVNCurrb+6ULIoHPgbvJPWTo3qV6 on: appveyor_repo_tag: true # deploy on tag push only - - provider: GitHub - name: latest - artifact: DEXBot-win64.zip - draft: false - prerelease: true - force_update: true - auth_token: - secure: 9qvwlVUHFBV4GwMz1Gu2HSnqU8Ex2nv5dsY4mVNCurrb+6ULIoHPgbvJPWTo3qV6 - on: - branch: master # deploy on master only #---------------------------------# # notifications # From 2a33fd184040be493594a1557fd411699049e1d6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Mar 2018 15:15:10 +0200 Subject: [PATCH 025/187] Change python-bitshares module version --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index f0cf6b4d3..8599f6ace 100755 --- a/setup.py +++ b/setup.py @@ -49,16 +49,12 @@ def run(self): ], }, install_requires=[ - "bitshares==0.1.11.beta", + "bitshares", "uptick>=0.1.4", "click", "sqlalchemy", "appdirs", "ruamel.yaml" ], - dependency_links=[ - # Temporally force downloads from a different repo, change this once the websocket fix has been merged - "https://github.com/mikakoi/python-bitshares/tarball/websocket-fix#egg=bitshares-0.1.11.beta" - ], include_package_data=True, ) From 2fabd77bfcdf50dee5c2469d9f7e753b664f7495 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sat, 17 Mar 2018 04:25:39 +1100 Subject: [PATCH 026/187] remove private key. fix variable names --- dexbot/tests/test.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dexbot/tests/test.py b/dexbot/tests/test.py index e1d7ce7eb..e3be13f20 100755 --- a/dexbot/tests/test.py +++ b/dexbot/tests/test.py @@ -31,14 +31,16 @@ 'spread': 5, 'reset': True, 'staggers': 2, - 'wall': 5, + 'wall_percent': 5, 'staggerspread': 5, 'min': 0, 'max': 100000, - 'start': 50 + 'start': 50, + 'bias': 1 }}} -KEYS = ['5JV32w3BgPgHV1VoELuDQxvt1gdfuXHo2Rm8TrEn6SQwSsLjnH8'] +# user need sto put a key in +KEYS = [''] class TestDexbot(unittest.TestCase): @@ -50,7 +52,7 @@ def test_dexbot(self): def wait_then_stop(): time.sleep(20) - bitshares_instance.do_next_tick(bitshares_instance.stop) + bot_infrastructure.do_next_tick(bot_infrastructure.stop) stopper = threading.Thread(target=wait_then_stop) stopper.start() From 6a550b7e684e44072ce73fabee79aea19d52319d Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sat, 17 Mar 2018 21:40:17 +1100 Subject: [PATCH 027/187] UNIX signal handling. Allows clean exit when receives Control-C or TERM signal. Will work limited extent on Windows too. --- dexbot/bot.py | 11 +++++++++++ dexbot/cli.py | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/dexbot/bot.py b/dexbot/bot.py index a69607150..461813705 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -36,6 +36,7 @@ def __init__( self.bitshares = bitshares_instance or shared_bitshares_instance() self.config = config self.view = view + self.jobs = set() def init_bots(self): """Do the actual initialisation of bots @@ -93,6 +94,12 @@ def init_bots(self): # Events def on_block(self, data): + if self.jobs: + try: + for i in self.jobs: + i () + finally: + self.jobs = set() for botname, bot in self.config["bots"].items(): if botname not in self.bots or self.bots[botname].disabled: continue @@ -147,3 +154,7 @@ def remove_offline_bot(config, bot_name): # Initialize the base strategy to get control over the data strategy = BaseStrategy(config, bot_name) strategy.purge() + + def do_next_tick(self, job): + """Add a callable to be executed on the next tick""" + self.jobs.add(job) diff --git a/dexbot/cli.py b/dexbot/cli.py index 117e07361..df6f47b10 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import logging import click +import signal import sys from .ui import ( verbose, @@ -52,6 +53,18 @@ def run(ctx): """ try: bot = BotInfrastructure(ctx.config) + # set up signalling. do it here as of no relevance to GUI + killbots = lambda x, y: bot.do_next_tick(bot.stop) + # these first two UNIX & Windows + signal.signal(signal.SIGTERM, killbots) + signal.signal(signal.SIGINT, killbots) + try: + # these signals are UNIX-only territory, will ValueError here on Windows + signal.signal(signal.SIGHUP, killbots) + # future plan: reload config on SIGUSR1 + #signal.signal(signal.SIGUSR1, lambda x, y: bot.do_next_tick(bot.reread_config)) + except ValueError: + log.debug("Cannot set all signals -- not available on this platform") bot.run() except errors.NoBotsAvailable: sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h From d04b9a47594ef59e4140c5c3637221ee9d8d6218 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sat, 17 Mar 2018 21:49:53 +1100 Subject: [PATCH 028/187] At the request of Gabriel, the fairly standard feature on UNIX: an ability to write the dexbot daemon's PID to an external file. --- dexbot/cli.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dexbot/cli.py b/dexbot/cli.py index 117e07361..88281d9ba 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import logging +import os import click import sys from .ui import ( @@ -34,6 +35,12 @@ type=int, default=3, help='Verbosity (0-15)') +@click.option( + '--pidfile', + '-p', + type=str, + default='', + help='File to write PID') @click.pass_context def main(ctx, **kwargs): ctx.obj = {} @@ -50,6 +57,9 @@ def main(ctx, **kwargs): def run(ctx): """ Continuously run the bot """ + if ctx.obj['pidfile']: + with open(ctx.obj['pidfile'],'w') as fd: + fd.write(str(os.getpid())) try: bot = BotInfrastructure(ctx.config) bot.run() From e9176ee2eb8b9f2a5771a68eadd9f001cd8c40c4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 19 Mar 2018 13:33:35 +0200 Subject: [PATCH 029/187] Fix pep8 errors --- dexbot/bot.py | 4 ++-- dexbot/cli.py | 25 +++++++++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 461813705..a3cf2c993 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -96,8 +96,8 @@ def init_bots(self): def on_block(self, data): if self.jobs: try: - for i in self.jobs: - i () + for job in self.jobs: + job() finally: self.jobs = set() for botname, bot in self.config["bots"].items(): diff --git a/dexbot/cli.py b/dexbot/cli.py index df6f47b10..9158bc396 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -53,21 +53,26 @@ def run(ctx): """ try: bot = BotInfrastructure(ctx.config) - # set up signalling. do it here as of no relevance to GUI - killbots = lambda x, y: bot.do_next_tick(bot.stop) - # these first two UNIX & Windows - signal.signal(signal.SIGTERM, killbots) - signal.signal(signal.SIGINT, killbots) + # Set up signalling. do it here as of no relevance to GUI + kill_bots = bot_job(bot, bot.stop) + # These first two UNIX & Windows + signal.signal(signal.SIGTERM, kill_bots) + signal.signal(signal.SIGINT, kill_bots) try: - # these signals are UNIX-only territory, will ValueError here on Windows - signal.signal(signal.SIGHUP, killbots) - # future plan: reload config on SIGUSR1 - #signal.signal(signal.SIGUSR1, lambda x, y: bot.do_next_tick(bot.reread_config)) + # These signals are UNIX-only territory, will ValueError here on Windows + signal.signal(signal.SIGHUP, kill_bots) + # TODO: reload config on SIGUSR1 + # signal.signal(signal.SIGUSR1, lambda x, y: bot.do_next_tick(bot.reread_config)) except ValueError: log.debug("Cannot set all signals -- not available on this platform") bot.run() except errors.NoBotsAvailable: - sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h + sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h + + +def bot_job(bot, job): + return lambda x, y: bot.do_next_tick(job) + if __name__ == '__main__': main() From 1e67eca647bb45e964f8e32d3993c1bdb0f05ee2 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Tue, 20 Mar 2018 11:13:55 +1100 Subject: [PATCH 030/187] PEP8ify --- dexbot/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 88281d9ba..7c66c9524 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -58,13 +58,14 @@ def run(ctx): """ Continuously run the bot """ if ctx.obj['pidfile']: - with open(ctx.obj['pidfile'],'w') as fd: + with open(ctx.obj['pidfile'], 'w') as fd: fd.write(str(os.getpid())) try: bot = BotInfrastructure(ctx.config) bot.run() except errors.NoBotsAvailable: - sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h + sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h + if __name__ == '__main__': main() From 603a7ec8f7cbac12c22cd9fa55fa954aef7b4e55 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Tue, 20 Mar 2018 15:30:37 +1100 Subject: [PATCH 031/187] use environment variable to hold WIF --- dexbot/tests/test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/tests/test.py b/dexbot/tests/test.py index e3be13f20..4e9d177ca 100755 --- a/dexbot/tests/test.py +++ b/dexbot/tests/test.py @@ -3,6 +3,7 @@ from bitshares.bitshares import BitShares import unittest import time +import os import threading import logging from dexbot.bot import BotInfrastructure @@ -40,7 +41,7 @@ }}} # user need sto put a key in -KEYS = [''] +KEYS = [os.environ['DEXBOT_TEST_WIF']] class TestDexbot(unittest.TestCase): From 5889ba6e5eacc391c5f6061ad8ea8bc19f2815d8 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Tue, 20 Mar 2018 15:40:13 +1100 Subject: [PATCH 032/187] remove PID file when done. Double-try as sys.exit means we would never execute if used one try..finally block --- dexbot/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 7c66c9524..6e9b8f81f 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -61,8 +61,12 @@ def run(ctx): with open(ctx.obj['pidfile'], 'w') as fd: fd.write(str(os.getpid())) try: - bot = BotInfrastructure(ctx.config) - bot.run() + try: + bot = BotInfrastructure(ctx.config) + bot.run() + finally: + if ctx.obj['pidfile']: + os.unlink(ctx.obj['pidfile']) except errors.NoBotsAvailable: sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h From 3a2b6140db75957ccbaad1c1dcf5b082dae2295f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 21 Mar 2018 10:46:22 +0200 Subject: [PATCH 033/187] Fix code styling in bot.py Fixed pep8 errors and changed % usage to .format() --- dexbot/bot.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index a3cf2c993..6a0b90751 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -37,6 +37,7 @@ def __init__( self.config = config self.view = view self.jobs = set() + self.notify = None def init_bots(self): """Do the actual initialisation of bots @@ -55,10 +56,14 @@ def init_bots(self): # Initialize bots: for botname, bot in self.config["bots"].items(): if "account" not in bot: - log_bots.critical("Bot has no account",extra={'botname':botname,'account':'unknown','market':'unknown','is_dsabled':(lambda: True)}) + log_bots.critical("Bot has no account", extra={ + 'botname': botname, 'account': 'unknown', 'market': 'unknown', 'is_disabled': (lambda: True) + }) continue if "market" not in bot: - log_bots.critical("Bot has no market",extra={'botname':botname,'account':bot['account'],'market':'unknown','is_disabled':(lambda: True)}) + log_bots.critical("Bot has no market", extra={ + 'botname': botname, 'account': bot['account'], 'market': 'unknown', 'is_disabled': (lambda: True) + }) continue try: klass = getattr( @@ -73,8 +78,10 @@ def init_bots(self): ) markets.add(bot['market']) accounts.add(bot['account']) - except: - log_bots.exception("Bot initialisation",extra={'botname':botname,'account':bot['account'],'market':'unknown','is_disabled':(lambda: True)}) + except BaseException: + log_bots.exception("Bot initialisation", extra={ + 'botname': botname, 'account': bot['account'], 'market': 'unknown', 'is_disabled': (lambda: True) + }) if len(markets) == 0: log.critical("No bots to launch, exiting") @@ -110,11 +117,11 @@ def on_block(self, data): self.bots[botname].log.exception("in .tick()") def on_market(self, data): - if data.get("deleted", False): # no info available on deleted orders + if data.get("deleted", False): # No info available on deleted orders return for botname, bot in self.config["bots"].items(): if self.bots[botname].disabled: - self.bots[botname].log.warning("disabled") + self.bots[botname].log.debug('Worker "{}" is disabled'.format(botname)) continue if bot["market"] == data.market: try: @@ -127,7 +134,7 @@ def on_account(self, accountupdate): account = accountupdate.account for botname, bot in self.config["bots"].items(): if self.bots[botname].disabled: - self.bots[botname].log.info("The bot %s has been disabled" % botname) + self.bots[botname].log.info('Worker "{}" is disabled'.format(botname)) continue if bot["account"] == account["name"]: try: From 3f659eb3333c877d1bcf774f0bd246e6a2fa06b0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 22 Mar 2018 09:22:19 +0200 Subject: [PATCH 034/187] Fix balance check in simple strategy --- dexbot/basestrategy.py | 8 ++++++++ dexbot/strategies/simple.py | 13 ++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index c2fee240e..eaac84fc6 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -277,3 +277,11 @@ def purge(self): """ self.cancel_all() self.clear() + + @staticmethod + def get_order_amount(order, asset_type): + try: + order_amount = order[asset_type]['amount'] + except KeyError: + order_amount = 0 + return order_amount diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index 7896854ee..8cecb1c61 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -129,12 +129,9 @@ def update_orders(self, new_sell_order, new_buy_order): if sold_amount: # We sold something, place updated buy order - try: - buy_order_amount = buy_order['quote']['amount'] - except KeyError: - buy_order_amount = 0 + buy_order_amount = self.get_order_amount(buy_order, 'quote') new_buy_amount = buy_order_amount - bought_amount + sold_amount - if float(self.balance(self.market["base"])) < new_buy_amount: + if float(self.balance(self.market["base"])) < self.buy_price * new_buy_amount: self.log.critical( 'Insufficient buy balance, needed {} {}'.format(self.buy_price * new_buy_amount, self.market['base']['symbol']) @@ -163,10 +160,7 @@ def update_orders(self, new_sell_order, new_buy_order): if bought_amount: # We bought something, place updated sell order - try: - sell_order_amount = sell_order['base']['amount'] - except KeyError: - sell_order_amount = 0 + sell_order_amount = self.get_order_amount(sell_order, 'quote') new_sell_amount = sell_order_amount + bought_amount - sold_amount if float(self.balance(self.market["quote"])) < new_sell_amount: self.log.critical( @@ -243,6 +237,7 @@ def test(self, *args, **kwargs): # GUI updaters def update_gui_profit(self): + # Fixme: profit calculation doesn't work this way, figure out a better way to do this. if self.initial_balance: profit = round((self.orders_balance() - self.initial_balance) / self.initial_balance, 3) else: From 8818912514efeb9b33896f914e44a181a92d597c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 23 Mar 2018 08:38:35 +0200 Subject: [PATCH 035/187] Change bot texts to worker --- app.py | 2 +- config.yml | 2 +- dexbot/basestrategy.py | 46 ++--- dexbot/bot.py | 167 ----------------- dexbot/cli.py | 24 +-- ...troller.py => create_worker_controller.py} | 81 ++++----- dexbot/controllers/main_controller.py | 73 ++++---- dexbot/errors.py | 4 +- dexbot/statemachine.py | 2 +- dexbot/storage.py | 14 +- dexbot/strategies/echo.py | 2 +- dexbot/strategies/simple.py | 8 +- dexbot/strategies/walls.py | 8 +- dexbot/ui.py | 12 +- dexbot/views/bot_item.py | 103 ----------- dexbot/views/bot_list.py | 78 -------- .../views/{create_bot.py => create_worker.py} | 30 ++-- dexbot/views/{edit_bot.py => edit_worker.py} | 59 +++--- ..._bot_window.ui => create_worker_window.ui} | 16 +- ...it_bot_window.ui => edit_worker_window.ui} | 16 +- ...t_item_widget.ui => worker_item_widget.ui} | 4 +- ...t_list_window.ui => worker_list_window.ui} | 10 +- dexbot/views/worker_item.py | 112 ++++++++++++ dexbot/views/worker_list.py | 81 +++++++++ dexbot/worker.py | 170 ++++++++++++++++++ docs/requirements.txt | 1 - 26 files changed, 562 insertions(+), 563 deletions(-) delete mode 100644 dexbot/bot.py rename dexbot/controllers/{create_bot_controller.py => create_worker_controller.py} (55%) delete mode 100644 dexbot/views/bot_item.py delete mode 100644 dexbot/views/bot_list.py rename dexbot/views/{create_bot.py => create_worker.py} (81%) rename dexbot/views/{edit_bot.py => edit_worker.py} (69%) rename dexbot/views/ui/{create_bot_window.ui => create_worker_window.ui} (97%) rename dexbot/views/ui/{edit_bot_window.ui => edit_worker_window.ui} (97%) rename dexbot/views/ui/{bot_item_widget.ui => worker_item_widget.ui} (99%) rename dexbot/views/ui/{bot_list_window.ui => worker_list_window.ui} (93%) create mode 100644 dexbot/views/worker_item.py create mode 100644 dexbot/views/worker_list.py create mode 100644 dexbot/worker.py delete mode 100644 docs/requirements.txt diff --git a/app.py b/app.py index 4fa494e20..c75827fd4 100644 --- a/app.py +++ b/app.py @@ -4,7 +4,7 @@ from bitshares import BitShares from dexbot.controllers.main_controller import MainController -from dexbot.views.bot_list import MainView +from dexbot.views.worker_list import MainView from dexbot.controllers.wallet_controller import WalletController from dexbot.views.unlock_wallet import UnlockWalletView from dexbot.views.create_wallet import CreateWalletView diff --git a/config.yml b/config.yml index 38d9ef064..4ea34f4a6 100644 --- a/config.yml +++ b/config.yml @@ -1,3 +1,3 @@ node: wss://bitshares.openledger.info/ws -bots: {} \ No newline at end of file +workers: {} \ No newline at end of file diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index eaac84fc6..662a1dd8e 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -25,12 +25,12 @@ class BaseStrategy(Storage, StateMachine, Events): * ``basestrategy.add_state``: Add a specific state * ``basestrategy.set_state``: Set finite state machine * ``basestrategy.get_state``: Change state of state machine - * ``basestrategy.account``: The Account object of this bot - * ``basestrategy.market``: The market used by this bot - * ``basestrategy.orders``: List of open orders of the bot's account in the bot's market - * ``basestrategy.balance``: List of assets and amounts available in the bot's account - * ``basestrategy.log``: a per-bot logger (actually LoggerAdapter) adds bot-specific context: botname & account - (Because some UIs might want to display per-bot logs) + * ``basestrategy.account``: The Account object of this worker + * ``basestrategy.market``: The market used by this worker + * ``basestrategy.orders``: List of open orders of the worker's account in the worker's market + * ``basestrategy.balance``: List of assets and amounts available in the worker's account + * ``basestrategy.log``: a per-worker logger (actually LoggerAdapter) adds worker-specific context: + worker name & account (Because some UIs might want to display per-worker logs) Also, Base Strategy inherits :class:`dexbot.storage.Storage` which allows to permanently store data in a sqlite database @@ -40,7 +40,7 @@ class BaseStrategy(Storage, StateMachine, Events): .. note:: This applies a ``json.loads(json.dumps(value))``! - Bots must never attempt to interact with the user, they must assume they are running unattended + Workers must never attempt to interact with the user, they must assume they are running unattended They can log events. If a problem occurs they can't fix they should set self.disabled = True and throw an exception The framework catches all exceptions thrown from event handlers and logs appropriately. """ @@ -100,28 +100,28 @@ def __init__( self.onMarketUpdate += self._callbackPlaceFillOrders self.config = config - self.bot = config["bots"][name] + self.worker = config["workers"][name] self._account = Account( - self.bot["account"], + self.worker["account"], full=True, bitshares_instance=self.bitshares ) self._market = Market( - config["bots"][name]["market"], + config["workers"][name]["market"], bitshares_instance=self.bitshares ) # Settings for bitshares instance - self.bitshares.bundle = bool(self.bot.get("bundle", False)) + self.bitshares.bundle = bool(self.worker.get("bundle", False)) - # disabled flag - this flag can be flipped to True by a bot and + # disabled flag - this flag can be flipped to True by a worker and # will be reset to False after reset only self.disabled = False - # a private logger that adds bot identify data to the LogRecord - self.log = logging.LoggerAdapter(logging.getLogger('dexbot.per_bot'), {'botname': name, - 'account': self.bot['account'], - 'market': self.bot['market'], + # a private logger that adds worker identify data to the LogRecord + self.log = logging.LoggerAdapter(logging.getLogger('dexbot.per_worker'), {'worker_name': name, + 'account': self.worker['account'], + 'market': self.worker['market'], 'is_disabled': lambda: self.disabled}) @property @@ -147,10 +147,10 @@ def calculate_center_price(self): @property def orders(self): - """ Return the bot's open accounts in the current market + """ Return the worker's open accounts in the current market """ self.account.refresh() - return [o for o in self.account.openorders if self.bot["market"] == o.market and self.account.openorders] + return [o for o in self.account.openorders if self.worker["market"] == o.market and self.account.openorders] def get_order(self, order_id): for order in self.orders: @@ -188,7 +188,7 @@ def updated_open_orders(self): for o in limit_orders ] - return [o for o in orders if self.bot["market"] == o.market] + return [o for o in orders if self.worker["market"] == o.market] @property def market(self): @@ -205,7 +205,7 @@ def account(self): return self._account def balance(self, asset): - """ Return the balance of your bot's account for a specific asset + """ Return the balance of your worker's account for a specific asset """ return self._account.balance(asset) @@ -227,7 +227,7 @@ def test_mode(self): @property def balances(self): - """ Return the balances of your bot's account + """ Return the balances of your worker's account """ return self._account.balances @@ -263,7 +263,7 @@ def cancel(self, orders): ) def cancel_all(self): - """ Cancel all orders of this bot + """ Cancel all orders of the worker's account """ if self.orders: return self.bitshares.cancel( @@ -273,7 +273,7 @@ def cancel_all(self): def purge(self): """ - Clear all the bot data from the database and cancel all orders + Clear all the worker data from the database and cancel all orders """ self.cancel_all() self.clear() diff --git a/dexbot/bot.py b/dexbot/bot.py deleted file mode 100644 index 6a0b90751..000000000 --- a/dexbot/bot.py +++ /dev/null @@ -1,167 +0,0 @@ -import importlib -import sys -import logging -import os.path -import threading - -from dexbot.basestrategy import BaseStrategy - -from bitshares.notify import Notify -from bitshares.instance import shared_bitshares_instance - -import dexbot.errors as errors - -log = logging.getLogger(__name__) - -log_bots = logging.getLogger('dexbot.per_bot') -# NOTE this is the special logger for per-bot events -# it returns LogRecords with extra fields: botname, account, market and is_disabled -# is_disabled is a callable returning True if the bot is currently disabled. -# GUIs can add a handler to this logger to get a stream of events re the running bots. - - -class BotInfrastructure(threading.Thread): - - bots = dict() - - def __init__( - self, - config, - bitshares_instance=None, - view=None - ): - super().__init__() - - # BitShares instance - self.bitshares = bitshares_instance or shared_bitshares_instance() - self.config = config - self.view = view - self.jobs = set() - self.notify = None - - def init_bots(self): - """Do the actual initialisation of bots - Potentially quite slow (tens of seconds) - So called as part of run() - """ - # set the module search path - user_bot_path = os.path.expanduser("~/bots") - if os.path.exists(user_bot_path): - sys.path.append(user_bot_path) - - # Load all accounts and markets in use to subscribe to them - accounts = set() - markets = set() - - # Initialize bots: - for botname, bot in self.config["bots"].items(): - if "account" not in bot: - log_bots.critical("Bot has no account", extra={ - 'botname': botname, 'account': 'unknown', 'market': 'unknown', 'is_disabled': (lambda: True) - }) - continue - if "market" not in bot: - log_bots.critical("Bot has no market", extra={ - 'botname': botname, 'account': bot['account'], 'market': 'unknown', 'is_disabled': (lambda: True) - }) - continue - try: - klass = getattr( - importlib.import_module(bot["module"]), - 'Strategy' - ) - self.bots[botname] = klass( - config=self.config, - name=botname, - bitshares_instance=self.bitshares, - view=self.view - ) - markets.add(bot['market']) - accounts.add(bot['account']) - except BaseException: - log_bots.exception("Bot initialisation", extra={ - 'botname': botname, 'account': bot['account'], 'market': 'unknown', 'is_disabled': (lambda: True) - }) - - if len(markets) == 0: - log.critical("No bots to launch, exiting") - raise errors.NoBotsAvailable() - - # Create notification instance - # Technically, this will multiplex markets and accounts and - # we need to demultiplex the events after we have received them - self.notify = Notify( - markets=list(markets), - accounts=list(accounts), - on_market=self.on_market, - on_account=self.on_account, - on_block=self.on_block, - bitshares_instance=self.bitshares - ) - - # Events - def on_block(self, data): - if self.jobs: - try: - for job in self.jobs: - job() - finally: - self.jobs = set() - for botname, bot in self.config["bots"].items(): - if botname not in self.bots or self.bots[botname].disabled: - continue - try: - self.bots[botname].ontick(data) - except Exception as e: - self.bots[botname].error_ontick(e) - self.bots[botname].log.exception("in .tick()") - - def on_market(self, data): - if data.get("deleted", False): # No info available on deleted orders - return - for botname, bot in self.config["bots"].items(): - if self.bots[botname].disabled: - self.bots[botname].log.debug('Worker "{}" is disabled'.format(botname)) - continue - if bot["market"] == data.market: - try: - self.bots[botname].onMarketUpdate(data) - except Exception as e: - self.bots[botname].error_onMarketUpdate(e) - self.bots[botname].log.exception(".onMarketUpdate()") - - def on_account(self, accountupdate): - account = accountupdate.account - for botname, bot in self.config["bots"].items(): - if self.bots[botname].disabled: - self.bots[botname].log.info('Worker "{}" is disabled'.format(botname)) - continue - if bot["account"] == account["name"]: - try: - self.bots[botname].onAccount(accountupdate) - except Exception as e: - self.bots[botname].error_onAccount(e) - self.bots[botname].log.exception(".onAccountUpdate()") - - def run(self): - self.init_bots() - self.notify.listen() - - def stop(self): - for bot in self.bots: - self.bots[bot].cancel_all() - self.notify.websocket.close() - - def remove_bot(self): - for bot in self.bots: - self.bots[bot].purge() - - @staticmethod - def remove_offline_bot(config, bot_name): - # Initialize the base strategy to get control over the data - strategy = BaseStrategy(config, bot_name) - strategy.purge() - - def do_next_tick(self, job): - """Add a callable to be executed on the next tick""" - self.jobs.add(job) diff --git a/dexbot/cli.py b/dexbot/cli.py index 22d69a5b2..023972aae 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -14,7 +14,7 @@ warning, alert, ) -from dexbot.bot import BotInfrastructure +from dexbot.worker import WorkerInfrastructure import dexbot.errors as errors log = logging.getLogger(__name__) @@ -56,36 +56,36 @@ def main(ctx, **kwargs): @unlock @verbose def run(ctx): - """ Continuously run the bot + """ Continuously run the worker """ if ctx.obj['pidfile']: with open(ctx.obj['pidfile'], 'w') as fd: fd.write(str(os.getpid())) try: try: - bot = BotInfrastructure(ctx.config) + worker = WorkerInfrastructure(ctx.config) # Set up signalling. do it here as of no relevance to GUI - kill_bots = bot_job(bot, bot.stop) + kill_workers = worker_job(worker, worker.stop) # These first two UNIX & Windows - signal.signal(signal.SIGTERM, kill_bots) - signal.signal(signal.SIGINT, kill_bots) + signal.signal(signal.SIGTERM, kill_workers) + signal.signal(signal.SIGINT, kill_workers) try: # These signals are UNIX-only territory, will ValueError here on Windows - signal.signal(signal.SIGHUP, kill_bots) + signal.signal(signal.SIGHUP, kill_workers) # TODO: reload config on SIGUSR1 - # signal.signal(signal.SIGUSR1, lambda x, y: bot.do_next_tick(bot.reread_config)) + # signal.signal(signal.SIGUSR1, lambda x, y: worker.do_next_tick(worker.reread_config)) except ValueError: log.debug("Cannot set all signals -- not available on this platform") - bot.run() + worker.run() finally: if ctx.obj['pidfile']: os.unlink(ctx.obj['pidfile']) - except errors.NoBotsAvailable: + except errors.NoWorkersAvailable: sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h -def bot_job(bot, job): - return lambda x, y: bot.do_next_tick(job) +def worker_job(worker, job): + return lambda x, y: worker.do_next_tick(job) if __name__ == '__main__': diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_worker_controller.py similarity index 55% rename from dexbot/controllers/create_bot_controller.py rename to dexbot/controllers/create_worker_controller.py index 3f0bd44f9..09af54d35 100644 --- a/dexbot/controllers/create_bot_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -8,7 +8,7 @@ from ruamel.yaml import YAML -class CreateBotController: +class CreateWorkerController: def __init__(self, main_ctrl): self.main_ctrl = main_ctrl @@ -31,17 +31,15 @@ def base_assets(self): ] return assets - def remove_bot(self, bot_name): - self.main_ctrl.remove_bot(bot_name) + def remove_worker(self, worker_name): + self.main_ctrl.remove_worker(worker_name) - def is_bot_name_valid(self, bot_name, old_bot_name=None): - bot_names = self.main_ctrl.get_bots_data().keys() - # and old_bot_name not in bot_names - if bot_name in bot_names and old_bot_name not in bot_names: - is_name_valid = False - else: - is_name_valid = True - return is_name_valid + def is_worker_name_valid(self, worker_name): + worker_names = self.main_ctrl.get_workers_data().keys() + # Check that the name is unique + if worker_name in worker_names: + return False + return True def is_asset_valid(self, asset): try: @@ -81,63 +79,52 @@ def add_private_key(self, private_key): pass @staticmethod - def get_unique_bot_name(): + def get_unique_worker_name(): """ - Returns unique bot name "Bot %n", where %n is the next available index + Returns unique worker name "Worker %n", where %n is the next available index """ index = 1 - bots = MainController.get_bots_data().keys() - botname = "Bot {0}".format(index) - while botname in bots: - botname = "Bot {0}".format(index) + workers = MainController.get_workers_data().keys() + worker_name = "Worker {0}".format(index) + while worker_name in workers: + worker_name = "worker {0}".format(index) index += 1 - return botname - - @staticmethod - def add_bot_config(botname, bot_data): - yaml = YAML() - with open('config.yml', 'r') as f: - config = yaml.load(f) - - config['bots'][botname] = bot_data - - with open("config.yml", "w") as f: - yaml.dump(config, f) + return worker_name @staticmethod - def get_bot_current_strategy(bot_data): + def get_worker_current_strategy(worker_data): strategies = { - bot_data['strategy']: bot_data['module'] + worker_data['strategy']: worker_data['module'] } return strategies @staticmethod - def get_assets(bot_data): - return bot_data['market'].split('/') + def get_assets(worker_data): + return worker_data['market'].split('/') - def get_base_asset(self, bot_data): - return self.get_assets(bot_data)[1] + def get_base_asset(self, worker_data): + return self.get_assets(worker_data)[1] - def get_quote_asset(self, bot_data): - return self.get_assets(bot_data)[0] + def get_quote_asset(self, worker_data): + return self.get_assets(worker_data)[0] @staticmethod - def get_account(bot_data): - return bot_data['account'] + def get_account(worker_data): + return worker_data['account'] @staticmethod - def get_target_amount(bot_data): - return bot_data['target']['amount'] + def get_target_amount(worker_data): + return worker_data['target']['amount'] @staticmethod - def get_target_center_price(bot_data): - return bot_data['target']['center_price'] + def get_target_center_price(worker_data): + return worker_data['target']['center_price'] @staticmethod - def get_target_center_price_automatic(bot_data): - return bot_data['target']['center_price_automatic'] + def get_target_center_price_dynamic(worker_data): + return worker_data['target']['center_price_dynamic'] @staticmethod - def get_target_spread(bot_data): - return bot_data['target']['spread'] + def get_target_spread(worker_data): + return worker_data['target']['spread'] diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index f2dc065e6..0a5c91066 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,4 +1,4 @@ -from dexbot.bot import BotInfrastructure +from dexbot.worker import WorkerInfrastructure from ruamel.yaml import YAML from bitshares.instance import set_shared_bitshares_instance @@ -6,37 +6,35 @@ class MainController: - bots = dict() + workers = dict() def __init__(self, bitshares_instance): self.bitshares_instance = bitshares_instance set_shared_bitshares_instance(bitshares_instance) - self.bot_template = BotInfrastructure + self.worker_template = WorkerInfrastructure - def create_bot(self, botname, config, view): + def create_worker(self, worker_name, config, view): # Todo: Add some threading here so that the GUI doesn't freeze - bot = self.bot_template(config, self.bitshares_instance, view) - bot.daemon = True - bot.start() - self.bots[botname] = bot + worker = self.worker_template(config, self.bitshares_instance, view) + worker.daemon = True + worker.start() + self.workers[worker_name] = worker - def stop_bot(self, bot_name): - self.bots[bot_name].stop() - self.bots.pop(bot_name, None) + def stop_worker(self, worker_name): + self.workers[worker_name].stop() + self.workers.pop(worker_name, None) - def remove_bot(self, bot_name): + def remove_worker(self, worker_name): # Todo: Add some threading here so that the GUI doesn't freeze - if bot_name in self.bots: - # Bot currently running - self.bots[bot_name].remove_bot() - self.bots[bot_name].stop() - self.bots.pop(bot_name, None) + if worker_name in self.workers: + # Worker currently running + self.workers[worker_name].remove_worker() + self.workers[worker_name].stop() + self.workers.pop(worker_name, None) else: - # Bot not running - config = self.get_bot_config(bot_name) - self.bot_template.remove_offline_bot(config, bot_name) - - self.remove_bot_config(bot_name) + # Worker not running + config = self.get_worker_config(worker_name) + self.worker_template.remove_offline_worker(config, worker_name) @staticmethod def load_config(): @@ -45,44 +43,43 @@ def load_config(): return yaml.load(f) @staticmethod - def get_bots_data(): + def get_workers_data(): """ - Returns dict of all the bots data + Returns dict of all the workers data """ with open('config.yml', 'r') as f: yaml = YAML() - return yaml.load(f)['bots'] + return yaml.load(f)['workers'] @staticmethod - def get_latest_bot_config(): + def get_worker_config(worker_name): """ - Returns config file data with only the latest bot data + Returns config file data with only the data from a specific worker """ with open('config.yml', 'r') as f: yaml = YAML() config = yaml.load(f) - latest_bot = list(config['bots'].keys())[-1] - config['bots'] = {latest_bot: config['bots'][latest_bot]} + config['workers'] = {worker_name: config['workers'][worker_name]} return config @staticmethod - def get_bot_config(botname): - """ - Returns config file data with only the data from a specific bot - """ + def remove_worker_config(worker_name): + yaml = YAML() with open('config.yml', 'r') as f: - yaml = YAML() config = yaml.load(f) - config['bots'] = {botname: config['bots'][botname]} - return config + + config['workers'].pop(worker_name, None) + + with open("config.yml", "w") as f: + yaml.dump(config, f) @staticmethod - def remove_bot_config(bot_name): + def add_worker_config(worker_name, worker_data): yaml = YAML() with open('config.yml', 'r') as f: config = yaml.load(f) - config['bots'].pop(bot_name, None) + config['workers'][worker_name] = worker_data with open("config.yml", "w") as f: yaml.dump(config, f) diff --git a/dexbot/errors.py b/dexbot/errors.py index b56af00ee..8cd19736e 100644 --- a/dexbot/errors.py +++ b/dexbot/errors.py @@ -7,4 +7,6 @@ def InsufficientFundsError(amount): "[InsufficientFunds] Need {}".format(str(amount)) ) -class NoBotsAvailable(Exception): pass + +class NoWorkersAvailable(Exception): + pass diff --git a/dexbot/statemachine.py b/dexbot/statemachine.py index 56c97751a..4a0906cd3 100644 --- a/dexbot/statemachine.py +++ b/dexbot/statemachine.py @@ -1,4 +1,4 @@ -class StateMachine(): +class StateMachine: """ Generic state machine """ def __init__(self, *args, **kwargs): diff --git a/dexbot/storage.py b/dexbot/storage.py index cfbec7bb6..4a1410a80 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -53,22 +53,22 @@ def __init__(self, category): self.category = category def __setitem__(self, key, value): - worker.execute_noreturn(worker.set_item, self.category, key, value) + db_worker.execute_noreturn(db_worker.set_item, self.category, key, value) def __getitem__(self, key): - return worker.execute(worker.get_item, self.category, key) + return db_worker.execute(db_worker.get_item, self.category, key) def __delitem__(self, key): - worker.execute_noreturn(worker.del_item, self.category, key) + db_worker.execute_noreturn(db_worker.del_item, self.category, key) def __contains__(self, key): - return worker.execute(worker.contains, self.category, key) + return db_worker.execute(db_worker.contains, self.category, key) def items(self): - return worker.execute(worker.get_items, self.category) + return db_worker.execute(db_worker.get_items, self.category) def clear(self): - worker.execute_noreturn(worker.clear, self.category) + db_worker.execute_noreturn(db_worker.clear, self.category) class DatabaseWorker(threading.Thread): @@ -186,4 +186,4 @@ def clear(self, category): # Create directory for sqlite file mkdir_p(data_dir) -worker = DatabaseWorker() +db_worker = DatabaseWorker() diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index e940145ed..476e1c516 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -83,7 +83,7 @@ def print_newBlock(self, i): # raise ValueError("Testing disabling") def print_accountUpdate(self, i): - """ This method is called when the bot's account name receives + """ This method is called when the worker's account name receives any update. This includes anything that changes ``2.6.xxxx``, e.g., any operation that affects your account. """ diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index 8cecb1c61..5aeae13b8 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -28,7 +28,7 @@ def __init__(self, *args, **kwargs): # Counter for blocks self.counter = Counter() - self.target = self.bot.get("target", {}) + self.target = self.worker.get("target", {}) self.is_center_price_dynamic = self.target["center_price_dynamic"] if self.is_center_price_dynamic: self.center_price = None @@ -40,7 +40,7 @@ def __init__(self, *args, **kwargs): self.calculate_order_prices() self.initial_balance = self['initial_balance'] or 0 - self.bot_name = kwargs.get('name') + self.worker_name = kwargs.get('name') self.view = kwargs.get('view') def calculate_order_prices(self): @@ -242,7 +242,7 @@ def update_gui_profit(self): profit = round((self.orders_balance() - self.initial_balance) / self.initial_balance, 3) else: profit = 0 - idle_add(self.view.set_bot_profit, self.bot_name, float(profit)) + idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) self['profit'] = profit def update_gui_slider(self): @@ -262,5 +262,5 @@ def update_gui_slider(self): percentage = 0 else: percentage = (buy_amount / total) * 100 - idle_add(self.view.set_bot_slider, self.bot_name, percentage) + idle_add(self.view.set_worker_slider, self.worker_name, percentage) self['slider'] = percentage diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py index 8a7974391..2d037756f 100644 --- a/dexbot/strategies/walls.py +++ b/dexbot/strategies/walls.py @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): self.counter = Counter() # Tests for actions - self.test_blocks = self.bot.get("test", {}).get("blocks", 0) + self.test_blocks = self.worker.get("test", {}).get("blocks", 0) def error(self, *args, **kwargs): self.disabled = True @@ -43,7 +43,7 @@ def updateorders(self): self.cancelall() # Target - target = self.bot.get("target", {}) + target = self.worker.get("target", {}) price = self.getprice() # prices @@ -83,7 +83,7 @@ def getprice(self): """ Here we obtain the price for the quote and make sure it has a feed price """ - target = self.bot.get("target", {}) + target = self.worker.get("target", {}) if target.get("reference") == "feed": assert self.market == self.market.core_quote_market(), "Wrong market for 'feed' reference!" ticker = self.market.ticker() @@ -118,7 +118,7 @@ def test(self, *args, **kwargs): # Test if price feed has moved more than the threshold if ( self["feed_price"] and - fabs(1 - float(self.getprice()) / self["feed_price"]) > self.bot["threshold"] / 100.0 + fabs(1 - float(self.getprice()) / self["feed_price"]) > self.worker["threshold"] / 100.0 ): self.log.info("Price feed moved by more than the threshold. Updating orders!") self.updateorders() diff --git a/dexbot/ui.py b/dexbot/ui.py index 3a0b665e9..5654683fa 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -17,21 +17,21 @@ def new_func(ctx, *args, **kwargs): if ctx.obj.get("systemd",False): # dont print the timestamps: systemd will log it for us formatter1 = logging.Formatter('%(name)s - %(levelname)s - %(message)s') - formatter2 = logging.Formatter('bot %(botname)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') + formatter2 = logging.Formatter('Worker %(worker_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') elif verbosity == "debug": # when debugging log where the log call came from formatter1 = logging.Formatter('%(asctime)s (%(module)s:%(lineno)d) - %(levelname)s - %(message)s') - formatter2 = logging.Formatter('%(asctime)s (%(module)s:%(lineno)d) - bot %(botname)s - %(levelname)s - %(message)s') + formatter2 = logging.Formatter('%(asctime)s (%(module)s:%(lineno)d) - worker %(worker_name)s - %(levelname)s - %(message)s') else: formatter1 = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - formatter2 = logging.Formatter('%(asctime)s - bot %(botname)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') + formatter2 = logging.Formatter('%(asctime)s - worker %(worker_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') - # use special format for special bots logger + # use special format for special workers logger ch = logging.StreamHandler() ch.setLevel(getattr(logging, verbosity.upper())) ch.setFormatter(formatter2) - logging.getLogger("dexbot.per_bot").addHandler(ch) - logging.getLogger("dexbot.per_bot").propagate = False # don't double up with root logger + logging.getLogger("dexbot.per_worker").addHandler(ch) + logging.getLogger("dexbot.per_worker").propagate = False # don't double up with root logger # set the root logger with basic format ch = logging.StreamHandler() ch.setLevel(getattr(logging, verbosity.upper())) diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py deleted file mode 100644 index 7816f14ad..000000000 --- a/dexbot/views/bot_item.py +++ /dev/null @@ -1,103 +0,0 @@ -from .ui.bot_item_widget_ui import Ui_widget -from .confirmation import ConfirmationDialog -from .edit_bot import EditBotView -from dexbot.storage import worker -from dexbot.controllers.create_bot_controller import CreateBotController - -from PyQt5 import QtWidgets - - -class BotItemWidget(QtWidgets.QWidget, Ui_widget): - - def __init__(self, botname, config, main_ctrl, view): - super(BotItemWidget, self).__init__() - - self.main_ctrl = main_ctrl - self.running = False - self.botname = botname - self.config = config - self.controller = main_ctrl - self.view = view - - self.setupUi(self) - self.pause_button.hide() - - self.pause_button.clicked.connect(self.pause_bot) - self.play_button.clicked.connect(self.start_bot) - self.remove_button.clicked.connect(self.remove_widget_dialog) - self.edit_button.clicked.connect(self.handle_edit_bot) - - self.setup_ui_data(config) - - def setup_ui_data(self, config): - botname = list(config['bots'].keys())[0] - self.set_bot_name(botname) - - market = config['bots'][botname]['market'] - self.set_bot_market(market) - - profit = worker.execute(worker.get_item, botname, 'profit') - if profit: - self.set_bot_profit(profit) - - percentage = worker.execute(worker.get_item, botname, 'slider') - if percentage: - self.set_bot_slider(percentage) - - def start_bot(self): - self.running = True - self.pause_button.show() - self.play_button.hide() - - self.controller.create_bot(self.botname, self.config, self.view) - - def pause_bot(self): - self.running = False - self.pause_button.hide() - self.play_button.show() - - self.controller.stop_bot(self.botname) - - def set_bot_name(self, value): - self.botname_label.setText(value) - - def set_bot_account(self, value): - pass - - def set_bot_market(self, value): - self.currency_label.setText(value) - - def set_bot_profit(self, value): - if value >= 0: - value = '+' + str(value) - - value = str(value) + '%' - self.profit_label.setText(value) - - def set_bot_slider(self, value): - self.order_slider.setSliderPosition(value) - - def remove_widget_dialog(self): - dialog = ConfirmationDialog('Are you sure you want to remove bot "{}"?'.format(self.botname)) - return_value = dialog.exec_() - if return_value: - self.remove_widget() - - def remove_widget(self): - self.controller.remove_bot(self.botname) - self.deleteLater() - - # Todo: Remove the line below this after multi-bot support is added - self.view.ui.add_bot_button.setEnabled(True) - - def handle_edit_bot(self): - controller = CreateBotController(self.main_ctrl) - edit_bot_dialog = EditBotView(controller, self.botname, self.config) - return_value = edit_bot_dialog.exec_() - - # User clicked save - if return_value == 1: - bot_name = edit_bot_dialog.bot_name - config = self.main_ctrl.get_bot_config(bot_name) - self.remove_widget() - self.view.add_bot_widget(bot_name, config) diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py deleted file mode 100644 index 1fb51c1ed..000000000 --- a/dexbot/views/bot_list.py +++ /dev/null @@ -1,78 +0,0 @@ -from .ui.bot_list_window_ui import Ui_MainWindow -from .create_bot import CreateBotView -from .bot_item import BotItemWidget -from dexbot.controllers.create_bot_controller import CreateBotController -from dexbot.queue.queue_dispatcher import ThreadDispatcher - -from PyQt5 import QtWidgets - - -class MainView(QtWidgets.QMainWindow): - - bot_widgets = dict() - - def __init__(self, main_ctrl): - self.main_ctrl = main_ctrl - super(MainView, self).__init__() - self.ui = Ui_MainWindow() - self.ui.setupUi(self) - self.bot_container = self.ui.verticalLayout - - self.ui.add_bot_button.clicked.connect(self.handle_add_bot) - - # Load bot widgets from config file - bots = main_ctrl.get_bots_data() - for botname in bots: - config = self.main_ctrl.get_bot_config(botname) - self.add_bot_widget(botname, config) - - # Artificially limit the number of bots to 1 until it's officially supported - # Todo: Remove the 2 lines below this after multi-bot support is added - self.ui.add_bot_button.setEnabled(False) - break - - # Dispatcher polls for events from the bots that are used to change the ui - self.dispatcher = ThreadDispatcher(self) - self.dispatcher.start() - - def add_bot_widget(self, botname, config): - widget = BotItemWidget(botname, config, self.main_ctrl, self) - widget.setFixedSize(widget.frameSize()) - self.bot_container.addWidget(widget) - self.bot_widgets[botname] = widget - - # Todo: Remove the line below this after multi-bot support is added - self.ui.add_bot_button.setEnabled(False) - - def handle_add_bot(self): - controller = CreateBotController(self.main_ctrl) - create_bot_dialog = CreateBotView(controller) - return_value = create_bot_dialog.exec_() - - # User clicked save - if return_value == 1: - botname = create_bot_dialog.bot_name - config = self.main_ctrl.get_bot_config(botname) - self.add_bot_widget(botname, config) - - def refresh_bot_list(self): - pass - - def set_bot_name(self, bot_name, value): - self.bot_widgets[bot_name].set_bot_name(value) - - def set_bot_account(self, bot_name, value): - self.bot_widgets[bot_name].set_bot_account(value) - - def set_bot_profit(self, bot_name, value): - self.bot_widgets[bot_name].set_bot_profit(value) - - def set_bot_market(self, bot_name, value): - self.bot_widgets[bot_name].set_bot_market(value) - - def set_bot_slider(self, bot_name, value): - self.bot_widgets[bot_name].set_bot_slider(value) - - def customEvent(self, event): - # Process idle_queue_dispatcher events - event.callback() diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_worker.py similarity index 81% rename from dexbot/views/create_bot.py rename to dexbot/views/create_worker.py index 57145a89c..501a932ac 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_worker.py @@ -1,10 +1,10 @@ from .notice import NoticeDialog -from .ui.create_bot_window_ui import Ui_Dialog +from .ui.create_worker_window_ui import Ui_Dialog from PyQt5 import QtWidgets -class CreateBotView(QtWidgets.QDialog): +class CreateWorkerView(QtWidgets.QDialog): def __init__(self, controller): super().__init__() @@ -17,12 +17,13 @@ def __init__(self, controller): self.ui.strategy_input.addItems(self.controller.strategies) self.ui.base_asset_input.addItems(self.controller.base_assets) - self.bot_name = controller.get_unique_bot_name() - self.ui.bot_name_input.setText(self.bot_name) + self.worker_name = controller.get_unique_worker_name() + self.ui.worker_name_input.setText(self.worker_name) self.ui.save_button.clicked.connect(self.handle_save) self.ui.cancel_button.clicked.connect(self.reject) self.ui.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) + self.worker_data = {} def onchange_center_price_dynamic_checkbox(self): checkbox = self.ui.center_price_dynamic_checkbox @@ -31,9 +32,9 @@ def onchange_center_price_dynamic_checkbox(self): else: self.ui.center_price_input.setDisabled(False) - def validate_bot_name(self): - bot_name = self.ui.bot_name_input.text() - return self.controller.is_bot_name_valid(bot_name) + def validate_worker_name(self): + worker_name = self.ui.worker_name_input.text() + return self.controller.is_worker_name_valid(worker_name) def validate_asset(self, asset): return self.controller.is_asset_valid(asset) @@ -56,9 +57,9 @@ def validate_form(self): error_text = '' base_asset = self.ui.base_asset_input.currentText() quote_asset = self.ui.quote_asset_input.text() - if not self.validate_bot_name(): - bot_name = self.ui.bot_name_input.text() - error_text += 'Bot name needs to be unique. "{}" is already in use.'.format(bot_name) + '\n' + if not self.validate_worker_name(): + worker_name = self.ui.worker_name_input.text() + error_text += 'Worker name needs to be unique. "{}" is already in use.'.format(worker_name) + '\n' if not self.validate_asset(base_asset): error_text += 'Field "Base Asset" does not have a valid asset.' + '\n' if not self.validate_asset(quote_asset): @@ -97,14 +98,13 @@ def handle_save(self): base_asset = ui.base_asset_input.currentText() quote_asset = ui.quote_asset_input.text() strategy = ui.strategy_input.currentText() - bot_module = self.controller.get_strategy_module(strategy) - bot_data = { + worker_module = self.controller.get_strategy_module(strategy) + self.worker_data = { 'account': ui.account_input.text(), 'market': '{}/{}'.format(quote_asset, base_asset), - 'module': bot_module, + 'module': worker_module, 'strategy': strategy, 'target': target } - self.bot_name = ui.bot_name_input.text() - self.controller.add_bot_config(self.bot_name, bot_data) + self.worker_name = ui.worker_name_input.text() self.accept() diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_worker.py similarity index 69% rename from dexbot/views/edit_bot.py rename to dexbot/views/edit_worker.py index 404bcfa78..677b3a69b 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_worker.py @@ -1,28 +1,29 @@ -from .ui.edit_bot_window_ui import Ui_Dialog +from .ui.edit_worker_window_ui import Ui_Dialog from .confirmation import ConfirmationDialog from .notice import NoticeDialog from PyQt5 import QtWidgets -class EditBotView(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, controller, botname, config): +class EditWorkerView(QtWidgets.QDialog, Ui_Dialog): + + def __init__(self, controller, worker_name, config): super().__init__() self.controller = controller self.setupUi(self) - bot_data = config['bots'][botname] - self.strategy_input.addItems(self.controller.get_bot_current_strategy(bot_data)) - self.bot_name = botname - self.bot_name_input.setText(botname) - self.base_asset_input.addItem(self.controller.get_base_asset(bot_data)) + worker_data = config['workers'][worker_name] + self.strategy_input.addItems(self.controller.get_worker_current_strategy(worker_data)) + self.worker_name = worker_name + self.worker_name_input.setText(worker_name) + self.base_asset_input.addItem(self.controller.get_base_asset(worker_data)) self.base_asset_input.addItems(self.controller.base_assets) - self.quote_asset_input.setText(self.controller.get_quote_asset(bot_data)) - self.account_name.setText(self.controller.get_account(bot_data)) - self.amount_input.setValue(self.controller.get_target_amount(bot_data)) - self.center_price_input.setValue(self.controller.get_target_center_price(bot_data)) + self.quote_asset_input.setText(self.controller.get_quote_asset(worker_data)) + self.account_name.setText(self.controller.get_account(worker_data)) + self.amount_input.setValue(self.controller.get_target_amount(worker_data)) + self.center_price_input.setValue(self.controller.get_target_center_price(worker_data)) - center_price_dynamic = self.controller.get_target_center_price_dynamic(bot_data) + center_price_dynamic = self.controller.get_target_center_price_dynamic(worker_data) if center_price_dynamic: self.center_price_input.setEnabled(False) self.center_price_dynamic_checkbox.setChecked(True) @@ -30,10 +31,11 @@ def __init__(self, controller, botname, config): self.center_price_input.setEnabled(True) self.center_price_dynamic_checkbox.setChecked(False) - self.spread_input.setValue(self.controller.get_target_spread(bot_data)) + self.spread_input.setValue(self.controller.get_target_spread(worker_data)) self.save_button.clicked.connect(self.handle_save) self.cancel_button.clicked.connect(self.reject) self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) + self.worker_data = {} def onchange_center_price_dynamic_checkbox(self): checkbox = self.center_price_dynamic_checkbox @@ -42,10 +44,12 @@ def onchange_center_price_dynamic_checkbox(self): else: self.center_price_input.setDisabled(False) - def validate_bot_name(self): - old_bot_name = self.bot_name - bot_name = self.bot_name_input.text() - return self.controller.is_bot_name_valid(bot_name, old_bot_name) + def validate_worker_name(self): + old_worker_name = self.worker_name + worker_name = self.worker_name_input.text() + if old_worker_name != worker_name: + return self.controller.is_worker_name_valid(worker_name) + return True def validate_asset(self, asset): return self.controller.is_asset_valid(asset) @@ -60,9 +64,9 @@ def validate_form(self): base_asset = self.base_asset_input.currentText() quote_asset = self.quote_asset_input.text() - if not self.validate_bot_name(): - bot_name = self.bot_name_input.text() - error_text += 'Bot name needs to be unique. "{}" is already in use.\n'.format(bot_name) + if not self.validate_worker_name(): + worker_name = self.worker_name_input.text() + error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) if not self.validate_asset(base_asset): error_text += 'Field "Base Asset" does not have a valid asset.\n' if not self.validate_asset(quote_asset): @@ -79,8 +83,8 @@ def validate_form(self): @staticmethod def handle_save_dialog(): - dialog = ConfirmationDialog('Saving the bot will cancel all the current orders.\n' - 'Are you sure you want to save the bot?') + dialog = ConfirmationDialog('Saving the worker will cancel all the current orders.\n' + 'Are you sure you want to do this?') return dialog.exec_() def handle_save(self): @@ -101,14 +105,13 @@ def handle_save(self): base_asset = self.base_asset_input.currentText() quote_asset = self.quote_asset_input.text() strategy = self.strategy_input.currentText() - bot_module = self.controller.get_strategy_module(strategy) - bot_data = { + worker_module = self.controller.get_strategy_module(strategy) + self.worker_data = { 'account': self.account_name.text(), 'market': '{}/{}'.format(quote_asset, base_asset), - 'module': bot_module, + 'module': worker_module, 'strategy': strategy, 'target': target } - self.bot_name = self.bot_name_input.text() - self.controller.add_bot_config(self.bot_name, bot_data) + self.worker_name = self.worker_name_input.text() self.accept() diff --git a/dexbot/views/ui/create_bot_window.ui b/dexbot/views/ui/create_worker_window.ui similarity index 97% rename from dexbot/views/ui/create_bot_window.ui rename to dexbot/views/ui/create_worker_window.ui index 8f8d0e824..aba78c846 100644 --- a/dexbot/views/ui/create_bot_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -11,7 +11,7 @@ - DEXBot - Create Bot + DEXBot - Create Worker true @@ -183,7 +183,7 @@ - Bot Parameters + Worker Parameters @@ -376,7 +376,7 @@ - Bot Details + Worker Details @@ -409,7 +409,7 @@ - + 110 @@ -423,15 +423,15 @@ - Bot Name + Worker Name - bot_name_input + worker_name_input - + @@ -516,7 +516,7 @@ strategy_input - bot_name_input + worker_name_input base_asset_input quote_asset_input account_input diff --git a/dexbot/views/ui/edit_bot_window.ui b/dexbot/views/ui/edit_worker_window.ui similarity index 97% rename from dexbot/views/ui/edit_bot_window.ui rename to dexbot/views/ui/edit_worker_window.ui index 6a682d7f6..fd4b61ea6 100644 --- a/dexbot/views/ui/edit_bot_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -7,17 +7,17 @@ 0 0 400 - 458 + 459 - DEXBot - Edit Bot + DEXBot - Edit Worker - Bot Details + Worker Details @@ -50,7 +50,7 @@ - + 110 @@ -64,15 +64,15 @@ - Bot Name + Worker Name - bot_name_input + worker_name_input - + @@ -200,7 +200,7 @@ - Bot Parameters + Worker Parameters diff --git a/dexbot/views/ui/bot_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui similarity index 99% rename from dexbot/views/ui/bot_item_widget.ui rename to dexbot/views/ui/worker_item_widget.ui index 4afc52378..57447bb20 100644 --- a/dexbot/views/ui/bot_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -92,7 +92,7 @@ 1 - + 12 @@ -104,7 +104,7 @@ color: #005B78; - Botname + Worker name Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft diff --git a/dexbot/views/ui/bot_list_window.ui b/dexbot/views/ui/worker_list_window.ui similarity index 93% rename from dexbot/views/ui/bot_list_window.ui rename to dexbot/views/ui/worker_list_window.ui index 142133653..e60edf4e9 100644 --- a/dexbot/views/ui/bot_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -71,7 +71,7 @@ -7 - + @@ -107,7 +107,7 @@ - + PointingHandCursor @@ -115,7 +115,7 @@ -1 - Add bot + Add worker @@ -145,10 +145,6 @@ - - scrollArea - widget - line diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py new file mode 100644 index 000000000..8d017347f --- /dev/null +++ b/dexbot/views/worker_item.py @@ -0,0 +1,112 @@ +from .ui.worker_item_widget_ui import Ui_widget +from .confirmation import ConfirmationDialog +from .edit_worker import EditWorkerView +from dexbot.storage import db_worker +from dexbot.controllers.create_worker_controller import CreateWorkerController + +from PyQt5 import QtWidgets + + +class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): + + def __init__(self, worker_name, config, main_ctrl, view): + super().__init__() + + self.main_ctrl = main_ctrl + self.running = False + self.worker_name = worker_name + self.config = config + self.view = view + + self.setupUi(self) + self.pause_button.hide() + + self.pause_button.clicked.connect(self.pause_worker) + self.play_button.clicked.connect(self.start_worker) + self.remove_button.clicked.connect(self.remove_widget_dialog) + self.edit_button.clicked.connect(self.handle_edit_worker) + + self.setup_ui_data(config) + + def setup_ui_data(self, config): + worker_name = list(config['workers'].keys())[0] + self.set_worker_name(worker_name) + + market = config['workers'][worker_name]['market'] + self.set_worker_market(market) + + profit = db_worker.execute(db_worker.get_item, worker_name, 'profit') + if profit: + self.set_worker_profit(profit) + + percentage = db_worker.execute(db_worker.get_item, worker_name, 'slider') + if percentage: + self.set_worker_slider(percentage) + + def start_worker(self): + self.running = True + self.pause_button.show() + self.play_button.hide() + + self.main_ctrl.create_worker(self.worker_name, self.config, self.view) + + def pause_worker(self): + self.running = False + self.pause_button.hide() + self.play_button.show() + + self.main_ctrl.stop_worker(self.worker_name) + + def set_worker_name(self, value): + self.worker_name_label.setText(value) + + def set_worker_account(self, value): + pass + + def set_worker_market(self, value): + self.currency_label.setText(value) + + def set_worker_profit(self, value): + if value >= 0: + value = '+' + str(value) + + value = str(value) + '%' + self.profit_label.setText(value) + + def set_worker_slider(self, value): + self.order_slider.setSliderPosition(value) + + def remove_widget_dialog(self): + dialog = ConfirmationDialog('Are you sure you want to remove worker "{}"?'.format(self.worker_name)) + return_value = dialog.exec_() + if return_value: + self.remove_widget() + self.main_ctrl.remove_worker_config(self.worker_name) + + def remove_widget(self): + self.main_ctrl.remove_worker(self.worker_name) + self.deleteLater() + self.view.remove_worker_widget(self.worker_name) + + # Todo: Remove the line below this after multi-worker support is added + self.view.ui.add_worker_button.setEnabled(True) + + def reload_widget(self, worker_name): + """ Cancels orders of the widget's worker and then reloads the data of the widget + """ + self.remove_widget() + self.view.add_worker_widget(worker_name) + self.config = self.main_ctrl.get_worker_config(worker_name) + + def handle_edit_worker(self): + controller = CreateWorkerController(self.main_ctrl) + edit_worker_dialog = EditWorkerView(controller, self.worker_name, self.config) + return_value = edit_worker_dialog.exec_() + + # User clicked save + if return_value: + self.main_ctrl.remove_worker_config(self.worker_name) + new_worker_name = edit_worker_dialog.worker_name + self.worker_name = new_worker_name + self.main_ctrl.add_worker_config(self.worker_name, edit_worker_dialog.worker_data) + self.reload_widget(self.worker_name) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py new file mode 100644 index 000000000..dbfd94e3e --- /dev/null +++ b/dexbot/views/worker_list.py @@ -0,0 +1,81 @@ +from .ui.worker_list_window_ui import Ui_MainWindow +from .create_worker import CreateWorkerView +from .worker_item import WorkerItemWidget +from dexbot.controllers.create_worker_controller import CreateWorkerController +from dexbot.queue.queue_dispatcher import ThreadDispatcher + +from PyQt5 import QtWidgets + + +class MainView(QtWidgets.QMainWindow): + + worker_widgets = dict() + + def __init__(self, main_ctrl): + self.main_ctrl = main_ctrl + super(MainView, self).__init__() + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + self.worker_container = self.ui.verticalLayout + + self.ui.add_worker_button.clicked.connect(self.handle_add_worker) + + # Load worker widgets from config file + workers = main_ctrl.get_workers_data() + for worker_name in workers: + self.add_worker_widget(worker_name) + + # Artificially limit the number of workers to 1 until it's officially supported + # Todo: Remove the 2 lines below this after multi-worker support is added + self.ui.add_worker_button.setEnabled(False) + break + + # Dispatcher polls for events from the workers that are used to change the ui + self.dispatcher = ThreadDispatcher(self) + self.dispatcher.start() + + def add_worker_widget(self, worker_name): + config = self.main_ctrl.get_worker_config(worker_name) + widget = WorkerItemWidget(worker_name, config, self.main_ctrl, self) + widget.setFixedSize(widget.frameSize()) + self.worker_container.addWidget(widget) + self.worker_widgets[worker_name] = widget + + # Todo: Remove the line below this after multi-worker support is added + self.ui.add_worker_button.setEnabled(False) + + def remove_worker_widget(self, worker_name): + self.worker_widgets.pop(worker_name, None) + + # Todo: Remove the line below this after multi-worker support is added + self.ui.add_worker_button.setEnabled(True) + + def handle_add_worker(self): + controller = CreateWorkerController(self.main_ctrl) + create_worker_dialog = CreateWorkerView(controller) + return_value = create_worker_dialog.exec_() + + # User clicked save + if return_value == 1: + worker_name = create_worker_dialog.worker_name + self.main_ctrl.add_worker_config(worker_name, create_worker_dialog.worker_data) + self.add_worker_widget(worker_name) + + def set_worker_name(self, worker_name, value): + self.worker_widgets[worker_name].set_worker_name(value) + + def set_worker_account(self, worker_name, value): + self.worker_widgets[worker_name].set_worker_account(value) + + def set_worker_profit(self, worker_name, value): + self.worker_widgets[worker_name].set_worker_profit(value) + + def set_worker_market(self, worker_name, value): + self.worker_widgets[worker_name].set_worker_market(value) + + def set_worker_slider(self, worker_name, value): + self.worker_widgets[worker_name].set_worker_slider(value) + + def customEvent(self, event): + # Process idle_queue_dispatcher events + event.callback() diff --git a/dexbot/worker.py b/dexbot/worker.py new file mode 100644 index 000000000..1971db032 --- /dev/null +++ b/dexbot/worker.py @@ -0,0 +1,170 @@ +import importlib +import sys +import logging +import os.path +import threading + +from dexbot.basestrategy import BaseStrategy + +from bitshares.notify import Notify +from bitshares.instance import shared_bitshares_instance + +import dexbot.errors as errors + +log = logging.getLogger(__name__) + +log_workers = logging.getLogger('dexbot.per_worker') +# NOTE this is the special logger for per-worker events +# it returns LogRecords with extra fields: worker_name, account, market and is_disabled +# is_disabled is a callable returning True if the worker is currently disabled. +# GUIs can add a handler to this logger to get a stream of events of the running workers. + + +class WorkerInfrastructure(threading.Thread): + + workers = dict() + + def __init__( + self, + config, + bitshares_instance=None, + view=None + ): + super().__init__() + + # BitShares instance + self.bitshares = bitshares_instance or shared_bitshares_instance() + self.config = config + self.view = view + self.jobs = set() + self.notify = None + + def init_workers(self): + """Do the actual initialisation of workers + Potentially quite slow (tens of seconds) + So called as part of run() + """ + # set the module search path + user_worker_path = os.path.expanduser("~/bots") + if os.path.exists(user_worker_path): + sys.path.append(user_worker_path) + + # Load all accounts and markets in use to subscribe to them + accounts = set() + markets = set() + + # Initialize workers: + for worker_name, worker in self.config["workers"].items(): + if "account" not in worker: + log_workers.critical("Worker has no account", extra={ + 'worker_name': worker_name, 'account': 'unknown', + 'market': 'unknown', 'is_disabled': (lambda: True) + }) + continue + if "market" not in worker: + log_workers.critical("Worker has no market", extra={ + 'worker_name': worker_name, 'account': worker['account'], + 'market': 'unknown', 'is_disabled': (lambda: True) + }) + continue + try: + strategy_class = getattr( + importlib.import_module(worker["module"]), + 'Strategy' + ) + self.workers[worker_name] = strategy_class( + config=self.config, + name=worker_name, + bitshares_instance=self.bitshares, + view=self.view + ) + markets.add(worker['market']) + accounts.add(worker['account']) + except BaseException: + log_workers.exception("Worker initialisation", extra={ + 'worker_name': worker_name, 'account': worker['account'], + 'market': 'unknown', 'is_disabled': (lambda: True) + }) + + if len(markets) == 0: + log.critical("No workers to launch, exiting") + raise errors.NoWorkersAvailable() + + # Create notification instance + # Technically, this will multiplex markets and accounts and + # we need to demultiplex the events after we have received them + self.notify = Notify( + markets=list(markets), + accounts=list(accounts), + on_market=self.on_market, + on_account=self.on_account, + on_block=self.on_block, + bitshares_instance=self.bitshares + ) + + # Events + def on_block(self, data): + if self.jobs: + try: + for job in self.jobs: + job() + finally: + self.jobs = set() + for worker_name, worker in self.config["workers"].items(): + if worker_name not in self.workers or self.workers[worker_name].disabled: + continue + try: + self.workers[worker_name].ontick(data) + except Exception as e: + self.workers[worker_name].error_ontick(e) + self.workers[worker_name].log.exception("in .tick()") + + def on_market(self, data): + if data.get("deleted", False): # No info available on deleted orders + return + for worker_name, worker in self.config["workers"].items(): + if self.workers[worker_name].disabled: + self.workers[worker_name].log.debug('Worker "{}" is disabled'.format(worker_name)) + continue + if worker["market"] == data.market: + try: + self.workers[worker_name].onMarketUpdate(data) + except Exception as e: + self.workers[worker_name].error_onMarketUpdate(e) + self.workers[worker_name].log.exception(".onMarketUpdate()") + + def on_account(self, account_update): + account = account_update.account + for worker_name, worker in self.config["workers"].items(): + if self.workers[worker_name].disabled: + self.workers[worker_name].log.info('Worker "{}" is disabled'.format(worker_name)) + continue + if worker["account"] == account["name"]: + try: + self.workers[worker_name].onAccount(account_update) + except Exception as e: + self.workers[worker_name].error_onAccount(e) + self.workers[worker_name].log.exception(".onAccountUpdate()") + + def run(self): + self.init_workers() + self.notify.listen() + + def stop(self): + for worker in self.workers: + self.workers[worker].cancel_all() + self.notify.websocket.close() + + def remove_worker(self): + for worker in self.workers: + self.workers[worker].purge() + + @staticmethod + def remove_offline_worker(config, worker_name): + # Initialize the base strategy to get control over the data + strategy = BaseStrategy(config, worker_name) + strategy.purge() + + def do_next_tick(self, job): + """Add a callable to be executed on the next tick""" + self.jobs.add(job) diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 8b1378917..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1 +0,0 @@ - From 25b3c3d8c729761f496f3626d105584befe23ccd Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 23 Mar 2018 09:17:40 +0200 Subject: [PATCH 036/187] Change the name of simple strategy to relative orders --- dexbot/controllers/create_worker_controller.py | 2 +- dexbot/strategies/{simple.py => relative_orders.py} | 2 +- dexbot/views/ui/worker_item_widget.ui | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename dexbot/strategies/{simple.py => relative_orders.py} (99%) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 09af54d35..c26bcc9a2 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -17,7 +17,7 @@ def __init__(self, main_ctrl): @property def strategies(self): strategies = { - 'Simple Strategy': 'dexbot.strategies.simple' + 'Relative Orders': 'dexbot.strategies.relative_orders' } return strategies diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/relative_orders.py similarity index 99% rename from dexbot/strategies/simple.py rename to dexbot/strategies/relative_orders.py index 5aeae13b8..7d281a448 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/relative_orders.py @@ -10,7 +10,7 @@ class Strategy(BaseStrategy): """ - Simple strategy + Relative Orders strategy This strategy places a buy and a sell wall that change height over time """ diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index 57447bb20..e55054703 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -136,7 +136,7 @@ color: #005B78; - SIMPLE STRATEGY + RELATIVE ORDERS Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing From a45a815334733d431a34cf191f484d186f4514d6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 23 Mar 2018 10:02:31 +0200 Subject: [PATCH 037/187] Fix typo --- dexbot/controllers/create_worker_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index c26bcc9a2..ee2d386a3 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -87,7 +87,7 @@ def get_unique_worker_name(): workers = MainController.get_workers_data().keys() worker_name = "Worker {0}".format(index) while worker_name in workers: - worker_name = "worker {0}".format(index) + worker_name = "Worker {0}".format(index) index += 1 return worker_name From 8b904663bd9450c500f1c5e80820972efb1cb7ee Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 23 Mar 2018 12:21:41 +0200 Subject: [PATCH 038/187] Fix crash when private active key is empty --- dexbot/controllers/create_worker_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index ee2d386a3..068684218 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -56,6 +56,9 @@ def account_exists(self, account): return False def is_account_valid(self, account, private_key): + if not private_key or not account: + return False + wallet = self.bitshares.wallet try: pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) From 2520ef81e54b005535674763c1efa8cc48f8a617 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 23 Mar 2018 12:29:15 +0200 Subject: [PATCH 039/187] Fix line-ending related issues in the gui --- dexbot/views/create_worker.py | 13 +++++++------ dexbot/views/edit_worker.py | 1 + 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 501a932ac..12d7cc8ec 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -59,17 +59,18 @@ def validate_form(self): quote_asset = self.ui.quote_asset_input.text() if not self.validate_worker_name(): worker_name = self.ui.worker_name_input.text() - error_text += 'Worker name needs to be unique. "{}" is already in use.'.format(worker_name) + '\n' + error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) if not self.validate_asset(base_asset): - error_text += 'Field "Base Asset" does not have a valid asset.' + '\n' + error_text += 'Field "Base Asset" does not have a valid asset.\n' if not self.validate_asset(quote_asset): - error_text += 'Field "Quote Asset" does not have a valid asset.' + '\n' + error_text += 'Field "Quote Asset" does not have a valid asset.\n' if not self.validate_market(): - error_text += "Market {}/{} doesn't exist.".format(base_asset, quote_asset) + '\n' + error_text += "Market {}/{} doesn't exist.\n".format(base_asset, quote_asset) if not self.validate_account_name(): - error_text += "Account doesn't exist." + '\n' + error_text += "Account doesn't exist.\n" if not self.validate_account(): - error_text += 'Private key is invalid.' + '\n' + error_text += 'Private key is invalid.\n' + error_text = error_text.rstrip() # Remove the extra line-ending if error_text: dialog = NoticeDialog(error_text) diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 677b3a69b..480c4c72b 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -73,6 +73,7 @@ def validate_form(self): error_text += 'Field "Quote Asset" does not have a valid asset.\n' if not self.validate_market(): error_text += "Market {}/{} doesn't exist.\n".format(base_asset, quote_asset) + error_text = error_text.rstrip() # Remove the extra line-ending if error_text: dialog = NoticeDialog(error_text) From 4dfdddbd05b2af651dbfdcad617e8747e45263ab Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 23 Mar 2018 12:31:56 +0200 Subject: [PATCH 040/187] Change max worker amount to 10 in the gui --- .../controllers/create_worker_controller.py | 9 +++++++- dexbot/views/create_worker.py | 7 +++++++ dexbot/views/worker_list.py | 21 ++++++++++++------- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 068684218..6e556a216 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -5,7 +5,6 @@ from bitshares.asset import Asset from bitshares.account import Account from bitsharesbase.account import PrivateKey -from ruamel.yaml import YAML class CreateWorkerController: @@ -73,6 +72,14 @@ def is_account_valid(self, account, private_key): else: return False + @staticmethod + def is_account_in_use(account): + workers = MainController.get_workers_data() + for worker_name, worker in workers.items(): + if worker['account'] == account: + return True + return False + def add_private_key(self, private_key): wallet = self.bitshares.wallet try: diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 12d7cc8ec..4ac723965 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -53,6 +53,10 @@ def validate_account(self): private_key = self.ui.private_key_input.text() return self.controller.is_account_valid(account, private_key) + def validate_account_not_in_use(self): + account = self.ui.account_input.text() + return not self.controller.is_account_in_use(account) + def validate_form(self): error_text = '' base_asset = self.ui.base_asset_input.currentText() @@ -70,6 +74,9 @@ def validate_form(self): error_text += "Account doesn't exist.\n" if not self.validate_account(): error_text += 'Private key is invalid.\n' + if not self.validate_account_not_in_use(): + account = self.ui.account_input.text() + error_text += 'Use a different account. "{}" is already in use.\n'.format(account) error_text = error_text.rstrip() # Remove the extra line-ending if error_text: diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index dbfd94e3e..935532e46 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -17,6 +17,8 @@ def __init__(self, main_ctrl): self.ui = Ui_MainWindow() self.ui.setupUi(self) self.worker_container = self.ui.verticalLayout + self.max_workers = 10 + self.num_of_workers = 0 self.ui.add_worker_button.clicked.connect(self.handle_add_worker) @@ -25,10 +27,11 @@ def __init__(self, main_ctrl): for worker_name in workers: self.add_worker_widget(worker_name) - # Artificially limit the number of workers to 1 until it's officially supported - # Todo: Remove the 2 lines below this after multi-worker support is added - self.ui.add_worker_button.setEnabled(False) - break + # Limit the max amount of workers so that the performance isn't greatly affected + self.num_of_workers += 1 + if self.num_of_workers >= self.max_workers: + self.ui.add_worker_button.setEnabled(False) + break # Dispatcher polls for events from the workers that are used to change the ui self.dispatcher = ThreadDispatcher(self) @@ -41,14 +44,16 @@ def add_worker_widget(self, worker_name): self.worker_container.addWidget(widget) self.worker_widgets[worker_name] = widget - # Todo: Remove the line below this after multi-worker support is added - self.ui.add_worker_button.setEnabled(False) + self.num_of_workers += 1 + if self.num_of_workers >= self.max_workers: + self.ui.add_worker_button.setEnabled(False) def remove_worker_widget(self, worker_name): self.worker_widgets.pop(worker_name, None) - # Todo: Remove the line below this after multi-worker support is added - self.ui.add_worker_button.setEnabled(True) + self.num_of_workers -= 1 + if self.num_of_workers < self.max_workers: + self.ui.add_worker_button.setEnabled(True) def handle_add_worker(self): controller = CreateWorkerController(self.main_ctrl) From a34b5c8d35c5ebcccc62ca177d3c37689e973899 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 26 Mar 2018 12:18:02 +0300 Subject: [PATCH 041/187] Change relative order strategy code layout --- dexbot/strategies/relative_orders.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 7d281a448..84dff3b8b 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -51,6 +51,7 @@ def calculate_order_prices(self): self.sell_price = self.center_price * (1 + (self.target["spread"] / 2) / 100) def error(self, *args, **kwargs): + self.cancell_all() self.disabled = True self.log.info(self.execute()) @@ -247,15 +248,9 @@ def update_gui_profit(self): def update_gui_slider(self): buy_order = self['buy_order'] - if buy_order: - buy_amount = buy_order['quote']['amount'] - else: - buy_amount = 0 + buy_amount = self.get_order_amount(buy_order, 'quote') sell_order = self['sell_order'] - if sell_order: - sell_amount = sell_order['base']['amount'] - else: - sell_amount = 0 + sell_amount = self.get_order_amount(sell_order, 'base') total = buy_amount + sell_amount if not total: # Prevent division by zero From f9af749bc05f7c13f18f90b3aef58e5e22b22630 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 28 Mar 2018 09:42:49 +0300 Subject: [PATCH 042/187] Change ruamel.yaml required version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8599f6ace..a80b9dfb9 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def run(self): "click", "sqlalchemy", "appdirs", - "ruamel.yaml" + "ruamel.yaml>=0.15.37" ], include_package_data=True, ) From cea9fc74c336184374929de96c1cc0b47b05bbc2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 28 Mar 2018 09:43:43 +0300 Subject: [PATCH 043/187] Fix typoed method in relative orders --- dexbot/strategies/relative_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 84dff3b8b..abf2ccfa0 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -51,7 +51,7 @@ def calculate_order_prices(self): self.sell_price = self.center_price * (1 + (self.target["spread"] / 2) / 100) def error(self, *args, **kwargs): - self.cancell_all() + self.cancel_all() self.disabled = True self.log.info(self.execute()) From deaeb4ffc9f68812c776533c4cdc694d7325406f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 28 Mar 2018 12:47:59 +0300 Subject: [PATCH 044/187] Fix pep8 error --- app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app.py b/app.py index c75827fd4..45992f4b9 100644 --- a/app.py +++ b/app.py @@ -32,6 +32,7 @@ def __init__(self, sys_argv): else: sys.exit() + if __name__ == '__main__': app = App(sys.argv) sys.exit(app.exec_()) From c98f5b1f9a55a331c684349cf779cf0299626376 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 28 Mar 2018 15:29:32 +0300 Subject: [PATCH 045/187] Add multi worker support to GUI --- dexbot/controllers/main_controller.py | 53 +++++++---- dexbot/views/worker_item.py | 32 ++++--- dexbot/worker.py | 125 +++++++++++++++++--------- 3 files changed, 139 insertions(+), 71 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 0a5c91066..638d84f44 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -6,35 +6,38 @@ class MainController: - workers = dict() - def __init__(self, bitshares_instance): self.bitshares_instance = bitshares_instance set_shared_bitshares_instance(bitshares_instance) - self.worker_template = WorkerInfrastructure + self.worker_manager = None def create_worker(self, worker_name, config, view): # Todo: Add some threading here so that the GUI doesn't freeze - worker = self.worker_template(config, self.bitshares_instance, view) - worker.daemon = True - worker.start() - self.workers[worker_name] = worker + if self.worker_manager and self.worker_manager.is_alive(): + self.worker_manager.add_worker(worker_name, config) + else: + self.worker_manager = WorkerInfrastructure(config, self.bitshares_instance, view) + self.worker_manager.daemon = True + self.worker_manager.start() def stop_worker(self, worker_name): - self.workers[worker_name].stop() - self.workers.pop(worker_name, None) + self.worker_manager.stop(worker_name) def remove_worker(self, worker_name): # Todo: Add some threading here so that the GUI doesn't freeze - if worker_name in self.workers: - # Worker currently running - self.workers[worker_name].remove_worker() - self.workers[worker_name].stop() - self.workers.pop(worker_name, None) + if self.worker_manager and self.worker_manager.is_alive(): + # Worker manager currently running + if worker_name in self.worker_manager.workers: + self.worker_manager.remove_worker(worker_name) + self.worker_manager.stop(worker_name) + else: + # Worker not running + config = self.get_worker_config(worker_name) + WorkerInfrastructure.remove_offline_worker(config, worker_name) else: - # Worker not running + # Worker manager not running config = self.get_worker_config(worker_name) - self.worker_template.remove_offline_worker(config, worker_name) + WorkerInfrastructure.remove_offline_worker(config, worker_name) @staticmethod def load_config(): @@ -83,3 +86,21 @@ def add_worker_config(worker_name, worker_data): with open("config.yml", "w") as f: yaml.dump(config, f) + + @staticmethod + def replace_worker_config(worker_name, new_worker_name, worker_data): + yaml = YAML() + with open('config.yml', 'r') as f: + config = yaml.load(f) + + workers = config['workers'] + # Rotate the dict keys to keep order + for _ in range(len(workers)): + key, value = workers.popitem(False) + if worker_name == key: + workers[new_worker_name] = worker_data + else: + workers[key] = value + + with open("config.yml", "w") as f: + yaml.dump(config, f) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 8d017347f..8cb0b48b2 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -38,25 +38,33 @@ def setup_ui_data(self, config): profit = db_worker.execute(db_worker.get_item, worker_name, 'profit') if profit: self.set_worker_profit(profit) + else: + self.set_worker_profit(0) percentage = db_worker.execute(db_worker.get_item, worker_name, 'slider') if percentage: self.set_worker_slider(percentage) + else: + self.set_worker_slider(50) def start_worker(self): + self._start_worker() + self.main_ctrl.create_worker(self.worker_name, self.config, self.view) + + def _start_worker(self): self.running = True self.pause_button.show() self.play_button.hide() - self.main_ctrl.create_worker(self.worker_name, self.config, self.view) - def pause_worker(self): + self._pause_worker() + self.main_ctrl.stop_worker(self.worker_name) + + def _pause_worker(self): self.running = False self.pause_button.hide() self.play_button.show() - self.main_ctrl.stop_worker(self.worker_name) - def set_worker_name(self, value): self.worker_name_label.setText(value) @@ -87,16 +95,15 @@ def remove_widget(self): self.main_ctrl.remove_worker(self.worker_name) self.deleteLater() self.view.remove_worker_widget(self.worker_name) - - # Todo: Remove the line below this after multi-worker support is added self.view.ui.add_worker_button.setEnabled(True) - def reload_widget(self, worker_name): + def reload_widget(self, worker_name, new_worker_name): """ Cancels orders of the widget's worker and then reloads the data of the widget """ - self.remove_widget() - self.view.add_worker_widget(worker_name) - self.config = self.main_ctrl.get_worker_config(worker_name) + self.main_ctrl.remove_worker(worker_name) + self.config = self.main_ctrl.get_worker_config(new_worker_name) + self.setup_ui_data(self.config) + self._pause_worker() def handle_edit_worker(self): controller = CreateWorkerController(self.main_ctrl) @@ -105,8 +112,7 @@ def handle_edit_worker(self): # User clicked save if return_value: - self.main_ctrl.remove_worker_config(self.worker_name) new_worker_name = edit_worker_dialog.worker_name + self.main_ctrl.replace_worker_config(self.worker_name, new_worker_name, edit_worker_dialog.worker_data) + self.reload_widget(self.worker_name, new_worker_name) self.worker_name = new_worker_name - self.main_ctrl.add_worker_config(self.worker_name, edit_worker_dialog.worker_data) - self.reload_widget(self.worker_name) diff --git a/dexbot/worker.py b/dexbot/worker.py index 1971db032..b3a8c5987 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -4,15 +4,13 @@ import os.path import threading +import dexbot.errors as errors from dexbot.basestrategy import BaseStrategy from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance -import dexbot.errors as errors - log = logging.getLogger(__name__) - log_workers = logging.getLogger('dexbot.per_worker') # NOTE this is the special logger for per-worker events # it returns LogRecords with extra fields: worker_name, account, market and is_disabled @@ -22,8 +20,6 @@ class WorkerInfrastructure(threading.Thread): - workers = dict() - def __init__( self, config, @@ -38,23 +34,21 @@ def __init__( self.view = view self.jobs = set() self.notify = None - - def init_workers(self): - """Do the actual initialisation of workers - Potentially quite slow (tens of seconds) - So called as part of run() - """ - # set the module search path + self.config_lock = threading.RLock() + self.workers = {} + + self.accounts = set() + self.markets = set() + + # Set the module search path user_worker_path = os.path.expanduser("~/bots") if os.path.exists(user_worker_path): sys.path.append(user_worker_path) - - # Load all accounts and markets in use to subscribe to them - accounts = set() - markets = set() - # Initialize workers: - for worker_name, worker in self.config["workers"].items(): + def init_workers(self, config): + """ Initialize the workers + """ + for worker_name, worker in config["workers"].items(): if "account" not in worker: log_workers.critical("Worker has no account", extra={ 'worker_name': worker_name, 'account': 'unknown', @@ -73,34 +67,37 @@ def init_workers(self): 'Strategy' ) self.workers[worker_name] = strategy_class( - config=self.config, + config=config, name=worker_name, bitshares_instance=self.bitshares, view=self.view ) - markets.add(worker['market']) - accounts.add(worker['account']) + self.markets.add(worker['market']) + self.accounts.add(worker['account']) except BaseException: log_workers.exception("Worker initialisation", extra={ 'worker_name': worker_name, 'account': worker['account'], 'market': 'unknown', 'is_disabled': (lambda: True) }) - if len(markets) == 0: + def update_notify(self): + if not self.config['workers']: log.critical("No workers to launch, exiting") raise errors.NoWorkersAvailable() - # Create notification instance - # Technically, this will multiplex markets and accounts and - # we need to demultiplex the events after we have received them - self.notify = Notify( - markets=list(markets), - accounts=list(accounts), - on_market=self.on_market, - on_account=self.on_account, - on_block=self.on_block, - bitshares_instance=self.bitshares - ) + if self.notify: + # Update the notification instance + self.notify.reset_subscriptions(list(self.accounts), list(self.markets)) + else: + # Initialize the notification instance + self.notify = Notify( + markets=list(self.markets), + accounts=list(self.accounts), + on_market=self.on_market, + on_account=self.on_account, + on_block=self.on_block, + bitshares_instance=self.bitshares + ) # Events def on_block(self, data): @@ -110,6 +107,8 @@ def on_block(self, data): job() finally: self.jobs = set() + + self.config_lock.acquire() for worker_name, worker in self.config["workers"].items(): if worker_name not in self.workers or self.workers[worker_name].disabled: continue @@ -118,10 +117,13 @@ def on_block(self, data): except Exception as e: self.workers[worker_name].error_ontick(e) self.workers[worker_name].log.exception("in .tick()") + self.config_lock.release() def on_market(self, data): if data.get("deleted", False): # No info available on deleted orders return + + self.config_lock.acquire() for worker_name, worker in self.config["workers"].items(): if self.workers[worker_name].disabled: self.workers[worker_name].log.debug('Worker "{}" is disabled'.format(worker_name)) @@ -132,8 +134,10 @@ def on_market(self, data): except Exception as e: self.workers[worker_name].error_onMarketUpdate(e) self.workers[worker_name].log.exception(".onMarketUpdate()") + self.config_lock.release() def on_account(self, account_update): + self.config_lock.acquire() account = account_update.account for worker_name, worker in self.config["workers"].items(): if self.workers[worker_name].disabled: @@ -145,19 +149,56 @@ def on_account(self, account_update): except Exception as e: self.workers[worker_name].error_onAccount(e) self.workers[worker_name].log.exception(".onAccountUpdate()") + self.config_lock.release() + + def add_worker(self, worker_name, config): + with self.config_lock: + self.config['workers'][worker_name] = config['workers'][worker_name] + self.init_workers(config) + self.update_notify() def run(self): - self.init_workers() + self.init_workers(self.config) + self.update_notify() self.notify.listen() - def stop(self): - for worker in self.workers: - self.workers[worker].cancel_all() - self.notify.websocket.close() - - def remove_worker(self): - for worker in self.workers: - self.workers[worker].purge() + def stop(self, worker_name=None): + if worker_name and len(self.workers) > 1: + # Kill only the specified worker + self.remove_market(worker_name) + with self.config_lock: + account = self.config['workers'][worker_name]['account'] + self.config['workers'].pop(worker_name) + + self.accounts.remove(account) + self.workers[worker_name].cancel_all() + self.workers.pop(worker_name, None) + self.update_notify() + else: + # Kill all of the workers + for worker in self.workers: + self.workers[worker].cancel_all() + self.workers = None + self.notify.websocket.close() + + def remove_worker(self, worker_name=None): + if worker_name: + self.workers[worker_name].purge() + else: + for worker in self.workers: + self.workers[worker].purge() + + def remove_market(self, worker_name): + """ Remove the market only if the worker is the only one using it + """ + with self.config_lock: + market = self.config['workers'][worker_name]['market'] + for name, worker in self.config['workers'].items(): + if market == worker['market']: + break # Found the same market, do nothing + else: + # No markets found, safe to remove + self.markets.remove(market) @staticmethod def remove_offline_worker(config, worker_name): From b3b7f50b3dc06af68162df4c4c3776c10cea4858 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 29 Mar 2018 13:23:12 +0300 Subject: [PATCH 046/187] Fix config passing bug --- dexbot/views/worker_item.py | 10 +++++----- dexbot/views/worker_list.py | 3 +-- dexbot/worker.py | 3 ++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 8cb0b48b2..bd54139c1 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -15,7 +15,7 @@ def __init__(self, worker_name, config, main_ctrl, view): self.main_ctrl = main_ctrl self.running = False self.worker_name = worker_name - self.config = config + self.worker_config = config self.view = view self.setupUi(self) @@ -49,7 +49,7 @@ def setup_ui_data(self, config): def start_worker(self): self._start_worker() - self.main_ctrl.create_worker(self.worker_name, self.config, self.view) + self.main_ctrl.create_worker(self.worker_name, self.worker_config, self.view) def _start_worker(self): self.running = True @@ -101,13 +101,13 @@ def reload_widget(self, worker_name, new_worker_name): """ Cancels orders of the widget's worker and then reloads the data of the widget """ self.main_ctrl.remove_worker(worker_name) - self.config = self.main_ctrl.get_worker_config(new_worker_name) - self.setup_ui_data(self.config) + self.worker_config = self.main_ctrl.get_worker_config(new_worker_name) + self.setup_ui_data(self.worker_config) self._pause_worker() def handle_edit_worker(self): controller = CreateWorkerController(self.main_ctrl) - edit_worker_dialog = EditWorkerView(controller, self.worker_name, self.config) + edit_worker_dialog = EditWorkerView(controller, self.worker_name, self.worker_config) return_value = edit_worker_dialog.exec_() # User clicked save diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 935532e46..b83c17b7f 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -9,8 +9,6 @@ class MainView(QtWidgets.QMainWindow): - worker_widgets = dict() - def __init__(self, main_ctrl): self.main_ctrl = main_ctrl super(MainView, self).__init__() @@ -19,6 +17,7 @@ def __init__(self, main_ctrl): self.worker_container = self.ui.verticalLayout self.max_workers = 10 self.num_of_workers = 0 + self.worker_widgets = {} self.ui.add_worker_button.clicked.connect(self.handle_add_worker) diff --git a/dexbot/worker.py b/dexbot/worker.py index b3a8c5987..c90231b1f 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -3,6 +3,7 @@ import logging import os.path import threading +import copy import dexbot.errors as errors from dexbot.basestrategy import BaseStrategy @@ -30,7 +31,7 @@ def __init__( # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() - self.config = config + self.config = copy.deepcopy(config) self.view = view self.jobs = set() self.notify = None From 5516febdfd367f53d09b30d6b8b90ca7df451747 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 26 Mar 2018 12:18:02 +0300 Subject: [PATCH 047/187] Change relative order strategy code layout --- dexbot/strategies/relative_orders.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 7d281a448..84dff3b8b 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -51,6 +51,7 @@ def calculate_order_prices(self): self.sell_price = self.center_price * (1 + (self.target["spread"] / 2) / 100) def error(self, *args, **kwargs): + self.cancell_all() self.disabled = True self.log.info(self.execute()) @@ -247,15 +248,9 @@ def update_gui_profit(self): def update_gui_slider(self): buy_order = self['buy_order'] - if buy_order: - buy_amount = buy_order['quote']['amount'] - else: - buy_amount = 0 + buy_amount = self.get_order_amount(buy_order, 'quote') sell_order = self['sell_order'] - if sell_order: - sell_amount = sell_order['base']['amount'] - else: - sell_amount = 0 + sell_amount = self.get_order_amount(sell_order, 'base') total = buy_amount + sell_amount if not total: # Prevent division by zero From 87b5a9b0b0328ad20d5b9711d3cd43d042e3ffaa Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 28 Mar 2018 09:43:43 +0300 Subject: [PATCH 048/187] Fix typoed method in relative orders --- dexbot/strategies/relative_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 84dff3b8b..abf2ccfa0 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -51,7 +51,7 @@ def calculate_order_prices(self): self.sell_price = self.center_price * (1 + (self.target["spread"] / 2) / 100) def error(self, *args, **kwargs): - self.cancell_all() + self.cancel_all() self.disabled = True self.log.info(self.execute()) From 3cf957bd4a2d9bc5f657756c6bffe762cecdd8e4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 29 Mar 2018 14:32:59 +0300 Subject: [PATCH 049/187] Change dexbot version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a80b9dfb9..07ddc743b 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from pyqt_distutils.build_ui import build_ui -VERSION = '0.1.2' +VERSION = '0.1.5' class InstallCommand(install): From 4c175236754d82e0cc257aaa9a682e9b623cdb17 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 09:40:44 +0300 Subject: [PATCH 050/187] Fix pep8 error --- dexbot/basestrategy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 662a1dd8e..e790adc07 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -120,9 +120,9 @@ def __init__( # a private logger that adds worker identify data to the LogRecord self.log = logging.LoggerAdapter(logging.getLogger('dexbot.per_worker'), {'worker_name': name, - 'account': self.worker['account'], - 'market': self.worker['market'], - 'is_disabled': lambda: self.disabled}) + 'account': self.worker['account'], + 'market': self.worker['market'], + 'is_disabled': lambda: self.disabled}) @property def calculate_center_price(self): From 4003a097627d5abc544be30d90c2605d28a24dfd Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 09:42:05 +0300 Subject: [PATCH 051/187] Change calculate_center_price method to be simpler --- dexbot/basestrategy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index e790adc07..3925dc3eb 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -134,13 +134,11 @@ def calculate_center_price(self): "Cannot estimate center price, there is no highest bid." ) self.disabled = True - return None - if lowest_ask is None or lowest_ask == 0.0: + elif lowest_ask is None or lowest_ask == 0.0: self.log.critical( "Cannot estimate center price, there is no lowest ask." ) self.disabled = True - return None else: center_price = (highest_bid['price'] + lowest_ask['price']) / 2 return center_price From f9e90ad9d975e9272d72c9f338eba41818a1330d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 10:04:10 +0300 Subject: [PATCH 052/187] Change get_updated_order method to accept ids --- dexbot/basestrategy.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 3925dc3eb..267e3d07d 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -157,12 +157,17 @@ def get_order(self, order_id): return False def get_updated_order(self, order): + """ Tries to get the updated order from the API + returns None if the order doesn't exist + """ if not order: - return False + return None + if isinstance(order, str): + order = {'id': order} for updated_order in self.updated_open_orders: if updated_order['id'] == order['id']: return updated_order - return False + return None @property def updated_open_orders(self): From 70f91c93f88bc848e15f9403ceeacc4b06437dc4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 10:05:05 +0300 Subject: [PATCH 053/187] Fix set_worker_profit method when using ints --- dexbot/views/worker_item.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index bd54139c1..e1dc922c3 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -75,6 +75,7 @@ def set_worker_market(self, value): self.currency_label.setText(value) def set_worker_profit(self, value): + value = float(value) if value >= 0: value = '+' + str(value) From bd8835b49f3644a6fa056b10359f94689563680c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 10:08:38 +0300 Subject: [PATCH 054/187] Remove get_converted_asset_amount method --- dexbot/basestrategy.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 267e3d07d..487f22c4b 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -212,18 +212,6 @@ def balance(self, asset): """ return self._account.balance(asset) - def get_converted_asset_amount(self, asset): - """ - Returns asset amount converted to base asset amount - """ - base_asset = self.market['base'] - quote_asset = Asset(asset['symbol'], bitshares_instance=self.bitshares) - if base_asset['symbol'] == quote_asset['symbol']: - return asset['amount'] - else: - market = Market(base=base_asset, quote=quote_asset, bitshares_instance=self.bitshares) - return market.ticker()['latest']['price'] * asset['amount'] - @property def test_mode(self): return self.config['node'] == "wss://node.testnet.bitshares.eu" From 90bb9c6faff1f49ecccf5a80319f5a61a4f4fd89 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 10:10:24 +0300 Subject: [PATCH 055/187] Change relative orders strategy behaviour --- dexbot/basestrategy.py | 65 ++++++++- dexbot/strategies/relative_orders.py | 196 ++++++-------------------- dexbot/views/ui/worker_item_widget.ui | 4 +- 3 files changed, 102 insertions(+), 163 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 487f22c4b..6f44f506e 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -1,6 +1,6 @@ import logging from events import Events -from bitshares.asset import Asset +from bitshares.amount import Amount from bitshares.market import Market from bitshares.account import Account from bitshares.price import FilledOrder, Order, UpdateCallOrder @@ -223,7 +223,7 @@ def balances(self): return self._account.balances def _callbackPlaceFillOrders(self, d): - """ This method distringuishes notifications caused by Matched orders + """ This method distinguishes notifications caused by Matched orders from those caused by placed orders """ if isinstance(d, FilledOrder): @@ -263,8 +263,7 @@ def cancel_all(self): ) def purge(self): - """ - Clear all the worker data from the database and cancel all orders + """ Clear all the worker data from the database and cancel all orders """ self.cancel_all() self.clear() @@ -273,6 +272,62 @@ def purge(self): def get_order_amount(order, asset_type): try: order_amount = order[asset_type]['amount'] - except KeyError: + except (KeyError, TypeError): order_amount = 0 return order_amount + + def total_balance(self, order_ids=None, return_asset=False): + """ Returns the combined balance of the given order ids and the account balance + The amounts are returned in quote and base assets of the market + + :param order_ids: list of order ids to be added to the balance + :param return_asset: true if returned values should be Amount instances + :return: dict with keys quote and base + """ + quote = 0 + base = 0 + quote_asset = self.market['quote']['id'] + base_asset = self.market['base']['id'] + + for balance in self.balances: + if balance.asset['id'] == quote_asset: + quote += balance['amount'] + elif balance.asset['id'] == base_asset: + base += balance['amount'] + + orders_balance = self.orders_balance(order_ids) + quote += orders_balance['quote'] + base += orders_balance['base'] + + if return_asset: + quote = Amount(quote, quote_asset) + base = Amount(base, base_asset) + + return {'quote': quote, 'base': base} + + def orders_balance(self, order_ids, return_asset=False): + if not order_ids: + order_ids = [] + elif isinstance(order_ids, str): + order_ids = [order_ids] + + quote = 0 + base = 0 + quote_asset = self.market['quote']['id'] + base_asset = self.market['base']['id'] + + for order_id in order_ids: + order = self.get_updated_order(order_id) + if not order: + continue + asset_id = order['base']['asset']['id'] + if asset_id == quote_asset: + quote += order['base']['amount'] + elif asset_id == base_asset: + base += order['base']['amount'] + + if return_asset: + quote = Amount(quote, quote_asset) + base = Amount(base, base_asset) + + return {'quote': quote, 'base': base} diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index abf2ccfa0..7b232a58b 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,33 +1,25 @@ -from collections import Counter +from dexbot.basestrategy import BaseStrategy +from dexbot.queue.idle_queue import idle_add from bitshares.amount import Amount from bitshares.price import Price -from bitshares.price import Order - -from dexbot.basestrategy import BaseStrategy -from dexbot.queue.idle_queue import idle_add class Strategy(BaseStrategy): - """ - Relative Orders strategy - This strategy places a buy and a sell wall that change height over time + """ Relative Orders strategy """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Define Callbacks - self.onMarketUpdate += self.test - self.ontick += self.tick + self.onMarketUpdate += self.check_orders + self.onAccount += self.check_orders self.error_ontick = self.error self.error_onMarketUpdate = self.error self.error_onAccount = self.error - # Counter for blocks - self.counter = Counter() - self.target = self.worker.get("target", {}) self.is_center_price_dynamic = self.target["center_price_dynamic"] if self.is_center_price_dynamic: @@ -43,6 +35,8 @@ def __init__(self, *args, **kwargs): self.worker_name = kwargs.get('name') self.view = kwargs.get('view') + self.check_orders() + def calculate_order_prices(self): if self.is_center_price_dynamic: self.center_price = self.calculate_center_price @@ -55,12 +49,18 @@ def error(self, *args, **kwargs): self.disabled = True self.log.info(self.execute()) - def init_strategy(self): - amount = self.target['amount'] / 2 + def update_orders(self): + amount = self.target['amount'] # Recalculate buy and sell order prices self.calculate_order_prices() + # Cancel the orders before redoing them + self.cancel_all() + self.log.info('An order was filled, canceling the orders') + + order_ids = [] + # Buy Side if float(self.balance(self.market["base"])) < self.buy_price * amount: self.log.critical( @@ -78,6 +78,7 @@ def init_strategy(self): self.log.info('Placed a buy order for {} {} @ {}'.format(amount, self.market["quote"], self.buy_price)) if buy_order: self['buy_order'] = buy_order + order_ids.append(buy_transaction['orderid']) # Sell Side if float(self.balance(self.market["quote"])) < amount: @@ -96,166 +97,49 @@ def init_strategy(self): self.log.info('Placed a sell order for {} {} @ {}'.format(amount, self.market["quote"], self.buy_price)) if sell_order: self['sell_order'] = sell_order + order_ids.append(sell_transaction['orderid']) - order_balance = self.orders_balance() - self['initial_balance'] = order_balance # Save to database - self.initial_balance = order_balance - - def update_orders(self, new_sell_order, new_buy_order): - """ - Update the orders - """ - # Stored orders - sell_order = self['sell_order'] - buy_order = self['buy_order'] - - # Recalculate buy and sell order prices - self.calculate_order_prices() - - sold_amount = 0 - if new_sell_order and new_sell_order['base']['amount'] < sell_order['base']['amount']: - # Some of the sell order was sold - sold_amount = sell_order['base']['amount'] - new_sell_order['base']['amount'] - elif not new_sell_order and sell_order: - # All of the sell order was sold - sold_amount = sell_order['base']['amount'] - - bought_amount = 0 - if new_buy_order and new_buy_order['quote']['amount'] < buy_order['quote']['amount']: - # Some of the buy order was bought - bought_amount = buy_order['quote']['amount'] - new_buy_order['quote']['amount'] - elif not new_buy_order and buy_order: - # All of the buy order was bought - bought_amount = buy_order['quote']['amount'] - - if sold_amount: - # We sold something, place updated buy order - buy_order_amount = self.get_order_amount(buy_order, 'quote') - new_buy_amount = buy_order_amount - bought_amount + sold_amount - if float(self.balance(self.market["base"])) < self.buy_price * new_buy_amount: - self.log.critical( - 'Insufficient buy balance, needed {} {}'.format(self.buy_price * new_buy_amount, - self.market['base']['symbol']) - ) - self.disabled = True - else: - if buy_order and not Order(buy_order['id'])['deleted']: - # Cancel the old order - self.cancel(buy_order) - - buy_transaction = self.market.buy( - self.buy_price, - Amount(amount=new_buy_amount, asset=self.market["quote"]), - account=self.account, - returnOrderId="head" - ) - buy_order = self.get_order(buy_transaction['orderid']) - self.log.info( - 'Placed a buy order for {} {} @ {}'.format(new_buy_amount, self.market["quote"], self.buy_price) - ) - if buy_order: - self['buy_order'] = buy_order - else: - # Update the buy order - self['buy_order'] = new_buy_order or {} - - if bought_amount: - # We bought something, place updated sell order - sell_order_amount = self.get_order_amount(sell_order, 'quote') - new_sell_amount = sell_order_amount + bought_amount - sold_amount - if float(self.balance(self.market["quote"])) < new_sell_amount: - self.log.critical( - "Insufficient sell balance, needed {} {}".format(new_sell_amount, self.market["quote"]['symbol']) - ) - self.disabled = True - else: - if sell_order and not Order(sell_order['id'])['deleted']: - # Cancel the old order - self.cancel(sell_order) - - sell_transaction = self.market.sell( - self.sell_price, - Amount(amount=new_sell_amount, asset=self.market["quote"]), - account=self.account, - returnOrderId="head" - ) - sell_order = self.get_order(sell_transaction['orderid']) - self.log.info( - 'Placed a sell order for {} {} @ {}'.format(new_sell_amount, self.market["quote"], self.buy_price) - ) - if sell_order: - self['sell_order'] = sell_order - else: - # Update the sell order - self['sell_order'] = new_sell_order or {} - - def orders_balance(self): - balance = 0 - orders = [o for o in [self['buy_order'], self['sell_order']] if o] # Strip empty orders - for order in orders: - if order['base']['symbol'] != self.market['base']['symbol']: - # Invert the market for easier calculation - if not isinstance(order, Price): - order = self.get_order(order['id']) - if order: - order.invert() - if order: - balance += order['base']['amount'] - - return balance - - def tick(self, d): - """ - Test orders every 10th block - """ - if not (self.counter["blocks"] or 0) % 10: - self.test() - self.counter["blocks"] += 1 + self['order_ids'] = order_ids - def test(self, *args, **kwargs): + def check_orders(self, *args, **kwargs): + """ Tests if the orders need updating """ - Tests if the orders need updating - """ - if 'sell_order' not in self or 'buy_order' not in self: - self.init_strategy() - else: - current_sell_order = self.get_updated_order(self['sell_order']) - current_buy_order = self.get_updated_order(self['buy_order']) + stored_sell_order = self['sell_order'] + stored_buy_order = self['buy_order'] + current_sell_order = self.get_updated_order(stored_sell_order) + current_buy_order = self.get_updated_order(stored_buy_order) - # Update checks - sell_order_updated = not current_sell_order or \ - current_sell_order['base']['amount'] != self['sell_order']['base']['amount'] - buy_order_updated = not current_buy_order or \ - current_buy_order['quote']['amount'] != self['buy_order']['quote']['amount'] + # Update checks + sell_order_updated = not current_sell_order or \ + current_sell_order['quote']['amount'] != stored_sell_order['quote']['amount'] + buy_order_updated = not current_buy_order or \ + current_buy_order['base']['amount'] != stored_buy_order['base']['amount'] - if (self['sell_order'] and sell_order_updated) or (self['buy_order'] and buy_order_updated): - # Either buy or sell order was changed, update both orders - self.update_orders(current_sell_order, current_buy_order) + if sell_order_updated or buy_order_updated: + # Either buy or sell order was changed, update both orders + self.update_orders() - if self.view: - self.update_gui_profit() - self.update_gui_slider() + if self.view: + self.update_gui_profit() + self.update_gui_slider() # GUI updaters def update_gui_profit(self): # Fixme: profit calculation doesn't work this way, figure out a better way to do this. if self.initial_balance: - profit = round((self.orders_balance() - self.initial_balance) / self.initial_balance, 3) + profit = round((self.orders_balance(None) - self.initial_balance) / self.initial_balance, 3) else: profit = 0 idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) self['profit'] = profit def update_gui_slider(self): - buy_order = self['buy_order'] - buy_amount = self.get_order_amount(buy_order, 'quote') - sell_order = self['sell_order'] - sell_amount = self.get_order_amount(sell_order, 'base') + total_balance = self.get_total_balance(self['order_ids']) + total = total_balance['quote'] + total_balance['base'] - total = buy_amount + sell_amount if not total: # Prevent division by zero - percentage = 0 + percentage = 50 else: - percentage = (buy_amount / total) * 100 + percentage = (total_balance['base'] / total) * 100 idle_add(self.view.set_worker_slider, self.worker_name, percentage) self['slider'] = percentage diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index e55054703..971260d3e 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -332,7 +332,7 @@ color: #005B78; - Buy + Base @@ -361,7 +361,7 @@ color: #005B78; - Sell + Quote From 3c1271f73601a8cafc5a66574f366796f9afc383 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 10:12:44 +0300 Subject: [PATCH 056/187] Remove unused import --- dexbot/strategies/relative_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 7b232a58b..be8234eda 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -2,7 +2,6 @@ from dexbot.queue.idle_queue import idle_add from bitshares.amount import Amount -from bitshares.price import Price class Strategy(BaseStrategy): From 6bb40656801a112166d71ed269526f4d8a7ee9e6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 10:26:48 +0300 Subject: [PATCH 057/187] Change dexbot version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 07ddc743b..75ec8121c 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from pyqt_distutils.build_ui import build_ui -VERSION = '0.1.5' +VERSION = '0.1.6' class InstallCommand(install): From a9431e15369e970f9e8ef82596e0fd9ab5bfbe88 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Wed, 4 Apr 2018 10:57:45 +0300 Subject: [PATCH 058/187] Removed confusing information and updated with relevant instructions --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 823fe4905..15a7e8eff 100644 --- a/README.md +++ b/README.md @@ -34,19 +34,16 @@ or $ pip install -r --user requirements.txt $ python setup.py install --user -## Configuration - -Configuration happens in `config.yml` +## Running the GUI -## Requirements +On Linux: `$ python ./app.py` -Add your account's private key to the pybitshares wallet using `uptick` +## Running the CLI + Check documentation here: [https://dexbot-ih.readthedocs.io/en/latest/setup.html] - uptick addkey - -## Execution +## Configuration - dexbot run +Configuration is done in the GUI or CLI interactively, and is stored in `config.yml`. You can change the default API node here if you want to, but otherwise there should be no need to touch it. # IMPORTANT NOTE From 858538859e27775890c73a53eee6cc543b38f7d2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 13:51:35 +0300 Subject: [PATCH 059/187] Change dexbot.sqlite file location Clean up the imports on the side --- dexbot/storage.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index 4a1410a80..ec785284a 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -1,19 +1,19 @@ -import sqlalchemy import os import json import threading import queue import uuid -import time -from sqlalchemy import create_engine, Table, Column, String, Integer, MetaData +from appdirs import user_data_dir + +from sqlalchemy import create_engine, Column, String, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from appdirs import user_data_dir + Base = declarative_base() # For dexbot.sqlite file appname = "dexbot" -appauthor = "ChainSquad GmbH" +appauthor = "Codaone Oy" storageDatabase = "dexbot.sqlite" From 9cabb63142faac8a401a6a8609b9b0583d387b16 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 15:10:13 +0300 Subject: [PATCH 060/187] Fix relative orders pyinstaller imports --- app.spec | 5 +++-- cli.spec | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app.spec b/app.spec index a3968ae3f..bd7a15f45 100644 --- a/app.spec +++ b/app.spec @@ -1,13 +1,14 @@ # -*- mode: python -*- -import os, sys +import os +import sys block_cipher = None hiddenimports_strategies = [ 'dexbot', 'dexbot.strategies', 'dexbot.strategies.echo', - 'dexbot.strategies.simple', + 'dexbot.strategies.relative_orders', 'dexbot.strategies.storagedemo', 'dexbot.strategies.walls', ] diff --git a/cli.spec b/cli.spec index 18daef423..70b92d4b2 100644 --- a/cli.spec +++ b/cli.spec @@ -1,6 +1,7 @@ # -*- mode: python -*- -import os, sys +import os +import sys block_cipher = None @@ -8,8 +9,7 @@ hiddenimports_strategies = [ 'dexbot', 'dexbot.strategies', 'dexbot.strategies.echo', - 'dexbot.strategies.follow_orders', - 'dexbot.strategies.simple', + 'dexbot.strategies.relative_orders', 'dexbot.strategies.storagedemo', 'dexbot.strategies.walls', ] From 372cb09999e071318924e789cf8c67246174283d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 15:24:09 +0300 Subject: [PATCH 061/187] Fix method typo in relative orders strategy --- dexbot/strategies/relative_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index be8234eda..a9c70d5e8 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -133,7 +133,7 @@ def update_gui_profit(self): self['profit'] = profit def update_gui_slider(self): - total_balance = self.get_total_balance(self['order_ids']) + total_balance = self.total_balance(self['order_ids']) total = total_balance['quote'] + total_balance['base'] if not total: # Prevent division by zero From 635f27149794a352bf025da2b87917f31e8a3942 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 4 Apr 2018 15:26:16 +0300 Subject: [PATCH 062/187] Add version number to the gui Also changed the version to be located in __init__.py of the module --- dexbot/__init__.py | 1 + dexbot/views/ui/worker_list_window.ui | 103 ++++++++++++++------------ dexbot/views/worker_list.py | 3 + setup.py | 7 +- 4 files changed, 64 insertions(+), 50 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e69de29bb..124e46203 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.7' diff --git a/dexbot/views/ui/worker_list_window.ui b/dexbot/views/ui/worker_list_window.ui index e60edf4e9..8d90a9691 100644 --- a/dexbot/views/ui/worker_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -27,52 +27,11 @@ - - - - - 1 - 1 - - - - false - - - QFrame::NoFrame - - - Qt::ScrollBarAsNeeded - - - QAbstractScrollArea::AdjustToContents - - - true - - - Qt::AlignHCenter|Qt::AlignTop + + + + Qt::Horizontal - - - - 389 - 0 - 18 - 18 - - - - - 0 - 0 - - - - -7 - - - @@ -137,15 +96,61 @@ - - - - Qt::Horizontal + + + + + 1 + 1 + + + + false + + + QFrame::NoFrame + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustToContents + + + true + + Qt::AlignHCenter|Qt::AlignTop + + + + + 389 + 0 + 18 + 18 + + + + + 0 + 0 + + + + -7 + + + + + + ArrowCursor + + diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index b83c17b7f..2c4c9084a 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -1,3 +1,5 @@ +from dexbot import __version__ + from .ui.worker_list_window_ui import Ui_MainWindow from .create_worker import CreateWorkerView from .worker_item import WorkerItemWidget @@ -18,6 +20,7 @@ def __init__(self, main_ctrl): self.max_workers = 10 self.num_of_workers = 0 self.worker_widgets = {} + self.ui.status_bar.showMessage("ver {}".format(__version__)) self.ui.add_worker_button.clicked.connect(self.handle_add_worker) diff --git a/setup.py b/setup.py index 75ec8121c..47ac1e7c6 100755 --- a/setup.py +++ b/setup.py @@ -2,10 +2,15 @@ from setuptools import setup from setuptools.command.install import install +from distutils.util import convert_path from pyqt_distutils.build_ui import build_ui -VERSION = '0.1.6' +main_ns = {} +ver_path = convert_path('dexbot/__init__.py') +with open(ver_path) as ver_file: + exec(ver_file.read(), main_ns) + VERSION = main_ns['__version__'] class InstallCommand(install): From 1632f90e9a167f3731175eab473d9d87e9f8cf0f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 6 Apr 2018 09:07:53 +0300 Subject: [PATCH 063/187] Remove unneeded operation --- dexbot/worker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index c90231b1f..07f502650 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -179,7 +179,6 @@ def stop(self, worker_name=None): # Kill all of the workers for worker in self.workers: self.workers[worker].cancel_all() - self.workers = None self.notify.websocket.close() def remove_worker(self, worker_name=None): From 63a7fd8d27a3614f45c896eb81e3ef0a5363b798 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 6 Apr 2018 09:10:27 +0300 Subject: [PATCH 064/187] Make logging messages more readable --- dexbot/basestrategy.py | 16 ++++++++++------ dexbot/cli.py | 10 ++++------ dexbot/strategies/relative_orders.py | 10 +++++++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6f44f506e..8915fc88d 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -114,15 +114,18 @@ def __init__( # Settings for bitshares instance self.bitshares.bundle = bool(self.worker.get("bundle", False)) - # disabled flag - this flag can be flipped to True by a worker and + # Disabled flag - this flag can be flipped to True by a worker and # will be reset to False after reset only self.disabled = False - # a private logger that adds worker identify data to the LogRecord - self.log = logging.LoggerAdapter(logging.getLogger('dexbot.per_worker'), {'worker_name': name, - 'account': self.worker['account'], - 'market': self.worker['market'], - 'is_disabled': lambda: self.disabled}) + # A private logger that adds worker identify data to the LogRecord + self.log = logging.LoggerAdapter( + logging.getLogger('dexbot.per_worker'), + {'worker_name': name, + 'account': self.worker['account'], + 'market': self.worker['market'], + 'is_disabled': lambda: self.disabled} + ) @property def calculate_center_price(self): @@ -257,6 +260,7 @@ def cancel_all(self): """ Cancel all orders of the worker's account """ if self.orders: + self.log.info('Canceling all orders') return self.bitshares.cancel( [o["id"] for o in self.orders], account=self.account diff --git a/dexbot/cli.py b/dexbot/cli.py index 023972aae..bae5b2be2 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 import logging import os -import click import signal import sys + from .ui import ( verbose, chain, unlock, - configfile, - confirmwarning, - confirmalert, - warning, - alert, + configfile ) from dexbot.worker import WorkerInfrastructure import dexbot.errors as errors +import click + log = logging.getLogger(__name__) logging.basicConfig( diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index a9c70d5e8..36255eb44 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -49,6 +49,7 @@ def error(self, *args, **kwargs): self.log.info(self.execute()) def update_orders(self): + self.log.info('Change detected, updating orders') amount = self.target['amount'] # Recalculate buy and sell order prices @@ -56,7 +57,6 @@ def update_orders(self): # Cancel the orders before redoing them self.cancel_all() - self.log.info('An order was filled, canceling the orders') order_ids = [] @@ -74,7 +74,9 @@ def update_orders(self): returnOrderId="head" ) buy_order = self.get_order(buy_transaction['orderid']) - self.log.info('Placed a buy order for {} {} @ {}'.format(amount, self.market["quote"], self.buy_price)) + self.log.info('Placed a buy order for {} {} @ {}'.format(amount, + self.market["quote"]['symbol'], + self.buy_price)) if buy_order: self['buy_order'] = buy_order order_ids.append(buy_transaction['orderid']) @@ -93,7 +95,9 @@ def update_orders(self): returnOrderId="head" ) sell_order = self.get_order(sell_transaction['orderid']) - self.log.info('Placed a sell order for {} {} @ {}'.format(amount, self.market["quote"], self.buy_price)) + self.log.info('Placed a sell order for {} {} @ {}'.format(amount, + self.market["quote"]['symbol'], + self.sell_price)) if sell_order: self['sell_order'] = sell_order order_ids.append(sell_transaction['orderid']) From 81685e2f4eb504fde0a11f45ded0b420ff67ab9e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 6 Apr 2018 09:11:19 +0300 Subject: [PATCH 065/187] Add file logging to the cli Also do some code cleaning --- dexbot/ui.py | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/dexbot/ui.py b/dexbot/ui.py index 5654683fa..c241f72a4 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -1,10 +1,12 @@ import os -import click import logging -from ruamel import yaml from functools import update_wrapper + +import click +from ruamel import yaml from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance + log = logging.getLogger(__name__) @@ -14,25 +16,35 @@ def new_func(ctx, *args, **kwargs): verbosity = [ "critical", "error", "warn", "info", "debug" ][int(min(ctx.obj.get("verbose", 0), 4))] - if ctx.obj.get("systemd",False): - # dont print the timestamps: systemd will log it for us + if ctx.obj.get("systemd", False): + # Don't print the timestamps: systemd will log it for us formatter1 = logging.Formatter('%(name)s - %(levelname)s - %(message)s') - formatter2 = logging.Formatter('Worker %(worker_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') + formatter2 = logging.Formatter( + '%(worker_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') elif verbosity == "debug": - # when debugging log where the log call came from + # When debugging: log where the log call came from formatter1 = logging.Formatter('%(asctime)s (%(module)s:%(lineno)d) - %(levelname)s - %(message)s') - formatter2 = logging.Formatter('%(asctime)s (%(module)s:%(lineno)d) - worker %(worker_name)s - %(levelname)s - %(message)s') + formatter2 = logging.Formatter( + '%(asctime)s (%(module)s:%(lineno)d) - %(worker_name)s - %(levelname)s - %(message)s') else: formatter1 = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - formatter2 = logging.Formatter('%(asctime)s - worker %(worker_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') + formatter2 = logging.Formatter( + '%(asctime)s - %(worker_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') - # use special format for special workers logger + # Use special format for special workers logger + logger = logging.getLogger("dexbot.per_worker") ch = logging.StreamHandler() ch.setLevel(getattr(logging, verbosity.upper())) ch.setFormatter(formatter2) - logging.getLogger("dexbot.per_worker").addHandler(ch) - logging.getLogger("dexbot.per_worker").propagate = False # don't double up with root logger - # set the root logger with basic format + logger.addHandler(ch) + + # Logging to a file + fh = logging.FileHandler('dexbot.log') + fh.setFormatter(formatter2) + logger.addHandler(fh) + + logger.propagate = False # Don't double up with root logger + # Set the root logger with basic format ch = logging.StreamHandler() ch.setLevel(getattr(logging, verbosity.upper())) ch.setFormatter(formatter1) @@ -44,17 +56,17 @@ def new_func(ctx, *args, **kwargs): verbosity = [ "critical", "error", "warn", "info", "debug" ][int(min(ctx.obj.get("verbose", 4) - 4, 4))] - log = logging.getLogger("grapheneapi") - log.setLevel(getattr(logging, verbosity.upper())) - log.addHandler(ch) + logger = logging.getLogger("grapheneapi") + logger.setLevel(getattr(logging, verbosity.upper())) + logger.addHandler(ch) if ctx.obj["verbose"] > 8: verbosity = [ "critical", "error", "warn", "info", "debug" ][int(min(ctx.obj.get("verbose", 8) - 8, 4))] - log = logging.getLogger("graphenebase") - log.setLevel(getattr(logging, verbosity.upper())) - log.addHandler(ch) + logger = logging.getLogger("graphenebase") + logger.setLevel(getattr(logging, verbosity.upper())) + logger.addHandler(ch) return ctx.invoke(f, *args, **kwargs) return update_wrapper(new_func, f) From 60db3cdb3f5a49a33516e9c109e052d9d192f294 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 6 Apr 2018 09:11:32 +0300 Subject: [PATCH 066/187] Add file logging to the gui --- dexbot/controllers/main_controller.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 638d84f44..e7a4e5e98 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,3 +1,5 @@ +import logging + from dexbot.worker import WorkerInfrastructure from ruamel.yaml import YAML @@ -11,6 +13,15 @@ def __init__(self, bitshares_instance): set_shared_bitshares_instance(bitshares_instance) self.worker_manager = None + # Configure logging + formatter = logging.Formatter( + '%(asctime)s - %(worker_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') + logger = logging.getLogger("dexbot.per_worker") + fh = logging.FileHandler('dexbot.log') + fh.setFormatter(formatter) + logger.addHandler(fh) + logger.setLevel(logging.INFO) + def create_worker(self, worker_name, config, view): # Todo: Add some threading here so that the GUI doesn't freeze if self.worker_manager and self.worker_manager.is_alive(): From 4b16b7458ce9138d2fa865617343b3ee0361b6e6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 6 Apr 2018 09:50:20 +0300 Subject: [PATCH 067/187] Change dexbot version --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 124e46203..c3bb2961b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1 +1 @@ -__version__ = '0.1.7' +__version__ = '0.1.8' From 49ecd4078ad50f63db03148e8e10a8486828144d Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 6 Apr 2018 13:46:00 +0300 Subject: [PATCH 068/187] Moved installation instructions to the Wiki and removed wrong information regarding the cli interface --- README.md | 37 ++++--------------------------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 823fe4905..bb640f4ed 100644 --- a/README.md +++ b/README.md @@ -10,43 +10,14 @@ master: **Warning**: This is highly experimental code! Use at your OWN risk! -## Installation +## Installing and running the software -Python 3.4+ & pip are required. With make: +See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki/Installing-and-Running) - $ git clone https://github.com/codaone/dexbot - $ cd dexbot - $ make install +## Contributing -or +Install the software, use it and report any problems by creating a ticket. - $ make install-user - -Without make: - - $ git clone https://github.com/codaone/dexbot - $ cd dexbot - $ pip install -r requirements.txt - $ python setup.py install - -or - - $ pip install -r --user requirements.txt - $ python setup.py install --user - -## Configuration - -Configuration happens in `config.yml` - -## Requirements - -Add your account's private key to the pybitshares wallet using `uptick` - - uptick addkey - -## Execution - - dexbot run # IMPORTANT NOTE From e258820d50ceb242d44917a5d55b31f142fc0aac Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 6 Apr 2018 15:11:31 +0300 Subject: [PATCH 069/187] Fix gui crash on specific systems --- app.spec | 2 ++ cli.spec | 4 ++-- dexbot/__init__.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app.spec b/app.spec index bd7a15f45..192927f3e 100644 --- a/app.spec +++ b/app.spec @@ -31,6 +31,8 @@ a = Analysis(['app.py'], pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) +a.binaries = [b for b in a.binaries if "libdrm.so.2" not in b[0]] + exe = EXE(pyz, a.scripts, a.binaries, diff --git a/cli.spec b/cli.spec index 70b92d4b2..532f3b9b2 100644 --- a/cli.spec +++ b/cli.spec @@ -4,7 +4,6 @@ import os import sys block_cipher = None - hiddenimports_strategies = [ 'dexbot', 'dexbot.strategies', @@ -28,8 +27,9 @@ a = Analysis(['cli.py'], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher) + pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) + cipher=block_cipher) exe = EXE(pyz, a.scripts, diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c3bb2961b..1c98a23a8 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1 +1 @@ -__version__ = '0.1.8' +__version__ = '0.1.9' From 4d90e7ed2d7a3497dd676c1db10e57eef987d228 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 9 Apr 2018 12:48:45 +0300 Subject: [PATCH 070/187] Change windows binary file python version to 3.5.3 This fixes an issue with websockets on windows --- appveyor.yml | 8 +++----- dexbot/__init__.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 178fca97c..7d04aabba 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,10 +4,8 @@ image: Visual Studio 2015 environment: matrix: - # Python 3.6 - 32-bit - #- PYTHON: "C:\\Python36" - # Python 3.6 - 64-bit - - PYTHON: "C:\\Python36-x64" + # Python 3.5.3 - 64-bit + - PYTHON: "C:\\Python35-x64" #---------------------------------# # build # @@ -20,7 +18,7 @@ configuration: Release install: - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%;C:\MinGW\bin - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe - - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe + - copy c:\Python35-x64\python.exe c:\Python35-x64\python3.exe - python --version - make install diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 1c98a23a8..850505a32 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1 +1 @@ -__version__ = '0.1.9' +__version__ = '0.1.10' From 9325a0033ff96d4519cac6847a302bb29ecd9c55 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 11 Apr 2018 10:37:03 +0300 Subject: [PATCH 071/187] Change slider logic in relative orders The slider logic will now take the latest price into the calculation --- dexbot/strategies/relative_orders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 36255eb44..a595b3390 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -137,8 +137,10 @@ def update_gui_profit(self): self['profit'] = profit def update_gui_slider(self): + ticker = self.market.ticker() + latest_price = ticker.get('latest').get('price') total_balance = self.total_balance(self['order_ids']) - total = total_balance['quote'] + total_balance['base'] + total = (total_balance['quote'] * latest_price) + total_balance['base'] if not total: # Prevent division by zero percentage = 50 From 332b0ed8f6b02a04cc1b2535cf7738fa8e3b7d3a Mon Sep 17 00:00:00 2001 From: Nikolay Date: Wed, 11 Apr 2018 12:52:46 +0300 Subject: [PATCH 072/187] Add relative orders size logic --- .../controllers/create_worker_controller.py | 4 ++ dexbot/strategies/relative_orders.py | 52 +++++++++++++++---- dexbot/views/create_worker.py | 25 ++++++++- dexbot/views/edit_worker.py | 45 +++++++++++++++- dexbot/views/ui/create_worker_window.ui | 21 +++++--- dexbot/views/ui/edit_worker_window.ui | 25 +++++---- 6 files changed, 142 insertions(+), 30 deletions(-) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 6e556a216..d894c18a4 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -127,6 +127,10 @@ def get_account(worker_data): def get_target_amount(worker_data): return worker_data['target']['amount'] + @staticmethod + def get_target_amount_relative(worker_data): + return worker_data['target']['amount_relative'] + @staticmethod def get_target_center_price(worker_data): return worker_data['target']['center_price'] diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 36255eb44..afdccdad2 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,6 +1,5 @@ from dexbot.basestrategy import BaseStrategy from dexbot.queue.idle_queue import idle_add - from bitshares.amount import Amount @@ -26,9 +25,12 @@ def __init__(self, *args, **kwargs): else: self.center_price = self.target["center_price"] + self.is_relative_order_size = self.target['amount_relative'] + self.order_size = float(self.target['amount']) + self.buy_price = None self.sell_price = None - self.calculate_order_prices() + # self.calculate_order_prices() self.initial_balance = self['initial_balance'] or 0 self.worker_name = kwargs.get('name') @@ -36,6 +38,32 @@ def __init__(self, *args, **kwargs): self.check_orders() + @property + def amount_quote(self): + """"" + Get quote amount, calculate if order size is relative + """"" + if self.is_relative_order_size: + balance = float(self.balance(self.market["quote"])) + # return balance * (self.order_size / 2 / 100) + # amount = % of balance / sell_price = amount combined with calculated price to give % of balance + return ((balance / 100) * self.order_size) / self.sell_price + else: + return self.order_size + + @property + def amount_base(self): + """"" + Get base amount, calculate if order size is relative + """"" + if self.is_relative_order_size: + balance = float(self.balance(self.market["base"])) + # return balance * (self.order_size / 2 / 100) + # amount = % of balance / buy_price = amount combined with calculated price to give % of balance + return ((balance / 100) * self.order_size) / self.buy_price + else: + return self.order_size + def calculate_order_prices(self): if self.is_center_price_dynamic: self.center_price = self.calculate_center_price @@ -50,7 +78,6 @@ def error(self, *args, **kwargs): def update_orders(self): self.log.info('Change detected, updating orders') - amount = self.target['amount'] # Recalculate buy and sell order prices self.calculate_order_prices() @@ -60,21 +87,24 @@ def update_orders(self): order_ids = [] + amount_base = self.amount_base + amount_quote = self.amount_quote + # Buy Side - if float(self.balance(self.market["base"])) < self.buy_price * amount: + if float(self.balance(self.market["base"])) < self.buy_price * amount_base: self.log.critical( - 'Insufficient buy balance, needed {} {}'.format(self.buy_price * amount, self.market['base']['symbol']) + 'Insufficient buy balance, needed {} {}'.format(self.buy_price * amount_base, self.market['base']['symbol']) ) self.disabled = True else: buy_transaction = self.market.buy( self.buy_price, - Amount(amount=amount, asset=self.market["quote"]), + Amount(amount=amount_base, asset=self.market["quote"]), account=self.account, returnOrderId="head" ) buy_order = self.get_order(buy_transaction['orderid']) - self.log.info('Placed a buy order for {} {} @ {}'.format(amount, + self.log.info('Placed a buy order for {} {} @ {}'.format(amount_quote, self.market["quote"]['symbol'], self.buy_price)) if buy_order: @@ -82,20 +112,20 @@ def update_orders(self): order_ids.append(buy_transaction['orderid']) # Sell Side - if float(self.balance(self.market["quote"])) < amount: + if float(self.balance(self.market["quote"])) < amount_quote: self.log.critical( - "Insufficient sell balance, needed {} {}".format(amount, self.market['quote']['symbol']) + "Insufficient sell balance, needed {} {}".format(amount_quote, self.market['quote']['symbol']) ) self.disabled = True else: sell_transaction = self.market.sell( self.sell_price, - Amount(amount=amount, asset=self.market["quote"]), + Amount(amount=amount_quote, asset=self.market["quote"]), account=self.account, returnOrderId="head" ) sell_order = self.get_order(sell_transaction['orderid']) - self.log.info('Placed a sell order for {} {} @ {}'.format(amount, + self.log.info('Placed a sell order for {} {} @ {}'.format(amount_quote, self.market["quote"]['symbol'], self.sell_price)) if sell_order: diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 4ac723965..2df1365ae 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -23,8 +23,23 @@ def __init__(self, controller): self.ui.save_button.clicked.connect(self.handle_save) self.ui.cancel_button.clicked.connect(self.reject) self.ui.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) + self.ui.relative_order_size_checkbox.stateChanged.connect(self.onchange_relative_order_size_checkbox) self.worker_data = {} + def onchange_relative_order_size_checkbox(self): + checkbox = self.ui.relative_order_size_checkbox + if checkbox.isChecked(): + self.ui.amount_input.setSuffix('%') + self.ui.amount_input.setDecimals(2) + self.ui.amount_input.setMaximum(100.00) + self.ui.amount_input.setValue(10.00) + self.ui.amount_input.setMinimumWidth(151) + else: + self.ui.amount_input.setSuffix('') + self.ui.amount_input.setDecimals(8) + self.ui.amount_input.setMaximum(1000000000.000000) + self.ui.amount_input.setValue(0.000000) + def onchange_center_price_dynamic_checkbox(self): checkbox = self.ui.center_price_dynamic_checkbox if checkbox.isChecked(): @@ -96,8 +111,16 @@ def handle_save(self): ui = self.ui spread = float(ui.spread_input.text()[:-1]) # Remove the percentage character from the end + + # If order size is relative, remove percentage character in the end + if ui.relative_order_size_checkbox.isChecked(): + amount = float(ui.amount_input.text()[:-1]) + else: + amount = ui.amount_input.text() + target = { - 'amount': float(ui.amount_input.text()), + 'amount': amount, + 'amount_relative': bool(ui.relative_order_size_checkbox.isChecked()), 'center_price': float(ui.center_price_input.text()), 'center_price_dynamic': bool(ui.center_price_dynamic_checkbox.isChecked()), 'spread': spread diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 480c4c72b..8c504beb9 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -20,7 +20,16 @@ def __init__(self, controller, worker_name, config): self.base_asset_input.addItems(self.controller.base_assets) self.quote_asset_input.setText(self.controller.get_quote_asset(worker_data)) self.account_name.setText(self.controller.get_account(worker_data)) - self.amount_input.setValue(self.controller.get_target_amount(worker_data)) + + if self.controller.get_target_amount_relative(worker_data): + self.order_size_input_to_relative() + self.relative_order_size_checkbox.setChecked(True) + else: + self.order_size_input_to_static() + self.relative_order_size_checkbox.setChecked(False) + + self.amount_input.setValue(float(self.controller.get_target_amount(worker_data))) + self.center_price_input.setValue(self.controller.get_target_center_price(worker_data)) center_price_dynamic = self.controller.get_target_center_price_dynamic(worker_data) @@ -35,8 +44,32 @@ def __init__(self, controller, worker_name, config): self.save_button.clicked.connect(self.handle_save) self.cancel_button.clicked.connect(self.reject) self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) + self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) + self.relative_order_size_checkbox.stateChanged.connect(self.onchange_relative_order_size_checkbox) self.worker_data = {} + def order_size_input_to_relative(self): + input_field = self.amount_input + input_field.setSuffix('%') + input_field.setDecimals(2) + input_field.setMaximum(100.00) + input_field.setMinimumWidth(151) + + def order_size_input_to_static(self): + input_field = self.amount_input + input_field.setSuffix('') + input_field.setDecimals(8) + input_field.setMaximum(1000000000.000000) + input_field.setMinimumWidth(151) + + def onchange_relative_order_size_checkbox(self): + if self.relative_order_size_checkbox.isChecked(): + self.order_size_input_to_relative() + self.amount_input.setValue(10.00) + else: + self.order_size_input_to_static() + self.amount_input.setValue(0.000000) + def onchange_center_price_dynamic_checkbox(self): checkbox = self.center_price_dynamic_checkbox if checkbox.isChecked(): @@ -96,8 +129,16 @@ def handle_save(self): return spread = float(self.spread_input.text()[:-1]) # Remove the percentage character from the end + + # If order size is relative, remove percentage character in the end + if self.relative_order_size_checkbox.isChecked(): + amount = float(self.amount_input.text()[:-1]) + else: + amount = self.amount_input.text() + target = { - 'amount': float(self.amount_input.text()), + 'amount': amount, + 'amount_relative': bool(self.relative_order_size_checkbox.isChecked()), 'center_price': float(self.center_price_input.text()), 'center_price_dynamic': bool(self.center_price_dynamic_checkbox.isChecked()), 'spread': spread diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index aba78c846..b19e4458d 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -7,7 +7,7 @@ 0 0 418 - 507 + 529 @@ -235,11 +235,11 @@ 8 - 999999999.998999953269958 + 1000000000.000000000000000 - + @@ -267,7 +267,7 @@ - + false @@ -304,7 +304,7 @@ - + @@ -332,7 +332,7 @@ - + @@ -360,7 +360,7 @@ - + Calculate center price dynamically @@ -370,6 +370,13 @@ + + + + Relative order size + + + diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index fd4b61ea6..354ce3da0 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -7,7 +7,7 @@ 0 0 400 - 459 + 486 @@ -252,11 +252,11 @@ 8 - 999999999.998999953269958 + 1000000000.000000000000000 - + @@ -284,7 +284,7 @@ - + false @@ -321,7 +321,14 @@ - + + + + Calculate center price dynamically + + + + @@ -349,7 +356,7 @@ - + @@ -377,10 +384,10 @@ - - + + - Calculate center price dynamically + Relative order size From 9b5e82ede86ade02f36256ba5d7ce9fdb18e1b77 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Wed, 11 Apr 2018 13:02:09 +0300 Subject: [PATCH 073/187] Remove unnecessary comments --- dexbot/strategies/relative_orders.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index afdccdad2..387664dc9 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -30,7 +30,6 @@ def __init__(self, *args, **kwargs): self.buy_price = None self.sell_price = None - # self.calculate_order_prices() self.initial_balance = self['initial_balance'] or 0 self.worker_name = kwargs.get('name') @@ -45,7 +44,6 @@ def amount_quote(self): """"" if self.is_relative_order_size: balance = float(self.balance(self.market["quote"])) - # return balance * (self.order_size / 2 / 100) # amount = % of balance / sell_price = amount combined with calculated price to give % of balance return ((balance / 100) * self.order_size) / self.sell_price else: @@ -58,7 +56,6 @@ def amount_base(self): """"" if self.is_relative_order_size: balance = float(self.balance(self.market["base"])) - # return balance * (self.order_size / 2 / 100) # amount = % of balance / buy_price = amount combined with calculated price to give % of balance return ((balance / 100) * self.order_size) / self.buy_price else: From 708d8edfee32ef9d3cac15552ee7671890b1ef2c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 12 Apr 2018 10:18:18 +0300 Subject: [PATCH 074/187] Fix relative order size calculation for quote asset Also bumb the version --- dexbot/__init__.py | 2 +- dexbot/strategies/relative_orders.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 850505a32..e6d0c4f45 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1 +1 @@ -__version__ = '0.1.10' +__version__ = '0.1.12' diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 387664dc9..f1e5dd441 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -43,9 +43,8 @@ def amount_quote(self): Get quote amount, calculate if order size is relative """"" if self.is_relative_order_size: - balance = float(self.balance(self.market["quote"])) - # amount = % of balance / sell_price = amount combined with calculated price to give % of balance - return ((balance / 100) * self.order_size) / self.sell_price + quote_balance = float(self.balance(self.market["quote"])) + return quote_balance * (self.order_size / 100) else: return self.order_size @@ -55,9 +54,9 @@ def amount_base(self): Get base amount, calculate if order size is relative """"" if self.is_relative_order_size: - balance = float(self.balance(self.market["base"])) + base_balance = float(self.balance(self.market["base"])) # amount = % of balance / buy_price = amount combined with calculated price to give % of balance - return ((balance / 100) * self.order_size) / self.buy_price + return base_balance * (self.order_size / 100) / self.buy_price else: return self.order_size @@ -90,7 +89,8 @@ def update_orders(self): # Buy Side if float(self.balance(self.market["base"])) < self.buy_price * amount_base: self.log.critical( - 'Insufficient buy balance, needed {} {}'.format(self.buy_price * amount_base, self.market['base']['symbol']) + 'Insufficient buy balance, needed {} {}'.format(self.buy_price * amount_base, + self.market['base']['symbol']) ) self.disabled = True else: @@ -101,8 +101,8 @@ def update_orders(self): returnOrderId="head" ) buy_order = self.get_order(buy_transaction['orderid']) - self.log.info('Placed a buy order for {} {} @ {}'.format(amount_quote, - self.market["quote"]['symbol'], + self.log.info('Placed a buy order for {} {} @ {}'.format(self.buy_price * amount_base, + self.market["base"]['symbol'], self.buy_price)) if buy_order: self['buy_order'] = buy_order From 6fbd7c21efaeaad028493b21ed5af2143e247b57 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 12 Apr 2018 10:59:32 +0300 Subject: [PATCH 075/187] Change relative orders config structure Flattened the target key for simpler structure --- .../controllers/create_worker_controller.py | 20 ++++++++-------- dexbot/strategies/relative_orders.py | 13 +++++----- dexbot/views/create_worker.py | 14 ++++------- dexbot/views/edit_worker.py | 24 ++++++++----------- 4 files changed, 31 insertions(+), 40 deletions(-) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index d894c18a4..28ae5cbf7 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -124,21 +124,21 @@ def get_account(worker_data): return worker_data['account'] @staticmethod - def get_target_amount(worker_data): - return worker_data['target']['amount'] + def get_amount(worker_data): + return worker_data.get('amount', 0) @staticmethod - def get_target_amount_relative(worker_data): - return worker_data['target']['amount_relative'] + def get_amount_relative(worker_data): + return worker_data.get('amount_relative', False) @staticmethod - def get_target_center_price(worker_data): - return worker_data['target']['center_price'] + def get_center_price(worker_data): + return worker_data.get('center_price', 0) @staticmethod - def get_target_center_price_dynamic(worker_data): - return worker_data['target']['center_price_dynamic'] + def get_center_price_dynamic(worker_data): + return worker_data.get('center_price_dynamic', True) @staticmethod - def get_target_spread(worker_data): - return worker_data['target']['spread'] + def get_spread(worker_data): + return worker_data.get('spread', 5) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index f1e5dd441..85786e05d 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -18,15 +18,14 @@ def __init__(self, *args, **kwargs): self.error_onMarketUpdate = self.error self.error_onAccount = self.error - self.target = self.worker.get("target", {}) - self.is_center_price_dynamic = self.target["center_price_dynamic"] + self.is_center_price_dynamic = self.worker["center_price_dynamic"] if self.is_center_price_dynamic: self.center_price = None else: - self.center_price = self.target["center_price"] + self.center_price = self.worker["center_price"] - self.is_relative_order_size = self.target['amount_relative'] - self.order_size = float(self.target['amount']) + self.is_relative_order_size = self.worker['amount_relative'] + self.order_size = float(self.worker['amount']) self.buy_price = None self.sell_price = None @@ -64,8 +63,8 @@ def calculate_order_prices(self): if self.is_center_price_dynamic: self.center_price = self.calculate_center_price - self.buy_price = self.center_price * (1 - (self.target["spread"] / 2) / 100) - self.sell_price = self.center_price * (1 + (self.target["spread"] / 2) / 100) + self.buy_price = self.center_price * (1 - (self.worker["spread"] / 2) / 100) + self.sell_price = self.center_price * (1 + (self.worker["spread"] / 2) / 100) def error(self, *args, **kwargs): self.cancel_all() diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 2df1365ae..bca589b0b 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -118,14 +118,6 @@ def handle_save(self): else: amount = ui.amount_input.text() - target = { - 'amount': amount, - 'amount_relative': bool(ui.relative_order_size_checkbox.isChecked()), - 'center_price': float(ui.center_price_input.text()), - 'center_price_dynamic': bool(ui.center_price_dynamic_checkbox.isChecked()), - 'spread': spread - } - base_asset = ui.base_asset_input.currentText() quote_asset = ui.quote_asset_input.text() strategy = ui.strategy_input.currentText() @@ -135,7 +127,11 @@ def handle_save(self): 'market': '{}/{}'.format(quote_asset, base_asset), 'module': worker_module, 'strategy': strategy, - 'target': target + 'amount': amount, + 'amount_relative': bool(ui.relative_order_size_checkbox.isChecked()), + 'center_price': float(ui.center_price_input.text()), + 'center_price_dynamic': bool(ui.center_price_dynamic_checkbox.isChecked()), + 'spread': spread } self.worker_name = ui.worker_name_input.text() self.accept() diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 8c504beb9..533db183b 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -21,18 +21,18 @@ def __init__(self, controller, worker_name, config): self.quote_asset_input.setText(self.controller.get_quote_asset(worker_data)) self.account_name.setText(self.controller.get_account(worker_data)) - if self.controller.get_target_amount_relative(worker_data): + if self.controller.get_amount_relative(worker_data): self.order_size_input_to_relative() self.relative_order_size_checkbox.setChecked(True) else: self.order_size_input_to_static() self.relative_order_size_checkbox.setChecked(False) - self.amount_input.setValue(float(self.controller.get_target_amount(worker_data))) + self.amount_input.setValue(float(self.controller.get_amount(worker_data))) - self.center_price_input.setValue(self.controller.get_target_center_price(worker_data)) + self.center_price_input.setValue(self.controller.get_center_price(worker_data)) - center_price_dynamic = self.controller.get_target_center_price_dynamic(worker_data) + center_price_dynamic = self.controller.get_center_price_dynamic(worker_data) if center_price_dynamic: self.center_price_input.setEnabled(False) self.center_price_dynamic_checkbox.setChecked(True) @@ -40,7 +40,7 @@ def __init__(self, controller, worker_name, config): self.center_price_input.setEnabled(True) self.center_price_dynamic_checkbox.setChecked(False) - self.spread_input.setValue(self.controller.get_target_spread(worker_data)) + self.spread_input.setValue(self.controller.get_spread(worker_data)) self.save_button.clicked.connect(self.handle_save) self.cancel_button.clicked.connect(self.reject) self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) @@ -136,14 +136,6 @@ def handle_save(self): else: amount = self.amount_input.text() - target = { - 'amount': amount, - 'amount_relative': bool(self.relative_order_size_checkbox.isChecked()), - 'center_price': float(self.center_price_input.text()), - 'center_price_dynamic': bool(self.center_price_dynamic_checkbox.isChecked()), - 'spread': spread - } - base_asset = self.base_asset_input.currentText() quote_asset = self.quote_asset_input.text() strategy = self.strategy_input.currentText() @@ -153,7 +145,11 @@ def handle_save(self): 'market': '{}/{}'.format(quote_asset, base_asset), 'module': worker_module, 'strategy': strategy, - 'target': target + 'amount': amount, + 'amount_relative': bool(self.relative_order_size_checkbox.isChecked()), + 'center_price': float(self.center_price_input.text()), + 'center_price_dynamic': bool(self.center_price_dynamic_checkbox.isChecked()), + 'spread': spread } self.worker_name = self.worker_name_input.text() self.accept() From c3f8899e071c117c4631c6acb4a30ecf3fe7abc4 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 12 Apr 2018 17:08:00 +0200 Subject: [PATCH 076/187] - Build GUI via setuptools' build instead of setuptools' install - Autodiscover packages via find_packages() - Updated license --- LICENSE.txt | 2 +- MANIFEST.in | 4 ++++ Makefile | 9 ++++++--- dexbot/queue/__init__.py | 0 setup.py | 20 +++++++------------- 5 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 dexbot/queue/__init__.py diff --git a/LICENSE.txt b/LICENSE.txt index 0e4f4b8da..671e4a6dd 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 ChainSquad GmbH +Copyright (c) 2018 Codaone Oy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index efa752eaf..5e0ad39d9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,5 @@ include *.md +include app.py +include cli.py +include config.yml +include dexbot/resources/img/* \ No newline at end of file diff --git a/Makefile b/Makefile index 3a9277531..a35342652 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ .PHONY: clean-pyc clean-build docs -clean: clean-build clean-pyc +clean: clean-build clean-pyc clean-ui + +clean-ui: + find dexbot/views/ui/*.py ! -name '__init__.py' -type f -exec rm -f {} + clean-build: rm -fr build/ @@ -18,7 +21,7 @@ pip: lint: flake8 dexbot/ -build: pip +build: clean pip python3 setup.py build install: build @@ -38,7 +41,7 @@ package: build pyinstaller app.spec pyinstaller cli.spec -dist: pip +dist: build python3 setup.py sdist upload -r pypi python3 setup.py bdist_wheel upload diff --git a/dexbot/queue/__init__.py b/dexbot/queue/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/setup.py b/setup.py index 47ac1e7c6..5b31ddb19 100755 --- a/setup.py +++ b/setup.py @@ -1,9 +1,8 @@ #!/usr/bin/env python -from setuptools import setup -from setuptools.command.install import install +from setuptools import setup, find_packages +from distutils.command import build as build_module from distutils.util import convert_path - from pyqt_distutils.build_ui import build_ui main_ns = {} @@ -13,13 +12,10 @@ VERSION = main_ns['__version__'] -class InstallCommand(install): - """Customized setuptools install command - converts .ui and .qrc files to .py files - """ +class BuildCommand(build_module.build): def run(self): - # Workaround for https://github.com/pypa/setuptools/issues/456 - self.do_egg_install() self.run_command('build_ui') + build_module.build.run(self) setup( @@ -33,10 +29,7 @@ def run(self): maintainer_email='support@codaone.com', url='http://www.github.com/codaone/dexbot', keywords=['DEX', 'bot', 'trading', 'api', 'blockchain'], - packages=[ - "dexbot", - "dexbot.strategies", - ], + packages=find_packages(), classifiers=[ 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', @@ -46,7 +39,7 @@ def run(self): ], cmdclass={ 'build_ui': build_ui, - 'install': InstallCommand, + 'build': BuildCommand }, entry_points={ 'console_scripts': [ @@ -63,3 +56,4 @@ def run(self): ], include_package_data=True, ) + From 871137e2f9a276cd040a2385c6c6c79177f636c6 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 12 Apr 2018 18:30:28 +0200 Subject: [PATCH 077/187] - Moved cli & gui modules inside dexbot package - created separate console entrypoints for cli & gui dexbot versions: dexbot-cli & dexbot-gui - Create default config.yml file initially at the user's default config directory and load it from here everywhere. - Fix for #57 : config.yml is created automatically in case it does not exist --- .travis.yml | 2 +- Makefile | 2 +- cli.py | 5 ----- cli.spec | 2 +- dexbot/__init__.py | 25 ++++++++++++++++++++++++- dexbot/cli.py | 5 +++-- dexbot/controllers/main_controller.py | 19 ++++++++++--------- app.py => dexbot/gui.py | 6 +++++- app.spec => gui.spec | 2 +- setup.py | 14 +++++--------- 10 files changed, 51 insertions(+), 31 deletions(-) delete mode 100755 cli.py rename app.py => dexbot/gui.py (97%) rename app.spec => gui.spec (97%) diff --git a/.travis.yml b/.travis.yml index 1f11fe239..59d77a3dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: - make install script: - echo "@TODO - Running tests..." - - pyinstaller --distpath dist/$TRAVIS_OS_NAME app.spec + - pyinstaller --distpath dist/$TRAVIS_OS_NAME gui.spec - pyinstaller --distpath dist/$TRAVIS_OS_NAME cli.spec before_deploy: - git config --local user.name "Travis" diff --git a/Makefile b/Makefile index a35342652..f1194c06a 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ check: pip python3 setup.py check package: build - pyinstaller app.spec + pyinstaller gui.spec pyinstaller cli.spec dist: build diff --git a/cli.py b/cli.py deleted file mode 100755 index 9ee2f6f34..000000000 --- a/cli.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 - -from dexbot import cli - -cli.main() diff --git a/cli.spec b/cli.spec index 532f3b9b2..2b482460c 100644 --- a/cli.spec +++ b/cli.spec @@ -17,7 +17,7 @@ hiddenimports_packaging = [ 'packaging', 'packaging.version', 'packaging.specifiers', 'packaging.requirements' ] -a = Analysis(['cli.py'], +a = Analysis(['dexbot/cli.py'], binaries=[], datas=[], hiddenimports=hiddenimports_packaging + hiddenimports_strategies, diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e6d0c4f45..866c2b4fa 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1 +1,24 @@ -__version__ = '0.1.12' +import pathlib +import os +from appdirs import user_config_dir + +APP_NAME = "dexbot" +VERSION = '0.1.12' +AUTHOR = "codaone" +__version__ = VERSION + + +config_dir = user_config_dir(APP_NAME, appauthor=AUTHOR) +config_file = config_dir + "/config.yml" + +default_config = """ +node: wss://bitshares.openledger.info/ws +workers: {} +""" + +if not os.path.isfile(config_file): + pathlib.Path(config_dir).mkdir(parents=True, exist_ok=True) + with open(config_file, 'w') as f: + f.write(default_config) + print("Created default config file at {}".format(config_file)) + diff --git a/dexbot/cli.py b/dexbot/cli.py index bae5b2be2..7010c12b9 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -4,7 +4,8 @@ import signal import sys -from .ui import ( +from dexbot import config_file +from dexbot.ui import ( verbose, chain, unlock, @@ -26,7 +27,7 @@ @click.group() @click.option( "--configfile", - default="config.yml", + default=config_file, ) @click.option( '--verbose', diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index e7a4e5e98..a140526da 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,5 +1,6 @@ import logging +from dexbot import config_file from dexbot.worker import WorkerInfrastructure from ruamel.yaml import YAML @@ -53,7 +54,7 @@ def remove_worker(self, worker_name): @staticmethod def load_config(): yaml = YAML() - with open('config.yml', 'r') as f: + with open(config_file, 'r') as f: return yaml.load(f) @staticmethod @@ -61,7 +62,7 @@ def get_workers_data(): """ Returns dict of all the workers data """ - with open('config.yml', 'r') as f: + with open(config_file, 'r') as f: yaml = YAML() return yaml.load(f)['workers'] @@ -70,7 +71,7 @@ def get_worker_config(worker_name): """ Returns config file data with only the data from a specific worker """ - with open('config.yml', 'r') as f: + with open(config_file, 'r') as f: yaml = YAML() config = yaml.load(f) config['workers'] = {worker_name: config['workers'][worker_name]} @@ -79,29 +80,29 @@ def get_worker_config(worker_name): @staticmethod def remove_worker_config(worker_name): yaml = YAML() - with open('config.yml', 'r') as f: + with open(config_file, 'r') as f: config = yaml.load(f) config['workers'].pop(worker_name, None) - with open("config.yml", "w") as f: + with open(config_file, "w") as f: yaml.dump(config, f) @staticmethod def add_worker_config(worker_name, worker_data): yaml = YAML() - with open('config.yml', 'r') as f: + with open(config_file, 'r') as f: config = yaml.load(f) config['workers'][worker_name] = worker_data - with open("config.yml", "w") as f: + with open(config_file, "w") as f: yaml.dump(config, f) @staticmethod def replace_worker_config(worker_name, new_worker_name, worker_data): yaml = YAML() - with open('config.yml', 'r') as f: + with open(config_file, 'r') as f: config = yaml.load(f) workers = config['workers'] @@ -113,5 +114,5 @@ def replace_worker_config(worker_name, new_worker_name, worker_data): else: workers[key] = value - with open("config.yml", "w") as f: + with open(config_file, "w") as f: yaml.dump(config, f) diff --git a/app.py b/dexbot/gui.py similarity index 97% rename from app.py rename to dexbot/gui.py index 45992f4b9..2f4de84a1 100644 --- a/app.py +++ b/dexbot/gui.py @@ -33,6 +33,10 @@ def __init__(self, sys_argv): sys.exit() -if __name__ == '__main__': +def main(): app = App(sys.argv) sys.exit(app.exec_()) + + +if __name__ == '__main__': + main() diff --git a/app.spec b/gui.spec similarity index 97% rename from app.spec rename to gui.spec index 192927f3e..8c2ad41c4 100644 --- a/app.spec +++ b/gui.spec @@ -17,7 +17,7 @@ hiddenimports_packaging = [ 'packaging', 'packaging.version', 'packaging.specifiers', 'packaging.requirements' ] -a = Analysis(['app.py'], +a = Analysis(['dexbot/gui.py'], binaries=[], datas=[], hiddenimports=hiddenimports_packaging + hiddenimports_strategies, diff --git a/setup.py b/setup.py index 5b31ddb19..4fd3fa6e1 100755 --- a/setup.py +++ b/setup.py @@ -1,15 +1,10 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from setuptools import setup, find_packages from distutils.command import build as build_module -from distutils.util import convert_path from pyqt_distutils.build_ui import build_ui -main_ns = {} -ver_path = convert_path('dexbot/__init__.py') -with open(ver_path) as ver_file: - exec(ver_file.read(), main_ns) - VERSION = main_ns['__version__'] +from dexbot import VERSION, APP_NAME class BuildCommand(build_module.build): @@ -19,7 +14,7 @@ def run(self): setup( - name='dexbot', + name=APP_NAME, version=VERSION, description='Trading bot for the DEX (BitShares)', long_description=open('README.md').read(), @@ -43,7 +38,8 @@ def run(self): }, entry_points={ 'console_scripts': [ - 'dexbot = dexbot.cli:main', + 'dexbot-cli = dexbot.cli:main', + 'dexbot-gui = dexbot.gui:main', ], }, install_requires=[ From 2a5e2de2b4918f066e57604641c10776ad9b51d1 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 12 Apr 2018 19:37:26 +0200 Subject: [PATCH 078/187] - pr feedback --- LICENSE.txt | 4 +++- MANIFEST.in | 3 --- Makefile | 3 ++- dexbot/__init__.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 671e4a6dd..8cdb77d71 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,8 @@ The MIT License (MIT) -Copyright (c) 2018 Codaone Oy +Copyright for portions of project DEXBot are held by ChainSquad GmbH 2017 +as part of project stakemachine. All other copyright for project DEXBot +are held by Codaone Oy 2018. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 5e0ad39d9..d0d04a220 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,2 @@ include *.md -include app.py -include cli.py -include config.yml include dexbot/resources/img/* \ No newline at end of file diff --git a/Makefile b/Makefile index f1194c06a..6db5fbe90 100644 --- a/Makefile +++ b/Makefile @@ -15,13 +15,14 @@ clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + + pip: python3 -m pip install -r requirements.txt lint: flake8 dexbot/ -build: clean pip +build: pip python3 setup.py build install: build diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 866c2b4fa..f69e948d9 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -9,7 +9,7 @@ config_dir = user_config_dir(APP_NAME, appauthor=AUTHOR) -config_file = config_dir + "/config.yml" +config_file = os.path.join(config_dir, "config.yml") default_config = """ node: wss://bitshares.openledger.info/ws From 302c1cffc93e909d6bb1b9d388809cf8a81907c3 Mon Sep 17 00:00:00 2001 From: sergio Date: Fri, 13 Apr 2018 11:32:57 +0200 Subject: [PATCH 079/187] - move appdirs into requirements.txt --- requirements.txt | 3 ++- setup.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index fc6d4f96a..ed0bde7db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ pyqt5==5.10 pyqt-distutils==0.7.3 click-datetime==0.2 -pyinstaller==3.3.1 \ No newline at end of file +pyinstaller==3.3.1 +appdirs==1.4.3 \ No newline at end of file diff --git a/setup.py b/setup.py index 4fd3fa6e1..3035637f4 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,6 @@ def run(self): "uptick>=0.1.4", "click", "sqlalchemy", - "appdirs", "ruamel.yaml>=0.15.37" ], include_package_data=True, From 31f4f4f496d36ab0376c34327438d6823573bd00 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 13 Apr 2018 13:31:49 +0300 Subject: [PATCH 080/187] Fix exception catch in cli.py --- dexbot/__init__.py | 2 +- dexbot/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index f69e948d9..01fbe94b5 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.12' +VERSION = '0.1.13' AUTHOR = "codaone" __version__ = VERSION diff --git a/dexbot/cli.py b/dexbot/cli.py index 7010c12b9..75dff3903 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -73,7 +73,7 @@ def run(ctx): signal.signal(signal.SIGHUP, kill_workers) # TODO: reload config on SIGUSR1 # signal.signal(signal.SIGUSR1, lambda x, y: worker.do_next_tick(worker.reread_config)) - except ValueError: + except AttributeError: log.debug("Cannot set all signals -- not available on this platform") worker.run() finally: From b913fb9eef726ee5392353e2a7078740580e67df Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 17 Apr 2018 14:40:47 +0300 Subject: [PATCH 081/187] Add cli.py and gui.py as shortcuts to their counterparts --- cli.py | 6 ++++++ gui.py | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100755 cli.py create mode 100755 gui.py diff --git a/cli.py b/cli.py new file mode 100755 index 000000000..8a9fa0fc2 --- /dev/null +++ b/cli.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from dexbot import cli + +if __name__ == '__main__': + cli.main() diff --git a/gui.py b/gui.py new file mode 100755 index 000000000..ff5b8d7a1 --- /dev/null +++ b/gui.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from dexbot import gui + +if __name__ == '__main__': + gui.main() From 1d4acc612d03d2690fe3d60cbaa9fa64dab601b2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 17 Apr 2018 14:49:42 +0300 Subject: [PATCH 082/187] Fix worker crashing on order cancel that don't exist This is a rather dirty solution but works for now. The solution is bad because the bot would need to wait potentially for tens of seconds to cancel all orders. --- dexbot/basestrategy.py | 40 +++++++++++++++++++--------- dexbot/strategies/relative_orders.py | 11 ++++---- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 8915fc88d..6d79b062f 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -1,12 +1,16 @@ import logging + +from .storage import Storage +from .statemachine import StateMachine + from events import Events +import bitsharesapi from bitshares.amount import Amount from bitshares.market import Market from bitshares.account import Account from bitshares.price import FilledOrder, Order, UpdateCallOrder from bitshares.instance import shared_bitshares_instance -from .storage import Storage -from .statemachine import StateMachine + class BaseStrategy(Storage, StateMachine, Events): @@ -246,25 +250,37 @@ def execute(self): self.bitshares.blocking = False return r + def _cancel(self, orders): + try: + self.bitshares.cancel(orders, account=self.account) + except bitsharesapi.exceptions.UnhandledRPCError as e: + if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': + # The order(s) we tried to cancel doesn't exist + print('nope') + return False + else: + raise + return True + def cancel(self, orders): - """ Cancel specific orders + """ Cancel specific order(s) """ - if not isinstance(orders, list): + if not isinstance(orders, (list, set, tuple)): orders = [orders] - return self.bitshares.cancel( - [o["id"] for o in orders if "id" in o], - account=self.account - ) + + orders = [order['id'] for order in orders if 'id' in order] + + success = self._cancel(orders) + if not success and len(orders) > 1: + for order in orders: + self._cancel(order) def cancel_all(self): """ Cancel all orders of the worker's account """ if self.orders: self.log.info('Canceling all orders') - return self.bitshares.cancel( - [o["id"] for o in self.orders], - account=self.account - ) + self.cancel(self.orders) def purge(self): """ Clear all the worker data from the database and cancel all orders diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 3320c4cd4..fcd9e0ec7 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,5 +1,6 @@ from dexbot.basestrategy import BaseStrategy from dexbot.queue.idle_queue import idle_add + from bitshares.amount import Amount @@ -38,9 +39,8 @@ def __init__(self, *args, **kwargs): @property def amount_quote(self): - """"" - Get quote amount, calculate if order size is relative - """"" + """ Get quote amount, calculate if order size is relative + """ if self.is_relative_order_size: quote_balance = float(self.balance(self.market["quote"])) return quote_balance * (self.order_size / 100) @@ -49,9 +49,8 @@ def amount_quote(self): @property def amount_base(self): - """"" - Get base amount, calculate if order size is relative - """"" + """ Get base amount, calculate if order size is relative + """ if self.is_relative_order_size: base_balance = float(self.balance(self.market["base"])) # amount = % of balance / buy_price = amount combined with calculated price to give % of balance From f2ca73082e18dfeff3b8f381fa67b791f2784046 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 17 Apr 2018 14:50:10 +0300 Subject: [PATCH 083/187] Remove extra whitespace --- dexbot/basestrategy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6d79b062f..46a599ff6 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -12,7 +12,6 @@ from bitshares.instance import shared_bitshares_instance - class BaseStrategy(Storage, StateMachine, Events): """ Base Strategy and methods available in all Sub Classes that inherit this BaseStrategy. From cbf8375808f8efe5acc3eb161beb0057d9083edb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 17 Apr 2018 15:29:24 +0300 Subject: [PATCH 084/187] Add base for staggered orders strategy --- cli.spec | 1 + .../controllers/create_worker_controller.py | 3 +- dexbot/strategies/staggered_orders.py | 47 +++++++++++++++++++ gui.spec | 1 + 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 dexbot/strategies/staggered_orders.py diff --git a/cli.spec b/cli.spec index 2b482460c..00d38dbb0 100644 --- a/cli.spec +++ b/cli.spec @@ -9,6 +9,7 @@ hiddenimports_strategies = [ 'dexbot.strategies', 'dexbot.strategies.echo', 'dexbot.strategies.relative_orders', + 'dexbot.strategies.staggered_orders', 'dexbot.strategies.storagedemo', 'dexbot.strategies.walls', ] diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 28ae5cbf7..745092ee3 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -16,7 +16,8 @@ def __init__(self, main_ctrl): @property def strategies(self): strategies = { - 'Relative Orders': 'dexbot.strategies.relative_orders' + 'Relative Orders': 'dexbot.strategies.relative_orders', + 'Staggered Orders': 'dexbot.strategies.staggered_orders' } return strategies diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py new file mode 100644 index 000000000..b7f75510b --- /dev/null +++ b/dexbot/strategies/staggered_orders.py @@ -0,0 +1,47 @@ +from dexbot.basestrategy import BaseStrategy +from dexbot.queue.idle_queue import idle_add + +from bitshares.amount import Amount + + +class Strategy(BaseStrategy): + """ Staggered Orders strategy + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Define Callbacks + self.onMarketUpdate += self.check_orders + self.onAccount += self.check_orders + + self.error_ontick = self.error + self.error_onMarketUpdate = self.error + self.error_onAccount = self.error + + self.worker_name = kwargs.get('name') + self.view = kwargs.get('view') + + self.check_orders() + + def error(self, *args, **kwargs): + self.cancel_all() + self.disabled = True + self.log.info(self.execute()) + + def update_orders(self): + self.log.info('Change detected, updating orders') + # Todo: implement logic + + def check_orders(self, *args, **kwargs): + """ Tests if the orders need updating + """ + pass + # Todo: implement logic + + # GUI updaters + def update_gui_profit(self): + pass + + def update_gui_slider(self): + pass diff --git a/gui.spec b/gui.spec index 8c2ad41c4..206ae2dd6 100644 --- a/gui.spec +++ b/gui.spec @@ -9,6 +9,7 @@ hiddenimports_strategies = [ 'dexbot.strategies', 'dexbot.strategies.echo', 'dexbot.strategies.relative_orders', + 'dexbot.strategies.staggered_orders', 'dexbot.strategies.storagedemo', 'dexbot.strategies.walls', ] From db1da5c145771248240bcc8349978700e5cbc171 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 18 Apr 2018 08:14:00 +0300 Subject: [PATCH 085/187] Change dexbot version number to 0.1.14 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 01fbe94b5..e88a70d04 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.13' +VERSION = '0.1.14' AUTHOR = "codaone" __version__ = VERSION From 05a2c79c2b4be50405440db9da42611938a3db25 Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Fri, 20 Apr 2018 19:05:25 +0300 Subject: [PATCH 086/187] Add _scrypt module to hidden_imports --- gui.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui.spec b/gui.spec index 8c2ad41c4..a242030f5 100644 --- a/gui.spec +++ b/gui.spec @@ -20,7 +20,7 @@ hiddenimports_packaging = [ a = Analysis(['dexbot/gui.py'], binaries=[], datas=[], - hiddenimports=hiddenimports_packaging + hiddenimports_strategies, + hiddenimports=hiddenimports_packaging + hiddenimports_strategies + ['_scrypt'], hookspath=['hooks'], runtime_hooks=['hooks/rthook-Crypto.py'], excludes=[], From 94a7276feb837f68e39ac39e072d54ebe074cad5 Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Fri, 20 Apr 2018 20:27:39 +0300 Subject: [PATCH 087/187] Change dexbot version number to 0.1.15 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e88a70d04..cce7a58bb 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.14' +VERSION = '0.1.15' AUTHOR = "codaone" __version__ = VERSION From 0a7aa620259446d60ddf22c830cb360107c4c0ba Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Apr 2018 08:55:49 +0300 Subject: [PATCH 088/187] Fix random error when selling/buying --- dexbot/basestrategy.py | 43 +++++++++++++++++++++++++++- dexbot/strategies/relative_orders.py | 30 ++++++------------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 46a599ff6..4b659004a 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -255,7 +255,6 @@ def _cancel(self, orders): except bitsharesapi.exceptions.UnhandledRPCError as e: if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': # The order(s) we tried to cancel doesn't exist - print('nope') return False else: raise @@ -281,6 +280,48 @@ def cancel_all(self): self.log.info('Canceling all orders') self.cancel(self.orders) + def market_buy(self, amount, price): + try: + buy_transaction = self.market.buy( + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + returnOrderId="head" + ) + except bitsharesapi.exceptions.UnhandledRPCError as e: + if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': + return None + else: + raise + + self.log.info( + 'Placed a buy order for {} {} @ {}'.format(price * amount, + self.market["base"]['symbol'], + price)) + buy_order = self.get_order(buy_transaction['orderid']) + return buy_order + + def market_sell(self, amount, price): + try: + sell_transaction = self.market.sell( + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + returnOrderId="head" + ) + except bitsharesapi.exceptions.UnhandledRPCError as e: + if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': + return None + else: + raise + + sell_order = self.get_order(sell_transaction['orderid']) + self.log.info( + 'Placed a sell order for {} {} @ {}'.format(amount, + self.market["quote"]['symbol'], + price)) + return sell_order + def purge(self): """ Clear all the worker data from the database and cancel all orders """ diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index fcd9e0ec7..503750f66 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -92,19 +92,12 @@ def update_orders(self): ) self.disabled = True else: - buy_transaction = self.market.buy( - self.buy_price, - Amount(amount=amount_base, asset=self.market["quote"]), - account=self.account, - returnOrderId="head" - ) - buy_order = self.get_order(buy_transaction['orderid']) - self.log.info('Placed a buy order for {} {} @ {}'.format(self.buy_price * amount_base, - self.market["base"]['symbol'], - self.buy_price)) + buy_order = self.market_buy(amount_base, self.buy_price) if buy_order: self['buy_order'] = buy_order - order_ids.append(buy_transaction['orderid']) + order_ids.append(buy_order['id']) + else: + self['buy_order'] = {} # Sell Side if float(self.balance(self.market["quote"])) < amount_quote: @@ -113,19 +106,12 @@ def update_orders(self): ) self.disabled = True else: - sell_transaction = self.market.sell( - self.sell_price, - Amount(amount=amount_quote, asset=self.market["quote"]), - account=self.account, - returnOrderId="head" - ) - sell_order = self.get_order(sell_transaction['orderid']) - self.log.info('Placed a sell order for {} {} @ {}'.format(amount_quote, - self.market["quote"]['symbol'], - self.sell_price)) + sell_order = self.market_sell(amount_quote, self.sell_price) if sell_order: self['sell_order'] = sell_order - order_ids.append(sell_transaction['orderid']) + order_ids.append(sell_order['id']) + else: + self['sell_order'] = {} self['order_ids'] = order_ids From cc1fe69551db52b5aa82274115c4b0a0cb20092a Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Apr 2018 10:20:01 +0300 Subject: [PATCH 089/187] Delete config.yml from the project root --- config.yml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 config.yml diff --git a/config.yml b/config.yml deleted file mode 100644 index 4ea34f4a6..000000000 --- a/config.yml +++ /dev/null @@ -1,3 +0,0 @@ -node: wss://bitshares.openledger.info/ws - -workers: {} \ No newline at end of file From 653a90844a5343a9b89569a1f7e9c8ed521ae20e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 26 Apr 2018 10:18:49 +0300 Subject: [PATCH 090/187] Fix worker name editing in the gui --- dexbot/views/worker_item.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index e1dc922c3..64d522dab 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -98,11 +98,11 @@ def remove_widget(self): self.view.remove_worker_widget(self.worker_name) self.view.ui.add_worker_button.setEnabled(True) - def reload_widget(self, worker_name, new_worker_name): + def reload_widget(self, worker_name): """ Cancels orders of the widget's worker and then reloads the data of the widget """ self.main_ctrl.remove_worker(worker_name) - self.worker_config = self.main_ctrl.get_worker_config(new_worker_name) + self.worker_config = self.main_ctrl.get_worker_config(worker_name) self.setup_ui_data(self.worker_config) self._pause_worker() @@ -115,5 +115,5 @@ def handle_edit_worker(self): if return_value: new_worker_name = edit_worker_dialog.worker_name self.main_ctrl.replace_worker_config(self.worker_name, new_worker_name, edit_worker_dialog.worker_data) - self.reload_widget(self.worker_name, new_worker_name) + self.reload_widget(new_worker_name) self.worker_name = new_worker_name From b293070a7819a6863b6705065a83a9e339d7fa04 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 26 Apr 2018 10:42:04 +0300 Subject: [PATCH 091/187] Fix order canceling when reloading a worker widget --- dexbot/views/worker_item.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 64d522dab..ede4637af 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -99,9 +99,8 @@ def remove_widget(self): self.view.ui.add_worker_button.setEnabled(True) def reload_widget(self, worker_name): - """ Cancels orders of the widget's worker and then reloads the data of the widget + """ Reload the data of the widget """ - self.main_ctrl.remove_worker(worker_name) self.worker_config = self.main_ctrl.get_worker_config(worker_name) self.setup_ui_data(self.worker_config) self._pause_worker() @@ -114,6 +113,7 @@ def handle_edit_worker(self): # User clicked save if return_value: new_worker_name = edit_worker_dialog.worker_name + self.main_ctrl.remove_worker(self.worker_name) self.main_ctrl.replace_worker_config(self.worker_name, new_worker_name, edit_worker_dialog.worker_data) self.reload_widget(new_worker_name) self.worker_name = new_worker_name From 1f867e4425d4729b635ce809c98a5b542ed2e6c4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Apr 2018 12:52:06 +0300 Subject: [PATCH 092/187] Add staggered orders functionality to the gui --- .gitignore | 2 +- .../controllers/create_worker_controller.py | 163 +++++++-- dexbot/views/create_worker.py | 137 +------- dexbot/views/edit_worker.py | 151 +------- dexbot/views/strategy_form.py | 114 +++++++ dexbot/views/ui/create_worker_window.ui | 226 ++---------- dexbot/views/ui/edit_worker_window.ui | 209 +----------- dexbot/views/ui/forms/__init__.py | 0 .../views/ui/forms/relative_orders_widget.ui | 265 +++++++++++++++ .../views/ui/forms/staggered_orders_widget.ui | 321 ++++++++++++++++++ dexbot/views/worker_item.py | 11 +- dexbot/views/worker_list.py | 5 +- 12 files changed, 910 insertions(+), 694 deletions(-) create mode 100644 dexbot/views/strategy_form.py create mode 100644 dexbot/views/ui/forms/__init__.py create mode 100644 dexbot/views/ui/forms/relative_orders_widget.ui create mode 100644 dexbot/views/ui/forms/staggered_orders_widget.ui diff --git a/.gitignore b/.gitignore index eaaebbad1..34e70e6ea 100644 --- a/.gitignore +++ b/.gitignore @@ -75,5 +75,5 @@ deprecated *.yml venv/ .idea/ -dexbot/views/ui/*_ui.py +dexbot/views/ui/**/*_ui.py dexbot/resources/*_rc.py diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 745092ee3..43ee60447 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -1,4 +1,9 @@ +import collections + from dexbot.controllers.main_controller import MainController +from dexbot.views.notice import NoticeDialog +from dexbot.views.confirmation import ConfirmationDialog +from dexbot.views.strategy_form import StrategyFormWidget import bitshares from bitshares.instance import shared_bitshares_instance @@ -9,20 +14,29 @@ class CreateWorkerController: - def __init__(self, main_ctrl): - self.main_ctrl = main_ctrl - self.bitshares = main_ctrl.bitshares_instance or shared_bitshares_instance() + def __init__(self, bitshares_instance, mode): + self.bitshares = bitshares_instance or shared_bitshares_instance() + self.mode = mode @property def strategies(self): - strategies = { - 'Relative Orders': 'dexbot.strategies.relative_orders', - 'Staggered Orders': 'dexbot.strategies.staggered_orders' + strategies = collections.OrderedDict() + strategies['dexbot.strategies.relative_orders'] = { + 'name': 'Relative Orders', + 'form_module': 'dexbot.views.ui.forms.relative_orders_widget_ui' + } + strategies['dexbot.strategies.staggered_orders'] = { + 'name': 'Staggered Orders', + 'form_module': 'dexbot.views.ui.forms.staggered_orders_widget_ui' } return strategies - def get_strategy_module(self, strategy): - return self.strategies[strategy] + @staticmethod + def get_strategies(): + """ Static method for getting the strategies + """ + controller = CreateWorkerController(None, None) + return controller.strategies @property def base_assets(self): @@ -31,11 +45,9 @@ def base_assets(self): ] return assets - def remove_worker(self, worker_name): - self.main_ctrl.remove_worker(worker_name) - - def is_worker_name_valid(self, worker_name): - worker_names = self.main_ctrl.get_workers_data().keys() + @staticmethod + def is_worker_name_valid(worker_name): + worker_names = MainController.get_workers_data().keys() # Check that the name is unique if worker_name in worker_names: return False @@ -91,8 +103,7 @@ def add_private_key(self, private_key): @staticmethod def get_unique_worker_name(): - """ - Returns unique worker name "Worker %n", where %n is the next available index + """ Returns unique worker name "Worker %n", where %n is the next available index """ index = 1 workers = MainController.get_workers_data().keys() @@ -103,12 +114,12 @@ def get_unique_worker_name(): return worker_name + def get_strategy_name(self, module): + return self.strategies[module]['name'] + @staticmethod - def get_worker_current_strategy(worker_data): - strategies = { - worker_data['strategy']: worker_data['module'] - } - return strategies + def get_strategy_module(worker_data): + return worker_data['module'] @staticmethod def get_assets(worker_data): @@ -125,21 +136,105 @@ def get_account(worker_data): return worker_data['account'] @staticmethod - def get_amount(worker_data): - return worker_data.get('amount', 0) + def handle_save_dialog(): + dialog = ConfirmationDialog('Saving the worker will cancel all the current orders.\n' + 'Are you sure you want to do this?') + return dialog.exec_() + + def change_strategy_form(self, ui, worker_data=None): + # Make sure the container is empty + for index in reversed(range(ui.strategy_container.count())): + ui.strategy_container.itemAt(index).widget().setParent(None) + + strategy_module = ui.strategy_input.currentData() + ui.strategy_widget = StrategyFormWidget(self, strategy_module, worker_data) + ui.strategy_container.addWidget(ui.strategy_widget) + + # Resize the dialog to be minimum possible height + width = ui.geometry().width() + ui.setMinimumSize(width, 0) + ui.resize(width, 1) + + def validate_worker_name(self, worker_name, old_worker_name=None): + if self.mode == 'add': + return self.is_worker_name_valid(worker_name) + elif self.mode == 'edit': + if old_worker_name != worker_name: + return self.is_worker_name_valid(worker_name) + return True - @staticmethod - def get_amount_relative(worker_data): - return worker_data.get('amount_relative', False) + def validate_asset(self, asset): + return self.is_asset_valid(asset) + + def validate_market(self, base_asset, quote_asset): + return base_asset.lower() != quote_asset.lower() + + def validate_account_name(self, account): + return self.account_exists(account) + + def validate_account(self, account, private_key): + return self.is_account_valid(account, private_key) + + def validate_account_not_in_use(self, account): + return not self.is_account_in_use(account) + + def validate_form(self, ui): + error_text = '' + base_asset = ui.base_asset_input.currentText() + quote_asset = ui.quote_asset_input.text() + worker_name = ui.worker_name_input.text() + + if not self.validate_asset(base_asset): + error_text += 'Field "Base Asset" does not have a valid asset.\n' + if not self.validate_asset(quote_asset): + error_text += 'Field "Quote Asset" does not have a valid asset.\n' + if not self.validate_market(base_asset, quote_asset): + error_text += "Market {}/{} doesn't exist.\n".format(base_asset, quote_asset) + if self.mode == 'add': + account = ui.account_input.text() + private_key = ui.private_key_input.text() + if not self.validate_worker_name(worker_name): + error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) + if not self.validate_account_name(account): + error_text += "Account doesn't exist.\n" + if not self.validate_account(account, private_key): + error_text += 'Private key is invalid.\n' + if not self.validate_account_not_in_use(account): + error_text += 'Use a different account. "{}" is already in use.\n'.format(account) + elif self.mode == 'edit': + if not self.validate_worker_name(worker_name, ui.worker_name): + error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) + error_text = error_text.rstrip() # Remove the extra line-ending + + if error_text: + dialog = NoticeDialog(error_text) + dialog.exec_() + return False + else: + return True - @staticmethod - def get_center_price(worker_data): - return worker_data.get('center_price', 0) + def handle_save(self, ui): + if not self.validate_form(ui): + return - @staticmethod - def get_center_price_dynamic(worker_data): - return worker_data.get('center_price_dynamic', True) + if self.mode == 'add': + # Add the private key to the database + private_key = ui.private_key_input.text() + self.add_private_key(private_key) - @staticmethod - def get_spread(worker_data): - return worker_data.get('spread', 5) + account = ui.account_input.text() + else: + account = ui.account_name.text() + + base_asset = ui.base_asset_input.currentText() + quote_asset = ui.quote_asset_input.text() + strategy_module = ui.strategy_input.currentData() + + ui.worker_data = { + 'account': account, + 'market': '{}/{}'.format(quote_asset, base_asset), + 'module': strategy_module, + **ui.strategy_widget.values + } + ui.worker_name = ui.worker_name_input.text() + ui.accept() diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index bca589b0b..3434b6236 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -1,137 +1,32 @@ -from .notice import NoticeDialog from .ui.create_worker_window_ui import Ui_Dialog from PyQt5 import QtWidgets -class CreateWorkerView(QtWidgets.QDialog): +class CreateWorkerView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, controller): super().__init__() self.controller = controller + self.strategy_widget = None - self.ui = Ui_Dialog() - self.ui.setupUi(self) + self.setupUi(self) # Todo: Using a model here would be more Qt like - self.ui.strategy_input.addItems(self.controller.strategies) - self.ui.base_asset_input.addItems(self.controller.base_assets) + # Populate the comboboxes + strategies = self.controller.strategies + for strategy in strategies: + self.strategy_input.addItem(strategies[strategy]['name'], strategy) + self.base_asset_input.addItems(self.controller.base_assets) + # Generate a name for the worker self.worker_name = controller.get_unique_worker_name() - self.ui.worker_name_input.setText(self.worker_name) + self.worker_name_input.setText(self.worker_name) - self.ui.save_button.clicked.connect(self.handle_save) - self.ui.cancel_button.clicked.connect(self.reject) - self.ui.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) - self.ui.relative_order_size_checkbox.stateChanged.connect(self.onchange_relative_order_size_checkbox) - self.worker_data = {} - - def onchange_relative_order_size_checkbox(self): - checkbox = self.ui.relative_order_size_checkbox - if checkbox.isChecked(): - self.ui.amount_input.setSuffix('%') - self.ui.amount_input.setDecimals(2) - self.ui.amount_input.setMaximum(100.00) - self.ui.amount_input.setValue(10.00) - self.ui.amount_input.setMinimumWidth(151) - else: - self.ui.amount_input.setSuffix('') - self.ui.amount_input.setDecimals(8) - self.ui.amount_input.setMaximum(1000000000.000000) - self.ui.amount_input.setValue(0.000000) - - def onchange_center_price_dynamic_checkbox(self): - checkbox = self.ui.center_price_dynamic_checkbox - if checkbox.isChecked(): - self.ui.center_price_input.setDisabled(True) - else: - self.ui.center_price_input.setDisabled(False) - - def validate_worker_name(self): - worker_name = self.ui.worker_name_input.text() - return self.controller.is_worker_name_valid(worker_name) - - def validate_asset(self, asset): - return self.controller.is_asset_valid(asset) - - def validate_market(self): - base_asset = self.ui.base_asset_input.currentText() - quote_asset = self.ui.quote_asset_input.text() - return base_asset.lower() != quote_asset.lower() - - def validate_account_name(self): - account = self.ui.account_input.text() - return self.controller.account_exists(account) - - def validate_account(self): - account = self.ui.account_input.text() - private_key = self.ui.private_key_input.text() - return self.controller.is_account_valid(account, private_key) + # Set signals + self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form(self)) + self.save_button.clicked.connect(lambda: controller.handle_save(self)) + self.cancel_button.clicked.connect(self.reject) - def validate_account_not_in_use(self): - account = self.ui.account_input.text() - return not self.controller.is_account_in_use(account) - - def validate_form(self): - error_text = '' - base_asset = self.ui.base_asset_input.currentText() - quote_asset = self.ui.quote_asset_input.text() - if not self.validate_worker_name(): - worker_name = self.ui.worker_name_input.text() - error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) - if not self.validate_asset(base_asset): - error_text += 'Field "Base Asset" does not have a valid asset.\n' - if not self.validate_asset(quote_asset): - error_text += 'Field "Quote Asset" does not have a valid asset.\n' - if not self.validate_market(): - error_text += "Market {}/{} doesn't exist.\n".format(base_asset, quote_asset) - if not self.validate_account_name(): - error_text += "Account doesn't exist.\n" - if not self.validate_account(): - error_text += 'Private key is invalid.\n' - if not self.validate_account_not_in_use(): - account = self.ui.account_input.text() - error_text += 'Use a different account. "{}" is already in use.\n'.format(account) - error_text = error_text.rstrip() # Remove the extra line-ending - - if error_text: - dialog = NoticeDialog(error_text) - dialog.exec_() - return False - else: - return True - - def handle_save(self): - if not self.validate_form(): - return - - # Add the private key to the database - private_key = self.ui.private_key_input.text() - self.controller.add_private_key(private_key) - - ui = self.ui - spread = float(ui.spread_input.text()[:-1]) # Remove the percentage character from the end - - # If order size is relative, remove percentage character in the end - if ui.relative_order_size_checkbox.isChecked(): - amount = float(ui.amount_input.text()[:-1]) - else: - amount = ui.amount_input.text() - - base_asset = ui.base_asset_input.currentText() - quote_asset = ui.quote_asset_input.text() - strategy = ui.strategy_input.currentText() - worker_module = self.controller.get_strategy_module(strategy) - self.worker_data = { - 'account': ui.account_input.text(), - 'market': '{}/{}'.format(quote_asset, base_asset), - 'module': worker_module, - 'strategy': strategy, - 'amount': amount, - 'amount_relative': bool(ui.relative_order_size_checkbox.isChecked()), - 'center_price': float(ui.center_price_input.text()), - 'center_price_dynamic': bool(ui.center_price_dynamic_checkbox.isChecked()), - 'spread': spread - } - self.worker_name = ui.worker_name_input.text() - self.accept() + self.controller.change_strategy_form(self) + self.worker_data = {} diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 533db183b..a36a4e3f4 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -1,6 +1,4 @@ from .ui.edit_worker_window_ui import Ui_Dialog -from .confirmation import ConfirmationDialog -from .notice import NoticeDialog from PyQt5 import QtWidgets @@ -10,146 +8,31 @@ class EditWorkerView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, controller, worker_name, config): super().__init__() self.controller = controller + self.worker_name = worker_name + self.strategy_widget = None self.setupUi(self) worker_data = config['workers'][worker_name] - self.strategy_input.addItems(self.controller.get_worker_current_strategy(worker_data)) - self.worker_name = worker_name + + # Todo: Using a model here would be more Qt like + # Populate the comboboxes + strategies = self.controller.strategies + for strategy in strategies: + self.strategy_input.addItem(strategies[strategy]['name'], strategy) + + # Set values from config + index = self.strategy_input.findData(self.controller.get_strategy_module(worker_data)) + self.strategy_input.setCurrentIndex(index) self.worker_name_input.setText(worker_name) self.base_asset_input.addItem(self.controller.get_base_asset(worker_data)) self.base_asset_input.addItems(self.controller.base_assets) self.quote_asset_input.setText(self.controller.get_quote_asset(worker_data)) self.account_name.setText(self.controller.get_account(worker_data)) - if self.controller.get_amount_relative(worker_data): - self.order_size_input_to_relative() - self.relative_order_size_checkbox.setChecked(True) - else: - self.order_size_input_to_static() - self.relative_order_size_checkbox.setChecked(False) - - self.amount_input.setValue(float(self.controller.get_amount(worker_data))) - - self.center_price_input.setValue(self.controller.get_center_price(worker_data)) - - center_price_dynamic = self.controller.get_center_price_dynamic(worker_data) - if center_price_dynamic: - self.center_price_input.setEnabled(False) - self.center_price_dynamic_checkbox.setChecked(True) - else: - self.center_price_input.setEnabled(True) - self.center_price_dynamic_checkbox.setChecked(False) - - self.spread_input.setValue(self.controller.get_spread(worker_data)) - self.save_button.clicked.connect(self.handle_save) + # Set signals + self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form(self)) + self.save_button.clicked.connect(lambda: self.controller.handle_save(self)) self.cancel_button.clicked.connect(self.reject) - self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) - self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) - self.relative_order_size_checkbox.stateChanged.connect(self.onchange_relative_order_size_checkbox) - self.worker_data = {} - - def order_size_input_to_relative(self): - input_field = self.amount_input - input_field.setSuffix('%') - input_field.setDecimals(2) - input_field.setMaximum(100.00) - input_field.setMinimumWidth(151) - - def order_size_input_to_static(self): - input_field = self.amount_input - input_field.setSuffix('') - input_field.setDecimals(8) - input_field.setMaximum(1000000000.000000) - input_field.setMinimumWidth(151) - - def onchange_relative_order_size_checkbox(self): - if self.relative_order_size_checkbox.isChecked(): - self.order_size_input_to_relative() - self.amount_input.setValue(10.00) - else: - self.order_size_input_to_static() - self.amount_input.setValue(0.000000) - - def onchange_center_price_dynamic_checkbox(self): - checkbox = self.center_price_dynamic_checkbox - if checkbox.isChecked(): - self.center_price_input.setDisabled(True) - else: - self.center_price_input.setDisabled(False) - def validate_worker_name(self): - old_worker_name = self.worker_name - worker_name = self.worker_name_input.text() - if old_worker_name != worker_name: - return self.controller.is_worker_name_valid(worker_name) - return True - - def validate_asset(self, asset): - return self.controller.is_asset_valid(asset) - - def validate_market(self): - base_asset = self.base_asset_input.currentText() - quote_asset = self.quote_asset_input.text() - return base_asset.lower() != quote_asset.lower() - - def validate_form(self): - error_text = '' - base_asset = self.base_asset_input.currentText() - quote_asset = self.quote_asset_input.text() - - if not self.validate_worker_name(): - worker_name = self.worker_name_input.text() - error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) - if not self.validate_asset(base_asset): - error_text += 'Field "Base Asset" does not have a valid asset.\n' - if not self.validate_asset(quote_asset): - error_text += 'Field "Quote Asset" does not have a valid asset.\n' - if not self.validate_market(): - error_text += "Market {}/{} doesn't exist.\n".format(base_asset, quote_asset) - error_text = error_text.rstrip() # Remove the extra line-ending - - if error_text: - dialog = NoticeDialog(error_text) - dialog.exec_() - return False - else: - return True - - @staticmethod - def handle_save_dialog(): - dialog = ConfirmationDialog('Saving the worker will cancel all the current orders.\n' - 'Are you sure you want to do this?') - return dialog.exec_() - - def handle_save(self): - if not self.validate_form(): - return - - if not self.handle_save_dialog(): - return - - spread = float(self.spread_input.text()[:-1]) # Remove the percentage character from the end - - # If order size is relative, remove percentage character in the end - if self.relative_order_size_checkbox.isChecked(): - amount = float(self.amount_input.text()[:-1]) - else: - amount = self.amount_input.text() - - base_asset = self.base_asset_input.currentText() - quote_asset = self.quote_asset_input.text() - strategy = self.strategy_input.currentText() - worker_module = self.controller.get_strategy_module(strategy) - self.worker_data = { - 'account': self.account_name.text(), - 'market': '{}/{}'.format(quote_asset, base_asset), - 'module': worker_module, - 'strategy': strategy, - 'amount': amount, - 'amount_relative': bool(self.relative_order_size_checkbox.isChecked()), - 'center_price': float(self.center_price_input.text()), - 'center_price_dynamic': bool(self.center_price_dynamic_checkbox.isChecked()), - 'spread': spread - } - self.worker_name = self.worker_name_input.text() - self.accept() + self.controller.change_strategy_form(self, worker_data) + self.worker_data = {} diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py new file mode 100644 index 000000000..5be76d3b6 --- /dev/null +++ b/dexbot/views/strategy_form.py @@ -0,0 +1,114 @@ +import importlib + +from PyQt5 import QtWidgets + + +class StrategyFormWidget(QtWidgets.QWidget): + + def __init__(self, controller, strategy_module, config=None): + super().__init__() + self.controller = controller + self.module_name = strategy_module.split('.')[-1] + + form_module = controller.strategies[strategy_module]['form_module'] + widget = getattr( + importlib.import_module(form_module), + 'Ui_Form' + ) + self.strategy_widget = widget() + self.strategy_widget.setupUi(self) + + # Call methods based on the selected strategy + if self.module_name == 'relative_orders': + self.strategy_widget.relative_order_size_checkbox.toggled.connect( + self.onchange_relative_order_size_checkbox) + if config: + self.set_relative_orders_values(config) + elif self.module_name == 'staggered_orders': + if config: + self.set_staggered_orders_values(config) + + def onchange_relative_order_size_checkbox(self, checked): + if checked: + self.order_size_input_to_relative() + else: + self.order_size_input_to_static() + + def order_size_input_to_relative(self): + self.strategy_widget.amount_input.setSuffix('%') + self.strategy_widget.amount_input.setDecimals(2) + self.strategy_widget.amount_input.setMaximum(100.00) + self.strategy_widget.amount_input.setMinimumWidth(151) + self.strategy_widget.amount_input.setValue(10.00) + + def order_size_input_to_static(self): + self.strategy_widget.amount_input.setSuffix('') + self.strategy_widget.amount_input.setDecimals(8) + self.strategy_widget.amount_input.setMaximum(1000000000.000000) + self.strategy_widget.amount_input.setValue(0.000000) + + @property + def values(self): + """ Returns values all the form values based on selected strategy + """ + if self.module_name == 'relative_orders': + return self.relative_orders_values + elif self.module_name == 'staggered_orders': + return self.staggered_orders_values + + def set_relative_orders_values(self, worker_data): + if worker_data.get('amount_relative', False): + self.order_size_input_to_relative() + self.strategy_widget.relative_order_size_checkbox.setChecked(True) + else: + self.order_size_input_to_static() + self.strategy_widget.relative_order_size_checkbox.setChecked(False) + + self.strategy_widget.amount_input.setValue(float(worker_data.get('amount', 0))) + self.strategy_widget.center_price_input.setValue(worker_data.get('center_price', 0)) + self.strategy_widget.spread_input.setValue(worker_data.get('spread', 5)) + + if worker_data.get('center_price_dynamic', True): + self.strategy_widget.center_price_dynamic_checkbox.setChecked(True) + else: + self.strategy_widget.center_price_dynamic_checkbox.setChecked(False) + + def set_staggered_orders_values(self, worker_data): + self.strategy_widget.increment_input.setValue(worker_data.get('increment', 2.5)) + self.strategy_widget.spread_input.setValue(worker_data.get('spread', 5)) + self.strategy_widget.lower_bound_input.setValue(worker_data.get('lower_bound', 0.000001)) + self.strategy_widget.upper_bound_input.setValue(worker_data.get('upper_bound', 1000000)) + + @property + def relative_orders_values(self): + # Remove the percentage character from the end + spread = float(self.strategy_widget.spread_input.text()[:-1]) + + # If order size is relative, remove percentage character from the end + if self.strategy_widget.relative_order_size_checkbox.isChecked(): + amount = float(self.strategy_widget.amount_input.text()[:-1]) + else: + amount = self.strategy_widget.amount_input.text() + + data = { + 'amount': amount, + 'amount_relative': bool(self.strategy_widget.relative_order_size_checkbox.isChecked()), + 'center_price': float(self.strategy_widget.center_price_input.text()), + 'center_price_dynamic': bool(self.strategy_widget.center_price_dynamic_checkbox.isChecked()), + 'spread': spread + } + return data + + @property + def staggered_orders_values(self): + # Remove the percentage character from the end + spread = float(self.strategy_widget.spread_input.text()[:-1]) + increment = float(self.strategy_widget.increment_input.text()[:-1]) + + data = { + 'spread': spread, + 'increment': increment, + 'lower_bound': float(self.strategy_widget.lower_bound_input.text()), + 'upper_bound': float(self.strategy_widget.upper_bound_input.text()) + } + return data diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index b19e4458d..a5d0c60ba 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -7,12 +7,15 @@ 0 0 418 - 529 + 345 DEXBot - Create Worker + + + true @@ -180,206 +183,6 @@ - - - - Worker Parameters - - - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Amount - - - amount_input - - - - - - - - 0 - 0 - - - - - 140 - 0 - - - - - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Center Price - - - center_price_input - - - - - - - false - - - - 0 - 0 - - - - - 140 - 0 - - - - - - - false - - - false - - - 8 - - - -999999999.998999953269958 - - - 999999999.998999953269958 - - - - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Spread - - - spread_input - - - - - - - - 0 - 0 - - - - - 151 - 0 - - - - - - - % - - - 100000.000000000000000 - - - 5.000000000000000 - - - - - - - Calculate center price dynamically - - - true - - - - - - - Relative order size - - - - - - @@ -519,6 +322,24 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + @@ -528,9 +349,6 @@ quote_asset_input account_input private_key_input - amount_input - center_price_input - spread_input save_button cancel_button diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 354ce3da0..a8c28df04 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -7,7 +7,7 @@ 0 0 400 - 486 + 302 @@ -198,199 +198,20 @@ - - - Worker Parameters - - - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Amount - - - amount_input - - - - - - - - 0 - 0 - - - - - 140 - 0 - - - - - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Center Price - - - center_price_input - - - - - - - false - - - - 0 - 0 - - - - - 140 - 0 - - - - - - - false - - - false - - - 8 - - - -999999999.998999953269958 - - - 999999999.998999953269958 - - - - - - - Calculate center price dynamically - - - - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Spread - - - spread_input - - - - - - - - 0 - 0 - - - - - 151 - 0 - - - - - - - % - - - 100000.000000000000000 - - - 5.000000000000000 - - - - - - - Relative order size - - - + + + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/dexbot/views/ui/forms/__init__.py b/dexbot/views/ui/forms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui new file mode 100644 index 000000000..e239b0451 --- /dev/null +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -0,0 +1,265 @@ + + + Form + + + + 0 + 0 + 446 + 203 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Worker Parameters + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Amount + + + amount_input + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Center Price + + + center_price_input + + + + + + + false + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + false + + + false + + + 8 + + + -999999999.998999953269958 + + + 999999999.998999953269958 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Spread + + + spread_input + + + + + + + + 0 + 0 + + + + + 151 + 0 + + + + + + + % + + + 100000.000000000000000 + + + 5.000000000000000 + + + + + + + Calculate center price dynamically + + + true + + + + + + + Relative order size + + + + + + + + + + + + center_price_dynamic_checkbox + clicked(bool) + center_price_input + setDisabled(bool) + + + 284 + 129 + + + 208 + 99 + + + + + diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui new file mode 100644 index 000000000..bf10e2050 --- /dev/null +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -0,0 +1,321 @@ + + + Form + + + + 0 + 0 + 382 + 256 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Worker Parameters + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Spread + + + spread_input + + + + + + + + 0 + 0 + + + + + 151 + 0 + + + + + + + % + + + 100000.000000000000000 + + + 5.000000000000000 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Increment + + + spread_input + + + + + + + + 0 + 0 + + + + + 151 + 0 + + + + + + + % + + + 100000.000000000000000 + + + 2.500000000000000 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Lower bound + + + spread_input + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Upper bound + + + spread_input + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + 8 + + + 1000000000.000000000000000 + + + 0.000001000000000 + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + 8 + + + 1000000000.000000000000000 + + + 1000000.000000000000000 + + + + + + + + + + Worker info + + + + 3 + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Required quote + + + true + + + + + + + N/A + + + + + + + Required base + + + + + + + N/A + + + + + + + + + + + diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index e1dc922c3..ed6a37cab 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -35,6 +35,10 @@ def setup_ui_data(self, config): market = config['workers'][worker_name]['market'] self.set_worker_market(market) + module = config['workers'][worker_name]['module'] + strategies = CreateWorkerController.get_strategies() + self.set_worker_strategy(strategies[module]['name']) + profit = db_worker.execute(db_worker.get_item, worker_name, 'profit') if profit: self.set_worker_profit(profit) @@ -68,8 +72,9 @@ def _pause_worker(self): def set_worker_name(self, value): self.worker_name_label.setText(value) - def set_worker_account(self, value): - pass + def set_worker_strategy(self, value): + value = value.upper() + self.strategy_label.setText(value) def set_worker_market(self, value): self.currency_label.setText(value) @@ -107,7 +112,7 @@ def reload_widget(self, worker_name, new_worker_name): self._pause_worker() def handle_edit_worker(self): - controller = CreateWorkerController(self.main_ctrl) + controller = CreateWorkerController(self.main_ctrl.bitshares_instance, 'edit') edit_worker_dialog = EditWorkerView(controller, self.worker_name, self.worker_config) return_value = edit_worker_dialog.exec_() diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 2c4c9084a..41bd15092 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -1,8 +1,7 @@ -from dexbot import __version__ - from .ui.worker_list_window_ui import Ui_MainWindow from .create_worker import CreateWorkerView from .worker_item import WorkerItemWidget +from dexbot import __version__ from dexbot.controllers.create_worker_controller import CreateWorkerController from dexbot.queue.queue_dispatcher import ThreadDispatcher @@ -58,7 +57,7 @@ def remove_worker_widget(self, worker_name): self.ui.add_worker_button.setEnabled(True) def handle_add_worker(self): - controller = CreateWorkerController(self.main_ctrl) + controller = CreateWorkerController(self.main_ctrl.bitshares_instance, 'add') create_worker_dialog = CreateWorkerView(controller) return_value = create_worker_dialog.exec_() From 3c5190b313bae4b50c60c728fd5682a46b4c5ae5 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Apr 2018 10:20:01 +0300 Subject: [PATCH 093/187] Delete config.yml from the project root --- config.yml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 config.yml diff --git a/config.yml b/config.yml deleted file mode 100644 index 4ea34f4a6..000000000 --- a/config.yml +++ /dev/null @@ -1,3 +0,0 @@ -node: wss://bitshares.openledger.info/ws - -workers: {} \ No newline at end of file From 2103605144a314eb788190fb39d169175a1b5bb9 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 26 Apr 2018 10:18:49 +0300 Subject: [PATCH 094/187] Fix worker name editing in the gui --- dexbot/views/worker_item.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index e1dc922c3..64d522dab 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -98,11 +98,11 @@ def remove_widget(self): self.view.remove_worker_widget(self.worker_name) self.view.ui.add_worker_button.setEnabled(True) - def reload_widget(self, worker_name, new_worker_name): + def reload_widget(self, worker_name): """ Cancels orders of the widget's worker and then reloads the data of the widget """ self.main_ctrl.remove_worker(worker_name) - self.worker_config = self.main_ctrl.get_worker_config(new_worker_name) + self.worker_config = self.main_ctrl.get_worker_config(worker_name) self.setup_ui_data(self.worker_config) self._pause_worker() @@ -115,5 +115,5 @@ def handle_edit_worker(self): if return_value: new_worker_name = edit_worker_dialog.worker_name self.main_ctrl.replace_worker_config(self.worker_name, new_worker_name, edit_worker_dialog.worker_data) - self.reload_widget(self.worker_name, new_worker_name) + self.reload_widget(new_worker_name) self.worker_name = new_worker_name From 4a758f4822fa59ee7c75cfc37ecdf2e4d4eefba8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 26 Apr 2018 10:42:04 +0300 Subject: [PATCH 095/187] Fix order canceling when reloading a worker widget --- dexbot/views/worker_item.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 64d522dab..ede4637af 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -99,9 +99,8 @@ def remove_widget(self): self.view.ui.add_worker_button.setEnabled(True) def reload_widget(self, worker_name): - """ Cancels orders of the widget's worker and then reloads the data of the widget + """ Reload the data of the widget """ - self.main_ctrl.remove_worker(worker_name) self.worker_config = self.main_ctrl.get_worker_config(worker_name) self.setup_ui_data(self.worker_config) self._pause_worker() @@ -114,6 +113,7 @@ def handle_edit_worker(self): # User clicked save if return_value: new_worker_name = edit_worker_dialog.worker_name + self.main_ctrl.remove_worker(self.worker_name) self.main_ctrl.replace_worker_config(self.worker_name, new_worker_name, edit_worker_dialog.worker_data) self.reload_widget(new_worker_name) self.worker_name = new_worker_name From 7e700dacb2f67f1c594d7a1ee9ac68f4f771e322 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Apr 2018 13:14:43 +0300 Subject: [PATCH 096/187] Change dexbot version number to 0.1.16 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index cce7a58bb..59e42f3e1 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.15' +VERSION = '0.1.16' AUTHOR = "codaone" __version__ = VERSION From 15c3c4053b2b73cfbc3b74180df954e4b018227d Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Tue, 1 May 2018 21:03:52 +0300 Subject: [PATCH 097/187] Add connection latency status to status bar --- dexbot/views/worker_list.py | 48 ++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 2c4c9084a..8c77c026f 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -5,6 +5,10 @@ from .worker_item import WorkerItemWidget from dexbot.controllers.create_worker_controller import CreateWorkerController from dexbot.queue.queue_dispatcher import ThreadDispatcher +from dexbot.queue.idle_queue import idle_add +import time +from bitshares.instance import shared_bitshares_instance +from threading import Thread from PyQt5 import QtWidgets @@ -20,7 +24,9 @@ def __init__(self, main_ctrl): self.max_workers = 10 self.num_of_workers = 0 self.worker_widgets = {} - self.ui.status_bar.showMessage("ver {}".format(__version__)) + self.closing = False + self.statusbar_updater = None + self.statusbar_updater_first_run = True self.ui.add_worker_button.clicked.connect(self.handle_add_worker) @@ -39,6 +45,13 @@ def __init__(self, main_ctrl): self.dispatcher = ThreadDispatcher(self) self.dispatcher.start() + self.ui.status_bar.showMessage("ver {} - node delay: - ms".format(__version__)) + self.statusbar_updater = Thread( + target=self._update_statusbar_message + ) + self.statusbar_updater.start() + + def add_worker_widget(self, worker_name): config = self.main_ctrl.get_worker_config(worker_name) widget = WorkerItemWidget(worker_name, config, self.main_ctrl, self) @@ -86,3 +99,36 @@ def set_worker_slider(self, worker_name, value): def customEvent(self, event): # Process idle_queue_dispatcher events event.callback() + + def closeEvent(self, event): + self.closing = True + self.ui.status_bar.showMessage("Closing app...") + if self.statusbar_updater and self.statusbar_updater.is_alive(): + self.statusbar_updater.join() + + def _update_statusbar_message(self): + while not self.closing: + # When running first time the workers are also interrupting with the connection + # so we delay the first time to get correct information + if (self.statusbar_updater_first_run): + self.statusbar_updater_first_run = False + time.sleep(1) + + idle_add(self.set_statusbar_message) + runner_count = 0 + # Wait for 30s but do it in 0.5s pieces to not prevent closing the app + while not self.closing and runner_count < 60: + runner_count += 1 + time.sleep(0.5) + + def set_statusbar_message(self): + start = time.time() + bts_instance = shared_bitshares_instance() + try: + # @todo should here be used num_retries=1 ? + bts_instance.connect() + latency = (time.time() - start) * 1000 + except: + latency = -1 + + self.ui.status_bar.showMessage("ver {} - node delay: {:.2f}ms".format(__version__, latency)) \ No newline at end of file From f4fe281932194d2083f8024967eb5c7d8614cfbd Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Wed, 2 May 2018 10:30:29 +0300 Subject: [PATCH 098/187] Fix new line at end of file --- dexbot/views/worker_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 8c77c026f..dea7a6686 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -131,4 +131,4 @@ def set_statusbar_message(self): except: latency = -1 - self.ui.status_bar.showMessage("ver {} - node delay: {:.2f}ms".format(__version__, latency)) \ No newline at end of file + self.ui.status_bar.showMessage("ver {} - node delay: {:.2f}ms".format(__version__, latency)) From 785fa332cb831ac52d4d4b2f2fcefda9b8d5e3c3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 2 May 2018 10:37:01 +0300 Subject: [PATCH 099/187] Fix missing config lock in worker.py --- dexbot/__init__.py | 3 +-- dexbot/worker.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 59e42f3e1..3bd28f1ac 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.16' +VERSION = '0.1.17' AUTHOR = "codaone" __version__ = VERSION @@ -21,4 +21,3 @@ with open(config_file, 'w') as f: f.write(default_config) print("Created default config file at {}".format(config_file)) - diff --git a/dexbot/worker.py b/dexbot/worker.py index 07f502650..b60ee65e1 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -49,6 +49,7 @@ def __init__( def init_workers(self, config): """ Initialize the workers """ + self.config_lock.acquire() for worker_name, worker in config["workers"].items(): if "account" not in worker: log_workers.critical("Worker has no account", extra={ @@ -80,6 +81,7 @@ def init_workers(self, config): 'worker_name': worker_name, 'account': worker['account'], 'market': 'unknown', 'is_disabled': (lambda: True) }) + self.config_lock.release() def update_notify(self): if not self.config['workers']: From 91a528901d9edb83e20325665cf2a3f4191f7414 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 2 May 2018 13:31:38 +0300 Subject: [PATCH 100/187] Change latency calculation in worker_list.py --- dexbot/views/worker_list.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index dea7a6686..0e4f9518e 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -11,6 +11,7 @@ from threading import Thread from PyQt5 import QtWidgets +from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC class MainView(QtWidgets.QMainWindow): @@ -45,13 +46,12 @@ def __init__(self, main_ctrl): self.dispatcher = ThreadDispatcher(self) self.dispatcher.start() - self.ui.status_bar.showMessage("ver {} - node delay: - ms".format(__version__)) + self.ui.status_bar.showMessage("ver {} - Node delay: - ms".format(__version__)) self.statusbar_updater = Thread( target=self._update_statusbar_message ) self.statusbar_updater.start() - def add_worker_widget(self, worker_name): config = self.main_ctrl.get_worker_config(worker_name) widget = WorkerItemWidget(worker_name, config, self.main_ctrl, self) @@ -110,7 +110,7 @@ def _update_statusbar_message(self): while not self.closing: # When running first time the workers are also interrupting with the connection # so we delay the first time to get correct information - if (self.statusbar_updater_first_run): + if self.statusbar_updater_first_run: self.statusbar_updater_first_run = False time.sleep(1) @@ -122,13 +122,17 @@ def _update_statusbar_message(self): time.sleep(0.5) def set_statusbar_message(self): - start = time.time() - bts_instance = shared_bitshares_instance() + config = self.main_ctrl.load_config() + node = config['node'] + try: - # @todo should here be used num_retries=1 ? - bts_instance.connect() + start = time.time() + BitSharesNodeRPC(node, num_retries=1) latency = (time.time() - start) * 1000 - except: + except BaseException: latency = -1 - self.ui.status_bar.showMessage("ver {} - node delay: {:.2f}ms".format(__version__, latency)) + if latency != -1: + self.ui.status_bar.showMessage("ver {} - Node delay: {:.2f}ms".format(__version__, latency)) + else: + self.ui.status_bar.showMessage("ver {} - Node disconnected".format(__version__)) From bf3c7abffd003540cb2d7e1388abdc9b5cb8bb5e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 2 May 2018 13:53:40 +0300 Subject: [PATCH 101/187] Change dexbot version number to 0.1.18 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 59e42f3e1..e5554168b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.16' +VERSION = '0.1.18' AUTHOR = "codaone" __version__ = VERSION From ef105bee25c004c040aa451e0a6ce02f8b7855e0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 2 May 2018 13:56:11 +0300 Subject: [PATCH 102/187] Change import order in worker_list.py --- dexbot/views/worker_list.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 0e4f9518e..2864f4b4f 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -1,14 +1,13 @@ -from dexbot import __version__ +import time +from threading import Thread +from dexbot import __version__ from .ui.worker_list_window_ui import Ui_MainWindow from .create_worker import CreateWorkerView from .worker_item import WorkerItemWidget from dexbot.controllers.create_worker_controller import CreateWorkerController from dexbot.queue.queue_dispatcher import ThreadDispatcher from dexbot.queue.idle_queue import idle_add -import time -from bitshares.instance import shared_bitshares_instance -from threading import Thread from PyQt5 import QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC From ca8828827ce086554f56ffd72f4937dabf29f2f5 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 3 May 2018 08:03:35 +0300 Subject: [PATCH 103/187] Fix order canceling bug When order cancel fails, clear the transaction buffer so that the cancel call is removed from the transaction --- dexbot/__init__.py | 2 +- dexbot/basestrategy.py | 37 ++++++++++------------------ dexbot/strategies/relative_orders.py | 12 ++++++--- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index cce7a58bb..009a0bed4 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.15' +VERSION = '0.1.19' AUTHOR = "codaone" __version__ = VERSION diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 4b659004a..0e720eb5d 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -255,6 +255,7 @@ def _cancel(self, orders): except bitsharesapi.exceptions.UnhandledRPCError as e: if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': # The order(s) we tried to cancel doesn't exist + self.bitshares.txbuffer.clear() return False else: raise @@ -281,18 +282,12 @@ def cancel_all(self): self.cancel(self.orders) def market_buy(self, amount, price): - try: - buy_transaction = self.market.buy( - price, - Amount(amount=amount, asset=self.market["quote"]), - account=self.account.name, - returnOrderId="head" - ) - except bitsharesapi.exceptions.UnhandledRPCError as e: - if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': - return None - else: - raise + buy_transaction = self.market.buy( + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + returnOrderId="head" + ) self.log.info( 'Placed a buy order for {} {} @ {}'.format(price * amount, @@ -302,18 +297,12 @@ def market_buy(self, amount, price): return buy_order def market_sell(self, amount, price): - try: - sell_transaction = self.market.sell( - price, - Amount(amount=amount, asset=self.market["quote"]), - account=self.account.name, - returnOrderId="head" - ) - except bitsharesapi.exceptions.UnhandledRPCError as e: - if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': - return None - else: - raise + sell_transaction = self.market.sell( + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + returnOrderId="head" + ) sell_order = self.get_order(sell_transaction['orderid']) self.log.info( diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 503750f66..1890f842a 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -79,6 +79,10 @@ def update_orders(self): # Cancel the orders before redoing them self.cancel_all() + # Mark the orders empty + self['buy_order'] = {} + self['sell_order'] = {} + order_ids = [] amount_base = self.amount_base @@ -96,8 +100,6 @@ def update_orders(self): if buy_order: self['buy_order'] = buy_order order_ids.append(buy_order['id']) - else: - self['buy_order'] = {} # Sell Side if float(self.balance(self.market["quote"])) < amount_quote: @@ -110,11 +112,13 @@ def update_orders(self): if sell_order: self['sell_order'] = sell_order order_ids.append(sell_order['id']) - else: - self['sell_order'] = {} self['order_ids'] = order_ids + # Some orders weren't successfully created, redo them + if len(order_ids) < 2 and not self.disabled: + self.update_orders() + def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ From cb9156c3e6e38775db8cd343f6dc129ed48db304 Mon Sep 17 00:00:00 2001 From: mikakoi Date: Thu, 3 May 2018 14:51:02 +0300 Subject: [PATCH 104/187] Create issue_template.md --- docs/issue_template.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/issue_template.md diff --git a/docs/issue_template.md b/docs/issue_template.md new file mode 100644 index 000000000..4054600ea --- /dev/null +++ b/docs/issue_template.md @@ -0,0 +1,16 @@ +## Expected Behavior + + +## Actual Behavior + + +## Steps to Reproduce the Problem + + 1. + 2. + 3. + +## Specifications + + - Version: + - OS: From faf65bcea10ab45207faeb3ab1e3786ecb7019a7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 4 May 2018 09:24:30 +0300 Subject: [PATCH 105/187] Add shortcut methods to DatabaseWorker --- dexbot/storage.py | 50 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index ec785284a..b62199236 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -53,22 +53,22 @@ def __init__(self, category): self.category = category def __setitem__(self, key, value): - db_worker.execute_noreturn(db_worker.set_item, self.category, key, value) + db_worker.set_item(self.category, key, value) def __getitem__(self, key): - return db_worker.execute(db_worker.get_item, self.category, key) + return db_worker.get_item(self.category, key) def __delitem__(self, key): - db_worker.execute_noreturn(db_worker.del_item, self.category, key) + db_worker.del_item(self.category, key) def __contains__(self, key): - return db_worker.execute(db_worker.contains, self.category, key) + return db_worker.contains(self.category, key) def items(self): - return db_worker.execute(db_worker.get_items, self.category) + return db_worker.get_items(self.category) def clear(self): - db_worker.execute_noreturn(db_worker.clear, self.category) + db_worker.clear(self.category) class DatabaseWorker(threading.Thread): @@ -100,7 +100,7 @@ def run(self): args = args+(token,) func(*args) - def get_result(self, token): + def _get_result(self, token): while True: with self.lock: if token in self.results: @@ -111,7 +111,7 @@ def get_result(self, token): self.event.clear() self.event.wait() - def set_result(self, token, result): + def _set_result(self, token, result): with self.lock: self.results[token] = result self.event.set() @@ -119,12 +119,15 @@ def set_result(self, token, result): def execute(self, func, *args): token = str(uuid.uuid4) self.task_queue.put((func, args, token)) - return self.get_result(token) + return self._get_result(token) def execute_noreturn(self, func, *args): self.task_queue.put((func, args, None)) - + def set_item(self, category, key, value): + self.execute_noreturn(self._set_item, category, key, value) + + def _set_item(self, category, key, value): value = json.dumps(value) e = self.session.query(Config).filter_by( category=category, @@ -137,7 +140,10 @@ def set_item(self, category, key, value): self.session.add(e) self.session.commit() - def get_item(self, category, key, token): + def get_item(self, category, key): + self.execute(self._get_item, category, key) + + def _get_item(self, category, key, token): e = self.session.query(Config).filter_by( category=category, key=key @@ -146,9 +152,12 @@ def get_item(self, category, key, token): result = None else: result = json.loads(e.value) - self.set_result(token, result) + self._set_result(token, result) def del_item(self, category, key): + self.execute_noreturn(self._del_item, category, key) + + def _del_item(self, category, key): e = self.session.query(Config).filter_by( category=category, key=key @@ -156,21 +165,30 @@ def del_item(self, category, key): self.session.delete(e) self.session.commit() - def contains(self, category, key, token): + def contains(self, category, key): + self.execute(self._contains, category, key) + + def _contains(self, category, key, token): e = self.session.query(Config).filter_by( category=category, key=key ).first() - self.set_result(token, bool(e)) + self._set_result(token, bool(e)) + + def get_items(self, category): + self.execute(self._get_items, category) - def get_items(self, category, token): + def _get_items(self, category, token): es = self.session.query(Config).filter_by( category=category ).all() result = [(e.key, e.value) for e in es] - self.set_result(token, result) + self._set_result(token, result) def clear(self, category): + self.execute_noreturn(self._clear, category) + + def _clear(self, category): rows = self.session.query(Config).filter_by( category=category ) From e689ec1a6df13fcae98c657f1ccfe26ee43524a3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 4 May 2018 10:04:05 +0300 Subject: [PATCH 106/187] Change calculate_center_price to not be a property --- dexbot/basestrategy.py | 1 - dexbot/strategies/relative_orders.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 0e720eb5d..ec03e2d4c 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -130,7 +130,6 @@ def __init__( 'is_disabled': lambda: self.disabled} ) - @property def calculate_center_price(self): ticker = self.market.ticker() highest_bid = ticker.get("highestBid") diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 1890f842a..d8c4cacee 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -60,7 +60,7 @@ def amount_base(self): def calculate_order_prices(self): if self.is_center_price_dynamic: - self.center_price = self.calculate_center_price + self.center_price = self.calculate_center_price() self.buy_price = self.center_price * (1 - (self.worker["spread"] / 2) / 100) self.sell_price = self.center_price * (1 + (self.worker["spread"] / 2) / 100) From 4ec764bf97d1e607628144a5c02194e7ac9c4f80 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 4 May 2018 10:04:57 +0300 Subject: [PATCH 107/187] Add WIP staggered orders logic --- dexbot/strategies/staggered_orders.py | 55 +++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b7f75510b..99a2f4c4c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -21,6 +21,10 @@ def __init__(self, *args, **kwargs): self.worker_name = kwargs.get('name') self.view = kwargs.get('view') + self.spread = self.worker['spread'] + self.increment = self.worker['increment'] + self.upper_bound = self.worker['upper_bound'] + self.lower_bound = self.worker['lower_bound'] self.check_orders() @@ -29,15 +33,58 @@ def error(self, *args, **kwargs): self.disabled = True self.log.info(self.execute()) - def update_orders(self): + def init_strategy(self): + center_price = self.calculate_center_price() + buy_prices = [] + buy_price = center_price * (1 + self.spread / 2) + buy_prices.append(buy_price) + + while buy_price > self.lower_bound: + buy_price = buy_price / (1 + self.increment) + buy_prices.append(buy_price) + + sell_prices = [] + sell_price = center_price * (1 - self.spread / 2) + sell_prices.append(sell_price) + + while sell_price < self.upper_bound: + sell_price = sell_price * (1 + self.increment) + sell_prices.append(sell_price) + + self['orders'] = [] + + def update_order(self, order, order_type): self.log.info('Change detected, updating orders') - # Todo: implement logic + # Make sure + self.cancel(order) + + if order_type == 'buy': + amount = order['quote']['amount'] + price = order['price'] * self.spread + new_order = self.market_sell(amount, price) + else: + amount = order['base']['amount'] + price = order['price'] / self.spread + new_order = self.market_buy(amount, price) + + self['orders'] = new_order def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ - pass - # Todo: implement logic + for order in self['sell_orders']: + current_order = self.get_updated_order(order) + if current_order['quote']['amount'] != order['quote']['amount']: + self.update_order(order, 'sell') + + for order in self['buy_orders']: + current_order = self.get_updated_order(order) + if current_order['quote']['amount'] != order['quote']['amount']: + self.update_order(order, 'buy') + + if self.view: + self.update_gui_profit() + self.update_gui_slider() # GUI updaters def update_gui_profit(self): From 5b4631385162a3aa29ea93bb59d61810c9464fc3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 4 May 2018 14:57:23 +0300 Subject: [PATCH 108/187] Change center price calculation to be relative in relative orders --- dexbot/basestrategy.py | 31 ++++++++++++++++++++++++++++ dexbot/strategies/relative_orders.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 0e720eb5d..e8458ae24 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -149,6 +149,37 @@ def calculate_center_price(self): center_price = (highest_bid['price'] + lowest_ask['price']) / 2 return center_price + def calculate_relative_center_price(self, spread, order_ids=None): + """ Calculate center price which shifts based on available funds + """ + ticker = self.market.ticker() + highest_bid = ticker.get("highestBid").get('price') + lowest_ask = ticker.get("lowestAsk").get('price') + latest_price = ticker.get('latest').get('price') + if highest_bid is None or highest_bid == 0.0: + self.log.critical( + "Cannot estimate center price, there is no highest bid." + ) + self.disabled = True + elif lowest_ask is None or lowest_ask == 0.0: + self.log.critical( + "Cannot estimate center price, there is no lowest ask." + ) + self.disabled = True + else: + total_balance = self.total_balance(order_ids) + total = (total_balance['quote'] * latest_price) + total_balance['base'] + + if not total: # Prevent division by zero + percentage = 0.5 + else: + percentage = (total_balance['base'] / total) + center_price = (highest_bid + lowest_ask) / 2 + lowest_price = center_price * (1 - spread / 100) + highest_price = center_price * (1 + spread / 100) + relative_center_price = ((highest_price - lowest_price) * percentage) + lowest_price + return relative_center_price + @property def orders(self): """ Return the worker's open accounts in the current market diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 1890f842a..594c0340b 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -60,7 +60,7 @@ def amount_base(self): def calculate_order_prices(self): if self.is_center_price_dynamic: - self.center_price = self.calculate_center_price + self.center_price = self.calculate_relative_center_price(self['order_ids']) self.buy_price = self.center_price * (1 - (self.worker["spread"] / 2) / 100) self.sell_price = self.center_price * (1 + (self.worker["spread"] / 2) / 100) From c80d0effedb8a8f19ad78247fcd20d3f5c5765a2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 4 May 2018 14:59:25 +0300 Subject: [PATCH 109/187] Change dexbot version number to 0.1.20 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e6a0afcce..6dc4e3fca 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.19' +VERSION = '0.1.20' AUTHOR = "codaone" __version__ = VERSION From bd42bb596e1c4c6ee904679ec5f0ac9447ccd20f Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Fri, 4 May 2018 17:38:06 +0300 Subject: [PATCH 110/187] Hot fix for the dynamice center price --- dexbot/strategies/relative_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 594c0340b..53950dfe4 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -60,7 +60,7 @@ def amount_base(self): def calculate_order_prices(self): if self.is_center_price_dynamic: - self.center_price = self.calculate_relative_center_price(self['order_ids']) + self.center_price = self.calculate_relative_center_price(self.worker['spread'], self['order_ids']) self.buy_price = self.center_price * (1 - (self.worker["spread"] / 2) / 100) self.sell_price = self.center_price * (1 + (self.worker["spread"] / 2) / 100) From 2517ce6124689c625e217492b4e9cd2985cc9631 Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Fri, 4 May 2018 18:52:01 +0300 Subject: [PATCH 111/187] Change dexbot version number to 0.1.21 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 6dc4e3fca..e54070e2d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.20' +VERSION = '0.1.21' AUTHOR = "codaone" __version__ = VERSION From 5b76bc7c11cad5574c9e0878c025027566e42d40 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 7 May 2018 13:47:06 +0300 Subject: [PATCH 112/187] Fix storage data return logic --- dexbot/storage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index b62199236..fe8911041 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -141,7 +141,7 @@ def _set_item(self, category, key, value): self.session.commit() def get_item(self, category, key): - self.execute(self._get_item, category, key) + return self.execute(self._get_item, category, key) def _get_item(self, category, key, token): e = self.session.query(Config).filter_by( @@ -166,7 +166,7 @@ def _del_item(self, category, key): self.session.commit() def contains(self, category, key): - self.execute(self._contains, category, key) + return self.execute(self._contains, category, key) def _contains(self, category, key, token): e = self.session.query(Config).filter_by( @@ -176,7 +176,7 @@ def _contains(self, category, key, token): self._set_result(token, bool(e)) def get_items(self, category): - self.execute(self._get_items, category) + return self.execute(self._get_items, category) def _get_items(self, category, token): es = self.session.query(Config).filter_by( From f643f5933e8e45ba97e7ca593a263d87903bcb3f Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Tue, 8 May 2018 13:18:39 +1000 Subject: [PATCH 113/187] working on issue #36 - uses QMessageBox to display python exceptions raised, either from workers (via the logging interface) or from toplevel GUI event handlers using the new @guierror decorator. --- dexbot/controllers/main_controller.py | 5 +++++ dexbot/ui.py | 18 +++++++++++++++++- dexbot/views/create_wallet.py | 2 ++ dexbot/views/create_worker.py | 4 ++++ dexbot/views/edit_worker.py | 5 ++++- dexbot/views/unlock_wallet.py | 3 ++- dexbot/views/worker_item.py | 6 ++++++ dexbot/views/worker_list.py | 2 ++ 8 files changed, 42 insertions(+), 3 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index a140526da..f57a5e9b8 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -3,6 +3,8 @@ from dexbot import config_file from dexbot.worker import WorkerInfrastructure +from dexbot.views.errors import PyQtHandler + from ruamel.yaml import YAML from bitshares.instance import set_shared_bitshares_instance @@ -22,6 +24,9 @@ def __init__(self, bitshares_instance): fh.setFormatter(formatter) logger.addHandler(fh) logger.setLevel(logging.INFO) + pyqth = PyQtHandler() + pyqth.setLevel(logging.ERROR) + logger.addHandler(pyqth) def create_worker(self, worker_name, config, view): # Todo: Add some threading here so that the GUI doesn't freeze diff --git a/dexbot/ui.py b/dexbot/ui.py index c241f72a4..f563e91a4 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -50,7 +50,7 @@ def new_func(ctx, *args, **kwargs): ch.setFormatter(formatter1) logging.getLogger("dexbot").addHandler(ch) logging.getLogger("").handlers = [] - + # GrapheneAPI logging if ctx.obj["verbose"] > 4: verbosity = [ @@ -159,3 +159,19 @@ def confirmalert(msg): click.style("Alert", fg="red") + "] " + msg ) + +# error message "translation" +# here we convert some of the cryptic Graphene API error messages into a longer sentence +# particularly whe the problem is something the user themselves can fix (such as not enough +# money in account) +# it's here because both GUI and CLI might use it + + +TRANSLATIONS = {'amount_to_sell.amount > 0': "You need to have sufficient buy and sell amounts in your account"} + + +def translate_error(err): + for k, v in TRANSLATIONS.items(): + if k in err: + return v + return None diff --git a/dexbot/views/create_wallet.py b/dexbot/views/create_wallet.py index e8ef06025..84be09bc4 100644 --- a/dexbot/views/create_wallet.py +++ b/dexbot/views/create_wallet.py @@ -1,5 +1,6 @@ from .ui.create_wallet_window_ui import Ui_Dialog from .notice import NoticeDialog +from .errors import guierror from PyQt5 import QtWidgets @@ -13,6 +14,7 @@ def __init__(self, controller): self.ui.setupUi(self) self.ui.ok_button.clicked.connect(self.validate_form) + @guierror def validate_form(self): password = self.ui.password_input.text() confirm_password = self.ui.confirm_password_input.text() diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index bca589b0b..7e62d0def 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -1,5 +1,6 @@ from .notice import NoticeDialog from .ui.create_worker_window_ui import Ui_Dialog +from .errors import guierror from PyQt5 import QtWidgets @@ -26,6 +27,7 @@ def __init__(self, controller): self.ui.relative_order_size_checkbox.stateChanged.connect(self.onchange_relative_order_size_checkbox) self.worker_data = {} + @guierror def onchange_relative_order_size_checkbox(self): checkbox = self.ui.relative_order_size_checkbox if checkbox.isChecked(): @@ -40,6 +42,7 @@ def onchange_relative_order_size_checkbox(self): self.ui.amount_input.setMaximum(1000000000.000000) self.ui.amount_input.setValue(0.000000) + @guierror def onchange_center_price_dynamic_checkbox(self): checkbox = self.ui.center_price_dynamic_checkbox if checkbox.isChecked(): @@ -101,6 +104,7 @@ def validate_form(self): else: return True + @guierror def handle_save(self): if not self.validate_form(): return diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 533db183b..23875a615 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -1,6 +1,7 @@ from .ui.edit_worker_window_ui import Ui_Dialog from .confirmation import ConfirmationDialog from .notice import NoticeDialog +from .errors import guierror from PyQt5 import QtWidgets @@ -44,7 +45,6 @@ def __init__(self, controller, worker_name, config): self.save_button.clicked.connect(self.handle_save) self.cancel_button.clicked.connect(self.reject) self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) - self.center_price_dynamic_checkbox.stateChanged.connect(self.onchange_center_price_dynamic_checkbox) self.relative_order_size_checkbox.stateChanged.connect(self.onchange_relative_order_size_checkbox) self.worker_data = {} @@ -62,6 +62,7 @@ def order_size_input_to_static(self): input_field.setMaximum(1000000000.000000) input_field.setMinimumWidth(151) + @guierror def onchange_relative_order_size_checkbox(self): if self.relative_order_size_checkbox.isChecked(): self.order_size_input_to_relative() @@ -70,6 +71,7 @@ def onchange_relative_order_size_checkbox(self): self.order_size_input_to_static() self.amount_input.setValue(0.000000) + @guierror def onchange_center_price_dynamic_checkbox(self): checkbox = self.center_price_dynamic_checkbox if checkbox.isChecked(): @@ -121,6 +123,7 @@ def handle_save_dialog(): 'Are you sure you want to do this?') return dialog.exec_() + @guierror def handle_save(self): if not self.validate_form(): return diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py index f67efecc4..f940dc413 100644 --- a/dexbot/views/unlock_wallet.py +++ b/dexbot/views/unlock_wallet.py @@ -1,6 +1,6 @@ from .ui.unlock_wallet_window_ui import Ui_Dialog from .notice import NoticeDialog - +from .errors import guierror from PyQt5 import QtWidgets @@ -13,6 +13,7 @@ def __init__(self, controller): self.ui.setupUi(self) self.ui.ok_button.clicked.connect(self.validate_form) + @guierror def validate_form(self): password = self.ui.password_input.text() if not self.controller.unlock_wallet(password): diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index ede4637af..99f273db2 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -4,6 +4,8 @@ from dexbot.storage import db_worker from dexbot.controllers.create_worker_controller import CreateWorkerController +from dexbot.views.errors import guierror + from PyQt5 import QtWidgets @@ -47,6 +49,7 @@ def setup_ui_data(self, config): else: self.set_worker_slider(50) + @guierror def start_worker(self): self._start_worker() self.main_ctrl.create_worker(self.worker_name, self.worker_config, self.view) @@ -56,6 +59,7 @@ def _start_worker(self): self.pause_button.show() self.play_button.hide() + @guierror def pause_worker(self): self._pause_worker() self.main_ctrl.stop_worker(self.worker_name) @@ -85,6 +89,7 @@ def set_worker_profit(self, value): def set_worker_slider(self, value): self.order_slider.setSliderPosition(value) + @guierror def remove_widget_dialog(self): dialog = ConfirmationDialog('Are you sure you want to remove worker "{}"?'.format(self.worker_name)) return_value = dialog.exec_() @@ -105,6 +110,7 @@ def reload_widget(self, worker_name): self.setup_ui_data(self.worker_config) self._pause_worker() + @guierror def handle_edit_worker(self): controller = CreateWorkerController(self.main_ctrl) edit_worker_dialog = EditWorkerView(controller, self.worker_name, self.worker_config) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 2864f4b4f..d47d8dc8a 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -11,6 +11,7 @@ from PyQt5 import QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC +from .errors import guierror class MainView(QtWidgets.QMainWindow): @@ -69,6 +70,7 @@ def remove_worker_widget(self, worker_name): if self.num_of_workers < self.max_workers: self.ui.add_worker_button.setEnabled(True) + @guierror def handle_add_worker(self): controller = CreateWorkerController(self.main_ctrl) create_worker_dialog = CreateWorkerView(controller) From 42a4c74cde7b905fb5a987c947a1675f8c6ea50e Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Tue, 8 May 2018 13:24:20 +1000 Subject: [PATCH 114/187] this file needed too --- dexbot/views/errors.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 dexbot/views/errors.py diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py new file mode 100644 index 000000000..b04f9396e --- /dev/null +++ b/dexbot/views/errors.py @@ -0,0 +1,55 @@ +from PyQt5 import QtWidgets +from PyQt5.Qt import QApplication + +import logging +import traceback +import sys + +from dexbot.ui import translate_error +from dexbot.queue.idle_queue import idle_add + +class PyQtHandler(logging.Handler): + """ + Logging handler for Py Qt events. + Based on Vinay Sajip's DBHandler class (http://www.red-dove.com/python_logging.html) + """ + def emit(self, record): + # Use default formatting: + self.format(record) + message = record.msg + extra = translate_error(message) + if record.exc_info: + if not extra: + extra = translate_error(repr(record.exc_info[1])) + detail = logging._defaultFormatter.formatException(record.exc_info) + else: + detail = None + if hasattr(record, "worker_name"): + title = "Error on {}".format(record.worker_name) + else: + title = "DEXBot Error" + idle_add(showdialog, title, message, extra, detail) + +def guierror(func): + """A decorator for GUI handler functions - traps all exceptions and displays the dialog + """ + def func_wrapper(obj, *args, **kwargs): + try: + return func(obj) + except BaseException as exc: + showdialog("DEXBot Error", "An error occurred with DEXBOT: "+repr(exc), None, traceback.format_exc()) + + return func_wrapper + +def showdialog(title, message, extra=None, detail=None): + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Critical) + msg.setText(message) + if extra: + msg.setInformativeText(extra) + msg.setWindowTitle(title) + if detail: + msg.setDetailedText(detail) + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) + + msg.exec_() From 78812c61b7eb2f21e0b732a1a26ee33fd4f5a9d2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 8 May 2018 09:01:54 +0300 Subject: [PATCH 115/187] Fix pep8 errors --- dexbot/views/errors.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index b04f9396e..449a05779 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -8,6 +8,7 @@ from dexbot.ui import translate_error from dexbot.queue.idle_queue import idle_add + class PyQtHandler(logging.Handler): """ Logging handler for Py Qt events. @@ -28,7 +29,8 @@ def emit(self, record): title = "Error on {}".format(record.worker_name) else: title = "DEXBot Error" - idle_add(showdialog, title, message, extra, detail) + idle_add(show_dialog, title, message, extra, detail) + def guierror(func): """A decorator for GUI handler functions - traps all exceptions and displays the dialog @@ -37,19 +39,21 @@ def func_wrapper(obj, *args, **kwargs): try: return func(obj) except BaseException as exc: - showdialog("DEXBot Error", "An error occurred with DEXBOT: "+repr(exc), None, traceback.format_exc()) + show_dialog("DEXBot Error", "An error occurred with DEXBot: \n"+repr(exc), None, traceback.format_exc()) return func_wrapper -def showdialog(title, message, extra=None, detail=None): - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Critical) - msg.setText(message) - if extra: - msg.setInformativeText(extra) - msg.setWindowTitle(title) - if detail: - msg.setDetailedText(detail) - msg.setStandardButtons(QtWidgets.QMessageBox.Ok) - - msg.exec_() + +def show_dialog(title, message, extra=None, detail=None): + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Critical) + msg.setText(message) + if extra: + msg.setInformativeText(extra) + msg.setWindowTitle(title) + if detail: + msg.setDetailedText(detail) + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) + + msg.exec_() + From 61f88fca56f758296e9d79bb2e4f83825c655bb1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 8 May 2018 09:17:51 +0300 Subject: [PATCH 116/187] Change guierror function name to gui_error --- dexbot/views/create_wallet.py | 4 ++-- dexbot/views/create_worker.py | 8 ++++---- dexbot/views/edit_worker.py | 8 ++++---- dexbot/views/errors.py | 8 +++----- dexbot/views/unlock_wallet.py | 4 ++-- dexbot/views/worker_item.py | 10 +++++----- dexbot/views/worker_list.py | 4 ++-- 7 files changed, 22 insertions(+), 24 deletions(-) diff --git a/dexbot/views/create_wallet.py b/dexbot/views/create_wallet.py index 84be09bc4..a4016576c 100644 --- a/dexbot/views/create_wallet.py +++ b/dexbot/views/create_wallet.py @@ -1,6 +1,6 @@ from .ui.create_wallet_window_ui import Ui_Dialog from .notice import NoticeDialog -from .errors import guierror +from .errors import gui_error from PyQt5 import QtWidgets @@ -14,7 +14,7 @@ def __init__(self, controller): self.ui.setupUi(self) self.ui.ok_button.clicked.connect(self.validate_form) - @guierror + @gui_error def validate_form(self): password = self.ui.password_input.text() confirm_password = self.ui.confirm_password_input.text() diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 7e62d0def..3239cac70 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -1,6 +1,6 @@ from .notice import NoticeDialog from .ui.create_worker_window_ui import Ui_Dialog -from .errors import guierror +from .errors import gui_error from PyQt5 import QtWidgets @@ -27,7 +27,7 @@ def __init__(self, controller): self.ui.relative_order_size_checkbox.stateChanged.connect(self.onchange_relative_order_size_checkbox) self.worker_data = {} - @guierror + @gui_error def onchange_relative_order_size_checkbox(self): checkbox = self.ui.relative_order_size_checkbox if checkbox.isChecked(): @@ -42,7 +42,7 @@ def onchange_relative_order_size_checkbox(self): self.ui.amount_input.setMaximum(1000000000.000000) self.ui.amount_input.setValue(0.000000) - @guierror + @gui_error def onchange_center_price_dynamic_checkbox(self): checkbox = self.ui.center_price_dynamic_checkbox if checkbox.isChecked(): @@ -104,7 +104,7 @@ def validate_form(self): else: return True - @guierror + @gui_error def handle_save(self): if not self.validate_form(): return diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 23875a615..d7ea03881 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -1,7 +1,7 @@ from .ui.edit_worker_window_ui import Ui_Dialog from .confirmation import ConfirmationDialog from .notice import NoticeDialog -from .errors import guierror +from .errors import gui_error from PyQt5 import QtWidgets @@ -62,7 +62,7 @@ def order_size_input_to_static(self): input_field.setMaximum(1000000000.000000) input_field.setMinimumWidth(151) - @guierror + @gui_error def onchange_relative_order_size_checkbox(self): if self.relative_order_size_checkbox.isChecked(): self.order_size_input_to_relative() @@ -71,7 +71,7 @@ def onchange_relative_order_size_checkbox(self): self.order_size_input_to_static() self.amount_input.setValue(0.000000) - @guierror + @gui_error def onchange_center_price_dynamic_checkbox(self): checkbox = self.center_price_dynamic_checkbox if checkbox.isChecked(): @@ -123,7 +123,7 @@ def handle_save_dialog(): 'Are you sure you want to do this?') return dialog.exec_() - @guierror + @gui_error def handle_save(self): if not self.validate_form(): return diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index 449a05779..39a82bf49 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -1,13 +1,11 @@ -from PyQt5 import QtWidgets -from PyQt5.Qt import QApplication - import logging import traceback -import sys from dexbot.ui import translate_error from dexbot.queue.idle_queue import idle_add +from PyQt5 import QtWidgets + class PyQtHandler(logging.Handler): """ @@ -32,7 +30,7 @@ def emit(self, record): idle_add(show_dialog, title, message, extra, detail) -def guierror(func): +def gui_error(func): """A decorator for GUI handler functions - traps all exceptions and displays the dialog """ def func_wrapper(obj, *args, **kwargs): diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py index f940dc413..94036e318 100644 --- a/dexbot/views/unlock_wallet.py +++ b/dexbot/views/unlock_wallet.py @@ -1,6 +1,6 @@ from .ui.unlock_wallet_window_ui import Ui_Dialog from .notice import NoticeDialog -from .errors import guierror +from .errors import gui_error from PyQt5 import QtWidgets @@ -13,7 +13,7 @@ def __init__(self, controller): self.ui.setupUi(self) self.ui.ok_button.clicked.connect(self.validate_form) - @guierror + @gui_error def validate_form(self): password = self.ui.password_input.text() if not self.controller.unlock_wallet(password): diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 99f273db2..387ed57b8 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -4,7 +4,7 @@ from dexbot.storage import db_worker from dexbot.controllers.create_worker_controller import CreateWorkerController -from dexbot.views.errors import guierror +from dexbot.views.errors import gui_error from PyQt5 import QtWidgets @@ -49,7 +49,7 @@ def setup_ui_data(self, config): else: self.set_worker_slider(50) - @guierror + @gui_error def start_worker(self): self._start_worker() self.main_ctrl.create_worker(self.worker_name, self.worker_config, self.view) @@ -59,7 +59,7 @@ def _start_worker(self): self.pause_button.show() self.play_button.hide() - @guierror + @gui_error def pause_worker(self): self._pause_worker() self.main_ctrl.stop_worker(self.worker_name) @@ -89,7 +89,7 @@ def set_worker_profit(self, value): def set_worker_slider(self, value): self.order_slider.setSliderPosition(value) - @guierror + @gui_error def remove_widget_dialog(self): dialog = ConfirmationDialog('Are you sure you want to remove worker "{}"?'.format(self.worker_name)) return_value = dialog.exec_() @@ -110,7 +110,7 @@ def reload_widget(self, worker_name): self.setup_ui_data(self.worker_config) self._pause_worker() - @guierror + @gui_error def handle_edit_worker(self): controller = CreateWorkerController(self.main_ctrl) edit_worker_dialog = EditWorkerView(controller, self.worker_name, self.worker_config) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index d47d8dc8a..116ea7f64 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -11,7 +11,7 @@ from PyQt5 import QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC -from .errors import guierror +from .errors import gui_error class MainView(QtWidgets.QMainWindow): @@ -70,7 +70,7 @@ def remove_worker_widget(self, worker_name): if self.num_of_workers < self.max_workers: self.ui.add_worker_button.setEnabled(True) - @guierror + @gui_error def handle_add_worker(self): controller = CreateWorkerController(self.main_ctrl) create_worker_dialog = CreateWorkerView(controller) From 6f28cc8ca4a5930197d99d46586b052254b23ea9 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Tue, 8 May 2018 16:34:36 +1000 Subject: [PATCH 117/187] pep8 fixes as pointed out by mikakoi --- dexbot/views/errors.py | 28 ++++++++++++++++------------ dexbot/views/unlock_wallet.py | 1 + dexbot/views/worker_item.py | 1 - dexbot/views/worker_list.py | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index b04f9396e..a370502d8 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -8,11 +8,13 @@ from dexbot.ui import translate_error from dexbot.queue.idle_queue import idle_add + class PyQtHandler(logging.Handler): """ Logging handler for Py Qt events. Based on Vinay Sajip's DBHandler class (http://www.red-dove.com/python_logging.html) """ + def emit(self, record): # Use default formatting: self.format(record) @@ -30,6 +32,7 @@ def emit(self, record): title = "DEXBot Error" idle_add(showdialog, title, message, extra, detail) + def guierror(func): """A decorator for GUI handler functions - traps all exceptions and displays the dialog """ @@ -38,18 +41,19 @@ def func_wrapper(obj, *args, **kwargs): return func(obj) except BaseException as exc: showdialog("DEXBot Error", "An error occurred with DEXBOT: "+repr(exc), None, traceback.format_exc()) - + return func_wrapper + def showdialog(title, message, extra=None, detail=None): - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Critical) - msg.setText(message) - if extra: - msg.setInformativeText(extra) - msg.setWindowTitle(title) - if detail: - msg.setDetailedText(detail) - msg.setStandardButtons(QtWidgets.QMessageBox.Ok) - - msg.exec_() + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Critical) + msg.setText(message) + if extra: + msg.setInformativeText(extra) + msg.setWindowTitle(title) + if detail: + msg.setDetailedText(detail) + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) + + msg.exec_() diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py index f940dc413..765a094d8 100644 --- a/dexbot/views/unlock_wallet.py +++ b/dexbot/views/unlock_wallet.py @@ -1,6 +1,7 @@ from .ui.unlock_wallet_window_ui import Ui_Dialog from .notice import NoticeDialog from .errors import guierror + from PyQt5 import QtWidgets diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 99f273db2..2594b0be8 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -3,7 +3,6 @@ from .edit_worker import EditWorkerView from dexbot.storage import db_worker from dexbot.controllers.create_worker_controller import CreateWorkerController - from dexbot.views.errors import guierror from PyQt5 import QtWidgets diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index d47d8dc8a..4c5cb1242 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -8,10 +8,10 @@ from dexbot.controllers.create_worker_controller import CreateWorkerController from dexbot.queue.queue_dispatcher import ThreadDispatcher from dexbot.queue.idle_queue import idle_add +from .errors import guierror from PyQt5 import QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC -from .errors import guierror class MainView(QtWidgets.QMainWindow): From 8b468890db6e8f49c2d048a37df62e826cace4bc Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 8 May 2018 09:50:44 +0300 Subject: [PATCH 118/187] Change dexbot version number to 0.1.22 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e54070e2d..d54ae927d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.21' +VERSION = '0.1.22' AUTHOR = "codaone" __version__ = VERSION From 7affb07c284ca799785c8c343003c68730c824b0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 8 May 2018 12:46:33 +0300 Subject: [PATCH 119/187] Add amount user input to staggered orders --- dexbot/strategies/staggered_orders.py | 1 + dexbot/views/strategy_form.py | 2 + .../views/ui/forms/staggered_orders_widget.ui | 87 ++++++++++++++++--- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 99a2f4c4c..7f5da1b29 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -21,6 +21,7 @@ def __init__(self, *args, **kwargs): self.worker_name = kwargs.get('name') self.view = kwargs.get('view') + self.amount = self.worker['amount'] self.spread = self.worker['spread'] self.increment = self.worker['increment'] self.upper_bound = self.worker['upper_bound'] diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 5be76d3b6..1f5d7295e 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -74,6 +74,7 @@ def set_relative_orders_values(self, worker_data): self.strategy_widget.center_price_dynamic_checkbox.setChecked(False) def set_staggered_orders_values(self, worker_data): + self.strategy_widget.amount_input.setValue(worker_data.get('amount', 0)) self.strategy_widget.increment_input.setValue(worker_data.get('increment', 2.5)) self.strategy_widget.spread_input.setValue(worker_data.get('spread', 5)) self.strategy_widget.lower_bound_input.setValue(worker_data.get('lower_bound', 0.000001)) @@ -106,6 +107,7 @@ def staggered_orders_values(self): increment = float(self.strategy_widget.increment_input.text()[:-1]) data = { + 'amount': float(self.strategy_widget.amount_input.text()), 'spread': spread, 'increment': increment, 'lower_bound': float(self.strategy_widget.lower_bound_input.text()), diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index bf10e2050..bd9574faf 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -32,7 +32,7 @@ Worker Parameters - + @@ -60,7 +60,7 @@ - + @@ -88,7 +88,7 @@ - + @@ -116,7 +116,7 @@ - + @@ -144,7 +144,7 @@ - + @@ -172,7 +172,35 @@ - + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + 8 + + + 1000000000.000000000000000 + + + 0.000001000000000 + + + + @@ -200,8 +228,8 @@ - - + + 0 @@ -224,12 +252,12 @@ 1000000000.000000000000000 - 0.000001000000000 + 1000000.000000000000000 - - + + 0 @@ -238,21 +266,52 @@ - 140 + 151 0 + + + 8 - 1000000000.000000000000000 + 100000.000000000000000 - 1000000.000000000000000 + 0.000000000000000 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Amount + + + spread_input From db80349359ed00dabcec799cb70fbbe5065c811e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 8 May 2018 15:29:06 +0300 Subject: [PATCH 120/187] Change worker_item.py to use db_worker shortcut methods --- dexbot/views/worker_item.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 76e758e79..5ca9e4a7f 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -39,13 +39,13 @@ def setup_ui_data(self, config): strategies = CreateWorkerController.get_strategies() self.set_worker_strategy(strategies[module]['name']) - profit = db_worker.execute(db_worker.get_item, worker_name, 'profit') + profit = db_worker.get_item(worker_name, 'profit') if profit: self.set_worker_profit(profit) else: self.set_worker_profit(0) - percentage = db_worker.execute(db_worker.get_item, worker_name, 'slider') + percentage = db_worker.get_item(worker_name, 'slider') if percentage: self.set_worker_slider(percentage) else: From db5dc51dce3f2dadb9883a88603477cb4d698b4c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 9 May 2018 09:42:11 +0300 Subject: [PATCH 121/187] Change default spread and increment in staggered orders --- dexbot/views/ui/forms/staggered_orders_widget.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index bd9574faf..1d11e24fc 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -84,7 +84,7 @@ 100000.000000000000000 - 5.000000000000000 + 6.000000000000000 @@ -140,7 +140,7 @@ 100000.000000000000000 - 2.500000000000000 + 4.000000000000000 From d04cd0092d74c56d806637f5aed2a0528cf3fb83 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 9 May 2018 13:10:48 +0300 Subject: [PATCH 122/187] Add orders table to storage The table is used to store made orders --- dexbot/basestrategy.py | 2 +- dexbot/storage.py | 85 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index ec03e2d4c..185db6572 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -303,11 +303,11 @@ def market_sell(self, amount, price): returnOrderId="head" ) - sell_order = self.get_order(sell_transaction['orderid']) self.log.info( 'Placed a sell order for {} {} @ {}'.format(amount, self.market["quote"]['symbol'], price)) + sell_order = self.get_order(sell_transaction['orderid']) return sell_order def purge(self): diff --git a/dexbot/storage.py b/dexbot/storage.py index fe8911041..66f901826 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -43,6 +43,20 @@ def __init__(self, c, k, v): self.value = v +class Orders(Base): + __tablename__ = 'orders' + + id = Column(Integer, primary_key=True) + worker = Column(String) + order_id = Column(String) + order = Column(String) + + def __init__(self, worker, order_id, order): + self.worker = worker + self.order_id = order_id + self.order = order + + class Storage(dict): """ Storage class @@ -70,10 +84,25 @@ def items(self): def clear(self): db_worker.clear(self.category) + def save_order(self, order): + order_id = order['id'] + db_worker.save_order(self.category, order_id, order) + + def remove_order(self, order): + order_id = order['id'] + db_worker.remove_order(self.category, order_id) + + def clear_orders(self): + db_worker.clear_orders(self.category) + + def fetch_orders(self, worker=None): + if not worker: + worker = self.category + return db_worker.fetch_orders(worker) + class DatabaseWorker(threading.Thread): - """ - Thread safe database worker + """ Thread safe database worker """ def __init__(self): @@ -196,6 +225,58 @@ def _clear(self, category): self.session.delete(row) self.session.commit() + def save_order(self, worker, order_id, order): + self.execute_noreturn(self._save_order, worker, order_id, order) + + def _save_order(self, worker, order_id, order): + value = json.dumps(order) + e = self.session.query(Orders).filter_by( + order_id=order_id + ).first() + if e: + e.value = value + else: + e = Orders(worker, order_id, value) + self.session.add(e) + self.session.commit() + + def remove_order(self, worker, order_id): + self.execute_noreturn(self._remove_order, worker, order_id) + + def _remove_order(self, worker, order_id): + e = self.session.query(Orders).filter_by( + worker=worker, + order_id=order_id + ).first() + self.session.delete(e) + self.session.commit() + + def clear_orders(self, worker): + self.execute_noreturn(self._clear_orders, worker) + + def _clear_orders(self, worker): + rows = self.session.query(Orders).filter_by( + worker=worker + ) + for row in rows: + self.session.delete(row) + self.session.commit() + + def fetch_orders(self, category): + return self.execute(self._fetch_orders, category) + + def _fetch_orders(self, worker, token): + results = self.session.query(Orders).filter_by( + worker=worker, + ).all() + if not results: + result = None + else: + result = {} + for row in results: + result[row.order_id] = json.loads(row.order) + self._set_result(token, result) + # Derive sqlite file directory data_dir = user_data_dir(appname, appauthor) From 66e36eb2bf331dbe9ff553e5d8e80479d7cc2d79 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 9 May 2018 13:26:33 +0300 Subject: [PATCH 123/187] Change staggered orders strategy logic This is still very much still a WIP --- dexbot/basestrategy.py | 1 + dexbot/strategies/staggered_orders.py | 107 +++++++++++++++++++------- 2 files changed, 79 insertions(+), 29 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 185db6572..f946fc6e6 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -314,6 +314,7 @@ def purge(self): """ Clear all the worker data from the database and cancel all orders """ self.cancel_all() + self.clear_orders() self.clear() @staticmethod diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7f5da1b29..700e0a081 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,8 +1,8 @@ +import math + from dexbot.basestrategy import BaseStrategy from dexbot.queue.idle_queue import idle_add -from bitshares.amount import Amount - class Strategy(BaseStrategy): """ Staggered Orders strategy @@ -22,8 +22,8 @@ def __init__(self, *args, **kwargs): self.worker_name = kwargs.get('name') self.view = kwargs.get('view') self.amount = self.worker['amount'] - self.spread = self.worker['spread'] - self.increment = self.worker['increment'] + self.spread = self.worker['spread'] / 100 + self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] @@ -35,53 +35,102 @@ def error(self, *args, **kwargs): self.log.info(self.execute()) def init_strategy(self): + # Make sure no orders remain + self.cancel_all() + self.clear_orders() + center_price = self.calculate_center_price() - buy_prices = [] - buy_price = center_price * (1 + self.spread / 2) - buy_prices.append(buy_price) + # Calculate buy prices + buy_prices = [] + buy_price = center_price / math.sqrt(1 + self.spread) while buy_price > self.lower_bound: - buy_price = buy_price / (1 + self.increment) buy_prices.append(buy_price) + buy_price = buy_price * (1 - self.increment) + # Calculate sell prices sell_prices = [] - sell_price = center_price * (1 - self.spread / 2) - sell_prices.append(sell_price) - + sell_price = center_price * math.sqrt(1 + self.spread) while sell_price < self.upper_bound: - sell_price = sell_price * (1 + self.increment) sell_prices.append(sell_price) + sell_price = sell_price * (1 + self.increment) - self['orders'] = [] - - def update_order(self, order, order_type): + # Calculate buy amounts + highest_buy_price = buy_prices.pop(0) + buy_orders = [{'amount': self.amount, 'price': highest_buy_price}] + for buy_price in buy_prices: + last_amount = buy_orders[-1]['amount'] + amount = last_amount / math.sqrt(1 + self.increment) + buy_orders.append({'amount': amount, 'price': buy_price}) + + # Calculate sell amounts + lowest_sell_price = highest_buy_price * math.sqrt(1 + self.spread + self.increment) + sell_orders = [{'amount': self.amount, 'price': lowest_sell_price}] + for sell_price in sell_prices: + last_amount = sell_orders[-1]['amount'] + amount = last_amount / math.sqrt(1 + self.increment) + sell_orders.append({'amount': amount, 'price': sell_price}) + + # Make sure there is enough balance for the buy orders + needed_buy_asset = 0 + for buy_order in buy_orders: + needed_buy_asset += buy_order['amount'] * buy_order['price'] + if self.balance(self.market["base"]) < needed_buy_asset: + self.log.critical( + "Insufficient buy balance, needed {} {}".format(needed_buy_asset, self.market['base']['symbol']) + ) + self.disabled = True + return + + # Make sure there is enough balance for the sell orders + needed_sell_asset = 0 + for sell_order in sell_orders: + needed_sell_asset += sell_order['amount'] + if self.balance(self.market["quote"]) < needed_sell_asset: + self.log.critical( + "Insufficient sell balance, needed {} {}".format(needed_sell_asset, self.market['quote']['symbol']) + ) + self.disabled = True + return + + # Place the buy orders + for buy_order in buy_orders: + order = self.market_buy(buy_order['amount'], buy_order['price']) + self.save_order(order) + + # Place the sell orders + for sell_order in sell_orders: + order = self.market_sell(sell_order['amount'], sell_order['price']) + self.save_order(order) + + self['setup_done'] = True + + def replace_order(self, order): self.log.info('Change detected, updating orders') - # Make sure - self.cancel(order) + self.remove_order(order) - if order_type == 'buy': + if order['base']['symbol'] == self.market['base']['symbol']: # Buy order amount = order['quote']['amount'] price = order['price'] * self.spread new_order = self.market_sell(amount, price) - else: + else: # Sell order amount = order['base']['amount'] price = order['price'] / self.spread new_order = self.market_buy(amount, price) - self['orders'] = new_order + self.save_order(new_order) def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ - for order in self['sell_orders']: - current_order = self.get_updated_order(order) - if current_order['quote']['amount'] != order['quote']['amount']: - self.update_order(order, 'sell') - - for order in self['buy_orders']: - current_order = self.get_updated_order(order) - if current_order['quote']['amount'] != order['quote']['amount']: - self.update_order(order, 'buy') + if not self['setup_done']: + self.init_strategy() + + orders = self.fetch_orders() + for order in orders: + current_order = self.get_order(order) + if not current_order: + self.replace_order(order) if self.view: self.update_gui_profit() From 7a05eb49d182f77d7bce7da86a2dbecf9cee4443 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 9 May 2018 14:41:52 +0300 Subject: [PATCH 124/187] Add exception on exception in worker.py --- dexbot/worker.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index b60ee65e1..5fa128f54 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -118,8 +118,11 @@ def on_block(self, data): try: self.workers[worker_name].ontick(data) except Exception as e: - self.workers[worker_name].error_ontick(e) - self.workers[worker_name].log.exception("in .tick()") + self.workers[worker_name].log.exception("in ontick()") + try: + self.workers[worker_name].error_ontick(e) + except Exception: + self.workers[worker_name].log.exception("in error_ontick()") self.config_lock.release() def on_market(self, data): @@ -135,8 +138,11 @@ def on_market(self, data): try: self.workers[worker_name].onMarketUpdate(data) except Exception as e: - self.workers[worker_name].error_onMarketUpdate(e) - self.workers[worker_name].log.exception(".onMarketUpdate()") + self.workers[worker_name].log.exception("in onMarketUpdate()") + try: + self.workers[worker_name].error_onMarketUpdate(e) + except Exception: + self.workers[worker_name].log.exception("in error_onMarketUpdate()") self.config_lock.release() def on_account(self, account_update): @@ -150,8 +156,11 @@ def on_account(self, account_update): try: self.workers[worker_name].onAccount(account_update) except Exception as e: - self.workers[worker_name].error_onAccount(e) - self.workers[worker_name].log.exception(".onAccountUpdate()") + self.workers[worker_name].log.exception("in onAccountUpdate()") + try: + self.workers[worker_name].error_onAccount(e) + except Exception: + self.workers[worker_name].log.exception("in error_onAccountUpdate()") self.config_lock.release() def add_worker(self, worker_name, config): @@ -209,5 +218,5 @@ def remove_offline_worker(config, worker_name): strategy.purge() def do_next_tick(self, job): - """Add a callable to be executed on the next tick""" + """ Add a callable to be executed on the next tick """ self.jobs.add(job) From ee9a533a1f97b13ce8bedcbd3aa47b1161bf5935 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 9 May 2018 14:42:25 +0300 Subject: [PATCH 125/187] Change dexbot version number to 0.1.23 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index d54ae927d..a86f91aaa 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.22' +VERSION = '0.1.23' AUTHOR = "codaone" __version__ = VERSION From 41bcf2754d4f3e9b0478e74f883e173ed3ce08d1 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 14 May 2018 10:45:57 +1000 Subject: [PATCH 126/187] add assertions to understand why some fields are str instead of int before division --- dexbot/basestrategy.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index e8458ae24..ca27494fb 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -218,7 +218,14 @@ def updated_open_orders(self): limit_orders = self.account['limit_orders'][:] for o in limit_orders: base_amount = o['for_sale'] - price = o['sell_price']['base']['amount'] / o['sell_price']['quote']['amount'] + assert type(base_amount) in [int, float], "o['for_sale'] not num {}".format(dict(o)) + assert type(o['sell_price']['base']['amount']) in [ + int, float], "o['sell_base']['base']['amount'] not num {}".format(dict(o)) + assert type(o['sell_price']['quote']['amount']) in [ + int, float], "o['sell_base']['quote']['amount'] not num {}".format(dict(o)) + + price = o['sell_price']['base']['amount'] / \ + o['sell_price']['quote']['amount'] quote_amount = base_amount / price o['sell_price']['base']['amount'] = base_amount o['sell_price']['quote']['amount'] = quote_amount From ae690f3b1f34fd62baa77a194a02aaff1b5823bd Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 14 May 2018 15:21:33 +1000 Subject: [PATCH 127/187] log buys/sells before action --- dexbot/basestrategy.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index ca27494fb..dbc52543b 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -320,33 +320,33 @@ def cancel_all(self): self.cancel(self.orders) def market_buy(self, amount, price): + self.log.info( + 'Placing a buy order for {} {} @ {}'.format(price * amount, + self.market["base"]['symbol'], + price)) buy_transaction = self.market.buy( price, Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, returnOrderId="head" ) - - self.log.info( - 'Placed a buy order for {} {} @ {}'.format(price * amount, - self.market["base"]['symbol'], - price)) + self.log.info('Placed buy order {}'.format(buy_transaction)) buy_order = self.get_order(buy_transaction['orderid']) return buy_order def market_sell(self, amount, price): + self.log.info( + 'Placing a sell order for {} {} @ {}'.format(amount, + self.market["quote"]['symbol'], + price)) sell_transaction = self.market.sell( price, Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, returnOrderId="head" ) - + self.log.info('Placed sell order {}'.format(sell_transaction)) sell_order = self.get_order(sell_transaction['orderid']) - self.log.info( - 'Placed a sell order for {} {} @ {}'.format(amount, - self.market["quote"]['symbol'], - price)) return sell_order def purge(self): From 2b4edff26a3462589428e29c7b658bae0bfb8dad Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 14 May 2018 09:00:45 +0300 Subject: [PATCH 128/187] Change relative orders order updating logic Orders now get updated when an order is fully filled --- dexbot/strategies/relative_orders.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 53950dfe4..59c96059a 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -127,14 +127,8 @@ def check_orders(self, *args, **kwargs): current_sell_order = self.get_updated_order(stored_sell_order) current_buy_order = self.get_updated_order(stored_buy_order) - # Update checks - sell_order_updated = not current_sell_order or \ - current_sell_order['quote']['amount'] != stored_sell_order['quote']['amount'] - buy_order_updated = not current_buy_order or \ - current_buy_order['base']['amount'] != stored_buy_order['base']['amount'] - - if sell_order_updated or buy_order_updated: - # Either buy or sell order was changed, update both orders + if not current_sell_order or not current_buy_order: + # Either buy or sell order is missing, update both orders self.update_orders() if self.view: From ba5e6eb97ce5d7343973a90c77791b0992f3057b Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 14 May 2018 16:04:02 +1000 Subject: [PATCH 129/187] soft failure on graphene errors --- dexbot/basestrategy.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index dbc52543b..202059ddf 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -1,10 +1,12 @@ import logging +import time from .storage import Storage from .statemachine import StateMachine from events import Events import bitsharesapi +import bitsharesapi.exceptions from bitshares.amount import Amount from bitshares.market import Market from bitshares.account import Account @@ -12,6 +14,9 @@ from bitshares.instance import shared_bitshares_instance +MAX_TRIES = 3 + + class BaseStrategy(Storage, StateMachine, Events): """ Base Strategy and methods available in all Sub Classes that inherit this BaseStrategy. @@ -289,7 +294,7 @@ def execute(self): def _cancel(self, orders): try: - self.bitshares.cancel(orders, account=self.account) + self.retry_action(self.bitshares.cancel, orders, account=self.account) except bitsharesapi.exceptions.UnhandledRPCError as e: if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': # The order(s) we tried to cancel doesn't exist @@ -324,7 +329,8 @@ def market_buy(self, amount, price): 'Placing a buy order for {} {} @ {}'.format(price * amount, self.market["base"]['symbol'], price)) - buy_transaction = self.market.buy( + buy_transaction = self.retry_action( + self.market.buy, price, Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, @@ -339,7 +345,8 @@ def market_sell(self, amount, price): 'Placing a sell order for {} {} @ {}'.format(amount, self.market["quote"]['symbol'], price)) - sell_transaction = self.market.sell( + sell_transaction = self.retry_action( + self.market.sell, price, Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, @@ -418,3 +425,26 @@ def orders_balance(self, order_ids, return_asset=False): base = Amount(base, base_asset) return {'quote': quote, 'base': base} + + def retry_action(self, action, *args, **kwargs): + """ + perform an action, and if certain suspected-to-be-spurious graphene bugs occur, + instead of bubbling the exception, it is quietly logged (level WARN), and try again + tries a fixed number of times (MAX_TRIES) before failing + """ + tries = 0 + while True: + try: + return action(*args, **kwargs) + except bitsharesapi.exceptions.UnhandledRPCError as e: + if "Assert Exception: amount_to_sell.amount > 0" in str(e): + if tries > MAX_TRIES: + raise + else: + tries += 1 + self.log.warn("ignoring: '{}'".format(str(e))) + self.bitshares.txbuffer.clear() + self.account.refresh() + time.sleep(2) + else: + raise From ef71d8a0c2abd4ffa2b105c2f82477173b098893 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 14 May 2018 09:19:53 +0300 Subject: [PATCH 130/187] Change dexbot version number to 0.1.24 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a86f91aaa..baffb42a4 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.23' +VERSION = '0.1.24' AUTHOR = "codaone" __version__ = VERSION From ba61e2f90bfae2b723d32a1800536b1bab5276ae Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 14 May 2018 09:45:39 +0300 Subject: [PATCH 131/187] Change few small things in basestrategy --- dexbot/basestrategy.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 202059ddf..dbd97698d 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -229,8 +229,7 @@ def updated_open_orders(self): assert type(o['sell_price']['quote']['amount']) in [ int, float], "o['sell_base']['quote']['amount'] not num {}".format(dict(o)) - price = o['sell_price']['base']['amount'] / \ - o['sell_price']['quote']['amount'] + price = o['sell_price']['base']['amount'] / o['sell_price']['quote']['amount'] quote_amount = base_amount / price o['sell_price']['base']['amount'] = base_amount o['sell_price']['quote']['amount'] = quote_amount @@ -428,7 +427,7 @@ def orders_balance(self, order_ids, return_asset=False): def retry_action(self, action, *args, **kwargs): """ - perform an action, and if certain suspected-to-be-spurious graphene bugs occur, + Perform an action, and if certain suspected-to-be-spurious graphene bugs occur, instead of bubbling the exception, it is quietly logged (level WARN), and try again tries a fixed number of times (MAX_TRIES) before failing """ @@ -442,7 +441,7 @@ def retry_action(self, action, *args, **kwargs): raise else: tries += 1 - self.log.warn("ignoring: '{}'".format(str(e))) + self.log.warning("Ignoring: '{}'".format(str(e))) self.bitshares.txbuffer.clear() self.account.refresh() time.sleep(2) From a1d4435309fa663f2e91460017e65a1578c5a2cf Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 14 May 2018 09:47:45 +0300 Subject: [PATCH 132/187] Change dexbot version number to 0.1.25 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index baffb42a4..894716492 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.24' +VERSION = '0.1.25' AUTHOR = "codaone" __version__ = VERSION From 344a188512c5bd549aeb1ae34525876c6f0fd6ae Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 14 May 2018 16:51:05 +1000 Subject: [PATCH 133/187] log the version and platform --- dexbot/controllers/main_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index f57a5e9b8..64f27b73c 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,6 +1,7 @@ import logging +import sys -from dexbot import config_file +from dexbot import config_file, VERSION from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler @@ -27,6 +28,8 @@ def __init__(self, bitshares_instance): pyqth = PyQtHandler() pyqth.setLevel(logging.ERROR) logger.addHandler(pyqth) + logger.info("DEXBot {} on python {} {}".format(VERSION, sys.version[:6], sys.platform), extra={ + 'worker_name': 'NONE', 'account': 'NONE', 'market': 'NONE'}) def create_worker(self, worker_name, config, view): # Todo: Add some threading here so that the GUI doesn't freeze From f3c6610911f98bb422c3b91643a30c6b8aebcf27 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 14 May 2018 16:51:58 +1000 Subject: [PATCH 134/187] warnING --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 202059ddf..1b0dc862c 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -442,7 +442,7 @@ def retry_action(self, action, *args, **kwargs): raise else: tries += 1 - self.log.warn("ignoring: '{}'".format(str(e))) + self.log.warning("ignoring: '{}'".format(str(e))) self.bitshares.txbuffer.clear() self.account.refresh() time.sleep(2) From 4e85da200a034198ddab30bcfc94927709eebd8a Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 14 May 2018 09:59:00 +0300 Subject: [PATCH 135/187] Moved tests folder to the project root and made some small changes to the file --- {dexbot/tests => tests}/test.py | 39 +++++++++++---------------------- 1 file changed, 13 insertions(+), 26 deletions(-) rename {dexbot/tests => tests}/test.py (53%) diff --git a/dexbot/tests/test.py b/tests/test.py similarity index 53% rename from dexbot/tests/test.py rename to tests/test.py index 4e9d177ca..a4c0a88e9 100755 --- a/dexbot/tests/test.py +++ b/tests/test.py @@ -1,13 +1,13 @@ #!/usr/bin/python3 - -from bitshares.bitshares import BitShares +import threading import unittest +import logging import time import os -import threading -import logging -from dexbot.bot import BotInfrastructure +from dexbot.worker import WorkerInfrastructure + +from bitshares.bitshares import BitShares logging.basicConfig( level=logging.INFO, @@ -23,24 +23,11 @@ 'account': 'aud.bot.test4', 'market': 'TESTUSD:TEST', 'module': 'dexbot.strategies.echo' - }, - 'follow_orders': - { - 'account': 'aud.bot.test4', - 'market': 'TESTUSD:TEST', - 'module': 'dexbot.strategies.follow_orders', - 'spread': 5, - 'reset': True, - 'staggers': 2, - 'wall_percent': 5, - 'staggerspread': 5, - 'min': 0, - 'max': 100000, - 'start': 50, - 'bias': 1 - }}} + } + } +} -# user need sto put a key in +# User needs to put a key in KEYS = [os.environ['DEXBOT_TEST_WIF']] @@ -48,16 +35,16 @@ class TestDexbot(unittest.TestCase): def test_dexbot(self): bitshares_instance = BitShares(node=TEST_CONFIG['node'], keys=KEYS) - bot_infrastructure = BotInfrastructure(config=TEST_CONFIG, - bitshares_instance=bitshares_instance) + worker_infrastructure = WorkerInfrastructure(config=TEST_CONFIG, + bitshares_instance=bitshares_instance) def wait_then_stop(): time.sleep(20) - bot_infrastructure.do_next_tick(bot_infrastructure.stop) + worker_infrastructure.do_next_tick(worker_infrastructure.stop) stopper = threading.Thread(target=wait_then_stop) stopper.start() - bot_infrastructure.run() + worker_infrastructure.run() stopper.join() From 96dfcecbe0fb03ba80f6adf515a4308e7945fead Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 14 May 2018 10:49:23 +0300 Subject: [PATCH 136/187] Change dexbot version number to 0.1.26 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index baffb42a4..864d36c2f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.24' +VERSION = '0.1.26' AUTHOR = "codaone" __version__ = VERSION From 90204c06bb13d64bc9d59ab2edd7e485bb799823 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 14 May 2018 13:56:44 +0300 Subject: [PATCH 137/187] Fix error dialog parameter passing --- dexbot/views/create_wallet.py | 11 +++++------ dexbot/views/create_worker.py | 2 +- dexbot/views/edit_worker.py | 2 +- dexbot/views/errors.py | 6 +++--- dexbot/views/notice.py | 2 +- dexbot/views/unlock_wallet.py | 11 +++++------ dexbot/views/worker_item.py | 8 ++++---- dexbot/views/worker_list.py | 2 +- 8 files changed, 21 insertions(+), 23 deletions(-) diff --git a/dexbot/views/create_wallet.py b/dexbot/views/create_wallet.py index a4016576c..7ae192837 100644 --- a/dexbot/views/create_wallet.py +++ b/dexbot/views/create_wallet.py @@ -5,19 +5,18 @@ from PyQt5 import QtWidgets -class CreateWalletView(QtWidgets.QDialog): +class CreateWalletView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, controller): self.controller = controller super().__init__() - self.ui = Ui_Dialog() - self.ui.setupUi(self) - self.ui.ok_button.clicked.connect(self.validate_form) + self.setupUi(self) + self.ok_button.clicked.connect(lambda: self.validate_form()) @gui_error def validate_form(self): - password = self.ui.password_input.text() - confirm_password = self.ui.confirm_password_input.text() + password = self.password_input.text() + confirm_password = self.confirm_password_input.text() if not self.controller.create_wallet(password, confirm_password): dialog = NoticeDialog('Passwords do not match!') dialog.exec_() diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 687618ce6..2a50b2b45 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -27,7 +27,7 @@ def __init__(self, controller): # Set signals self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form(self)) self.save_button.clicked.connect(lambda: controller.handle_save(self)) - self.cancel_button.clicked.connect(self.reject) + self.cancel_button.clicked.connect(lambda: self.reject()) self.controller.change_strategy_form(self) self.worker_data = {} diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 877a1f0e7..b583f4462 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -35,7 +35,7 @@ def __init__(self, controller, worker_name, config): # Set signals self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form(self)) self.save_button.clicked.connect(lambda: self.controller.handle_save(self)) - self.cancel_button.clicked.connect(self.reject) + self.cancel_button.clicked.connect(lambda: self.reject()) self.controller.change_strategy_form(self, worker_data) self.worker_data = {} diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index 513098dfc..8b0cd3edf 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -32,11 +32,11 @@ def emit(self, record): def gui_error(func): - """A decorator for GUI handler functions - traps all exceptions and displays the dialog + """ A decorator for GUI handler functions - traps all exceptions and displays the dialog """ - def func_wrapper(obj, *args, **kwargs): + def func_wrapper(*args, **kwargs): try: - return func(obj) + return func(*args, **kwargs) except BaseException as exc: show_dialog("DEXBot Error", "An error occurred with DEXBot: \n"+repr(exc), None, traceback.format_exc()) diff --git a/dexbot/views/notice.py b/dexbot/views/notice.py index b04fa1d02..8e48a7a39 100644 --- a/dexbot/views/notice.py +++ b/dexbot/views/notice.py @@ -11,4 +11,4 @@ def __init__(self, text): self.ui.setupUi(self) self.ui.notice_label.setText(text) - self.ui.ok_button.clicked.connect(self.accept) + self.ui.ok_button.clicked.connect(lambda: self.accept()) diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py index 67aca4dd4..4eb892691 100644 --- a/dexbot/views/unlock_wallet.py +++ b/dexbot/views/unlock_wallet.py @@ -5,21 +5,20 @@ from PyQt5 import QtWidgets -class UnlockWalletView(QtWidgets.QDialog): +class UnlockWalletView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, controller): self.controller = controller super().__init__() - self.ui = Ui_Dialog() - self.ui.setupUi(self) - self.ui.ok_button.clicked.connect(self.validate_form) + self.setupUi(self) + self.ok_button.clicked.connect(lambda: self.validate_form()) @gui_error def validate_form(self): - password = self.ui.password_input.text() + password = self.password_input.text() if not self.controller.unlock_wallet(password): dialog = NoticeDialog('Invalid password!') dialog.exec_() - self.ui.password_input.setText('') + self.password_input.setText('') else: self.accept() diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 4fecf4746..3f25b4bee 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -23,10 +23,10 @@ def __init__(self, worker_name, config, main_ctrl, view): self.setupUi(self) self.pause_button.hide() - self.pause_button.clicked.connect(self.pause_worker) - self.play_button.clicked.connect(self.start_worker) - self.remove_button.clicked.connect(self.remove_widget_dialog) - self.edit_button.clicked.connect(self.handle_edit_worker) + self.pause_button.clicked.connect(lambda: self.pause_worker()) + self.play_button.clicked.connect(lambda: self.start_worker()) + self.remove_button.clicked.connect(lambda: self.remove_widget_dialog()) + self.edit_button.clicked.connect(lambda: self.handle_edit_worker()) self.setup_ui_data(config) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 13379c614..9be79d688 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -29,7 +29,7 @@ def __init__(self, main_ctrl): self.statusbar_updater = None self.statusbar_updater_first_run = True - self.ui.add_worker_button.clicked.connect(self.handle_add_worker) + self.ui.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) # Load worker widgets from config file workers = main_ctrl.get_workers_data() From ca0fa343327587a811e82be44da386b687f98021 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 16 May 2018 08:34:08 +0300 Subject: [PATCH 138/187] Change createWorkerController creation logic The controller is now created in EditWorkerView and CreateWorkerView for more control --- .../controllers/create_worker_controller.py | 61 ++++++++++--------- dexbot/views/create_worker.py | 13 ++-- dexbot/views/edit_worker.py | 15 +++-- dexbot/views/worker_item.py | 4 +- dexbot/views/worker_list.py | 4 +- 5 files changed, 48 insertions(+), 49 deletions(-) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 85e574dd1..4c4ede087 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -15,7 +15,8 @@ class CreateWorkerController: - def __init__(self, bitshares_instance, mode): + def __init__(self, view, bitshares_instance, mode): + self.view = view self.bitshares = bitshares_instance or shared_bitshares_instance() self.mode = mode @@ -36,7 +37,7 @@ def strategies(self): def get_strategies(): """ Static method for getting the strategies """ - controller = CreateWorkerController(None, None) + controller = CreateWorkerController(None, None, None) return controller.strategies @property @@ -143,19 +144,19 @@ def handle_save_dialog(): return dialog.exec_() @gui_error - def change_strategy_form(self, ui, worker_data=None): + def change_strategy_form(self, worker_data=None): # Make sure the container is empty - for index in reversed(range(ui.strategy_container.count())): - ui.strategy_container.itemAt(index).widget().setParent(None) + for index in reversed(range(self.view.strategy_container.count())): + self.view.strategy_container.itemAt(index).widget().setParent(None) - strategy_module = ui.strategy_input.currentData() - ui.strategy_widget = StrategyFormWidget(self, strategy_module, worker_data) - ui.strategy_container.addWidget(ui.strategy_widget) + strategy_module = self.view.strategy_input.currentData() + self.view.strategy_widget = StrategyFormWidget(self, strategy_module, worker_data) + self.view.strategy_container.addWidget(self.view.strategy_widget) # Resize the dialog to be minimum possible height - width = ui.geometry().width() - ui.setMinimumSize(width, 0) - ui.resize(width, 1) + width = self.view.geometry().width() + self.view.setMinimumSize(width, 0) + self.view.resize(width, 1) def validate_worker_name(self, worker_name, old_worker_name=None): if self.mode == 'add': @@ -181,11 +182,11 @@ def validate_account_not_in_use(self, account): return not self.is_account_in_use(account) @gui_error - def validate_form(self, ui): + def validate_form(self): error_text = '' - base_asset = ui.base_asset_input.currentText() - quote_asset = ui.quote_asset_input.text() - worker_name = ui.worker_name_input.text() + base_asset = self.view.base_asset_input.currentText() + quote_asset = self.view.quote_asset_input.text() + worker_name = self.view.worker_name_input.text() if not self.validate_asset(base_asset): error_text += 'Field "Base Asset" does not have a valid asset.\n' @@ -194,8 +195,8 @@ def validate_form(self, ui): if not self.validate_market(base_asset, quote_asset): error_text += "Market {}/{} doesn't exist.\n".format(base_asset, quote_asset) if self.mode == 'add': - account = ui.account_input.text() - private_key = ui.private_key_input.text() + account = self.view.account_input.text() + private_key = self.view.private_key_input.text() if not self.validate_worker_name(worker_name): error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) if not self.validate_account_name(account): @@ -205,7 +206,7 @@ def validate_form(self, ui): if not self.validate_account_not_in_use(account): error_text += 'Use a different account. "{}" is already in use.\n'.format(account) elif self.mode == 'edit': - if not self.validate_worker_name(worker_name, ui.worker_name): + if not self.validate_worker_name(worker_name, self.view.worker_name): error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) error_text = error_text.rstrip() # Remove the extra line-ending @@ -217,28 +218,28 @@ def validate_form(self, ui): return True @gui_error - def handle_save(self, ui): - if not self.validate_form(ui): + def handle_save(self): + if not self.validate_form(): return if self.mode == 'add': # Add the private key to the database - private_key = ui.private_key_input.text() + private_key = self.view.private_key_input.text() self.add_private_key(private_key) - account = ui.account_input.text() + account = self.view.account_input.text() else: - account = ui.account_name.text() + account = self.view.account_name.text() - base_asset = ui.base_asset_input.currentText() - quote_asset = ui.quote_asset_input.text() - strategy_module = ui.strategy_input.currentData() + base_asset = self.view.base_asset_input.currentText() + quote_asset = self.view.quote_asset_input.text() + strategy_module = self.view.strategy_input.currentData() - ui.worker_data = { + self.view.worker_data = { 'account': account, 'market': '{}/{}'.format(quote_asset, base_asset), 'module': strategy_module, - **ui.strategy_widget.values + **self.view.strategy_widget.values } - ui.worker_name = ui.worker_name_input.text() - ui.accept() + self.view.worker_name = self.view.worker_name_input.text() + self.view.accept() diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 2a50b2b45..fa5f6d40c 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -1,15 +1,16 @@ from .ui.create_worker_window_ui import Ui_Dialog -from .errors import gui_error +from dexbot.controllers.create_worker_controller import CreateWorkerController from PyQt5 import QtWidgets class CreateWorkerView(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, controller): + def __init__(self, bitshares_instance): super().__init__() - self.controller = controller self.strategy_widget = None + controller = CreateWorkerController(self, bitshares_instance, 'add') + self.controller = controller self.setupUi(self) @@ -25,9 +26,9 @@ def __init__(self, controller): self.worker_name_input.setText(self.worker_name) # Set signals - self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form(self)) - self.save_button.clicked.connect(lambda: controller.handle_save(self)) + self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form()) + self.save_button.clicked.connect(lambda: controller.handle_save()) self.cancel_button.clicked.connect(lambda: self.reject()) - self.controller.change_strategy_form(self) + self.controller.change_strategy_form() self.worker_data = {} diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index b583f4462..0df431576 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -1,18 +1,17 @@ from .ui.edit_worker_window_ui import Ui_Dialog -from .confirmation import ConfirmationDialog -from .notice import NoticeDialog -from .errors import gui_error +from dexbot.controllers.create_worker_controller import CreateWorkerController from PyQt5 import QtWidgets class EditWorkerView(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, controller, worker_name, config): + def __init__(self, bitshares_instance, worker_name, config): super().__init__() - self.controller = controller self.worker_name = worker_name self.strategy_widget = None + controller = CreateWorkerController(self, bitshares_instance, 'edit') + self.controller = controller self.setupUi(self) worker_data = config['workers'][worker_name] @@ -33,9 +32,9 @@ def __init__(self, controller, worker_name, config): self.account_name.setText(self.controller.get_account(worker_data)) # Set signals - self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form(self)) - self.save_button.clicked.connect(lambda: self.controller.handle_save(self)) + self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form()) + self.save_button.clicked.connect(lambda: self.controller.handle_save()) self.cancel_button.clicked.connect(lambda: self.reject()) - self.controller.change_strategy_form(self, worker_data) + self.controller.change_strategy_form(worker_data) self.worker_data = {} diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 3f25b4bee..54d2d8ca0 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -117,8 +117,8 @@ def reload_widget(self, worker_name): @gui_error def handle_edit_worker(self): - controller = CreateWorkerController(self.main_ctrl.bitshares_instance, 'edit') - edit_worker_dialog = EditWorkerView(controller, self.worker_name, self.worker_config) + edit_worker_dialog = EditWorkerView(self.main_ctrl.bitshares_instance, + self.worker_name, self.worker_config) return_value = edit_worker_dialog.exec_() # User clicked save diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 9be79d688..3fe957a91 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -5,7 +5,6 @@ from .ui.worker_list_window_ui import Ui_MainWindow from .create_worker import CreateWorkerView from .worker_item import WorkerItemWidget -from dexbot.controllers.create_worker_controller import CreateWorkerController from dexbot.queue.queue_dispatcher import ThreadDispatcher from dexbot.queue.idle_queue import idle_add from .errors import gui_error @@ -72,8 +71,7 @@ def remove_worker_widget(self, worker_name): @gui_error def handle_add_worker(self): - controller = CreateWorkerController(self.main_ctrl.bitshares_instance, 'add') - create_worker_dialog = CreateWorkerView(controller) + create_worker_dialog = CreateWorkerView(self.main_ctrl.bitshares_instance) return_value = create_worker_dialog.exec_() # User clicked save From 96b3f1dd4127b677a562330f8f08d0fd55e2a7ae Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 16 May 2018 12:05:54 +0300 Subject: [PATCH 139/187] Add asset requirement display to worker creator/editor The logic still has some WIP stuff in it --- dexbot/controllers/strategy_controller.py | 151 ++++++++++++++++++ dexbot/strategies/staggered_orders.py | 113 +++++++++++-- dexbot/views/strategy_form.py | 105 ++---------- .../views/ui/forms/staggered_orders_widget.ui | 18 +-- 4 files changed, 273 insertions(+), 114 deletions(-) create mode 100644 dexbot/controllers/strategy_controller.py diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py new file mode 100644 index 000000000..e451ef9bf --- /dev/null +++ b/dexbot/controllers/strategy_controller.py @@ -0,0 +1,151 @@ +from dexbot.queue.idle_queue import idle_add +from dexbot.views.errors import gui_error +from dexbot.strategies.staggered_orders import Strategy as StaggeredOrdersStrategy + +from bitshares.market import Market +from bitshares.asset import AssetDoesNotExistsException + + +class RelativeOrdersController: + + def __init__(self, view, worker_controller, worker_data): + self.view = view + self.worker_controller = worker_controller + self.view.strategy_widget.relative_order_size_checkbox.toggled.connect( + self.onchange_relative_order_size_checkbox + ) + + if worker_data: + self.set_config_values(worker_data) + + @gui_error + def onchange_relative_order_size_checkbox(self, checked): + if checked: + self.order_size_input_to_relative() + else: + self.order_size_input_to_static() + + @gui_error + def order_size_input_to_relative(self): + self.view.strategy_widget.amount_input.setSuffix('%') + self.view.strategy_widget.amount_input.setDecimals(2) + self.view.strategy_widget.amount_input.setMaximum(100.00) + self.view.strategy_widget.amount_input.setMinimumWidth(151) + self.view.strategy_widget.amount_input.setValue(10.00) + + @gui_error + def order_size_input_to_static(self): + self.view.strategy_widget.amount_input.setSuffix('') + self.view.strategy_widget.amount_input.setDecimals(8) + self.view.strategy_widget.amount_input.setMaximum(1000000000.000000) + self.view.strategy_widget.amount_input.setValue(0.000000) + + @gui_error + def set_config_values(self, worker_data): + if worker_data.get('amount_relative', False): + self.order_size_input_to_relative() + self.view.strategy_widget.relative_order_size_checkbox.setChecked(True) + else: + self.order_size_input_to_static() + self.view.strategy_widget.relative_order_size_checkbox.setChecked(False) + + self.view.strategy_widget.amount_input.setValue(float(worker_data.get('amount', 0))) + self.view.strategy_widget.center_price_input.setValue(worker_data.get('center_price', 0)) + self.view.strategy_widget.spread_input.setValue(worker_data.get('spread', 5)) + + if worker_data.get('center_price_dynamic', True): + self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(True) + else: + self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(False) + + @property + def values(self): + data = { + 'amount': self.view.strategy_widget.amount_input.value(), + 'amount_relative': self.view.strategy_widget.relative_order_size_checkbox.isChecked(), + 'center_price': self.view.strategy_widget.center_price_input.value(), + 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), + 'spread': self.view.strategy_widget.spread_input.value() + } + return data + + +class StaggeredOrdersController: + + def __init__(self, view, worker_controller, worker_data): + self.view = view + self.worker_controller = worker_controller + + if worker_data: + self.set_config_values(worker_data) + + worker_controller.view.base_asset_input.editTextChanged.connect(lambda: self.on_value_change()) + worker_controller.view.quote_asset_input.textChanged.connect(lambda: self.on_value_change()) + widget = self.view.strategy_widget + widget.amount_input.valueChanged.connect(lambda: self.on_value_change()) + widget.spread_input.valueChanged.connect(lambda: self.on_value_change()) + widget.increment_input.valueChanged.connect(lambda: self.on_value_change()) + widget.lower_bound_input.valueChanged.connect(lambda: self.on_value_change()) + widget.upper_bound_input.valueChanged.connect(lambda: self.on_value_change()) + self.on_value_change() + + @gui_error + def set_config_values(self, worker_data): + widget = self.view.strategy_widget + widget.amount_input.setValue(worker_data.get('amount', 0)) + widget.increment_input.setValue(worker_data.get('increment', 4)) + widget.spread_input.setValue(worker_data.get('spread', 6)) + widget.lower_bound_input.setValue(worker_data.get('lower_bound', 0.000001)) + widget.upper_bound_input.setValue(worker_data.get('upper_bound', 1000000)) + + @gui_error + def on_value_change(self): + base_asset = self.worker_controller.view.base_asset_input.currentText() + quote_asset = self.worker_controller.view.quote_asset_input.text() + try: + market = Market('{}:{}'.format(quote_asset, base_asset)) + except AssetDoesNotExistsException: + idle_add(self.set_required_base, 'N/A') + idle_add(self.set_required_quote, 'N/A') + return + + amount = self.view.strategy_widget.amount_input.value() + spread = self.view.strategy_widget.spread_input.value() / 100 + increment = self.view.strategy_widget.increment_input.value() / 100 + lower_bound = self.view.strategy_widget.lower_bound_input.value() + upper_bound = self.view.strategy_widget.upper_bound_input.value() + + if not (market or amount or spread or increment or lower_bound or upper_bound): + idle_add(self.set_required_base, 'N/A') + idle_add(self.set_required_quote, 'N/A') + return + + strategy = StaggeredOrdersStrategy + result = strategy.get_required_assets(market, amount, spread, increment, lower_bound, upper_bound) + if not result: + idle_add(self.set_required_base, 'N/A') + idle_add(self.set_required_quote, 'N/A') + return + + base, quote = result + text = '{:.8f} {}'.format(base, base_asset) + idle_add(self.set_required_base, text) + text = '{:.8f} {}'.format(quote, quote_asset) + idle_add(self.set_required_quote, text) + + def set_required_base(self, text): + self.view.strategy_widget.required_base_text.setText(text) + + def set_required_quote(self, text): + self.view.strategy_widget.required_quote_text.setText(text) + + @property + def values(self): + data = { + 'amount': self.view.strategy_widget.amount_input.value(), + 'spread': self.view.strategy_widget.spread_input.value(), + 'increment': self.view.strategy_widget.increment_input.value(), + 'lower_bound': self.view.strategy_widget.lower_bound_input.value(), + 'upper_bound': self.view.strategy_widget.upper_bound_input.value() + } + return data diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 700e0a081..a5132eec1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -40,20 +40,16 @@ def init_strategy(self): self.clear_orders() center_price = self.calculate_center_price() + spread = self.spread + increment = self.increment + lower_bound = self.lower_bound + upper_bound = self.upper_bound # Calculate buy prices - buy_prices = [] - buy_price = center_price / math.sqrt(1 + self.spread) - while buy_price > self.lower_bound: - buy_prices.append(buy_price) - buy_price = buy_price * (1 - self.increment) + buy_prices = self.calculate_buy_prices(center_price, spread, increment, lower_bound) # Calculate sell prices - sell_prices = [] - sell_price = center_price * math.sqrt(1 + self.spread) - while sell_price < self.upper_bound: - sell_prices.append(sell_price) - sell_price = sell_price * (1 + self.increment) + sell_prices = self.calculate_sell_prices(center_price, spread, increment, upper_bound) # Calculate buy amounts highest_buy_price = buy_prices.pop(0) @@ -136,9 +132,104 @@ def check_orders(self, *args, **kwargs): self.update_gui_profit() self.update_gui_slider() + @staticmethod + def calculate_buy_prices(center_price, spread, increment, lower_bound): + buy_prices = [] + if lower_bound > center_price / math.sqrt(1 + spread): + return buy_prices + + buy_price = center_price / math.sqrt(1 + spread) + while buy_price > lower_bound: + buy_prices.append(buy_price) + buy_price = buy_price * (1 - increment) + return buy_prices + + @staticmethod + def calculate_sell_prices(center_price, spread, increment, upper_bound): + sell_prices = [] + if upper_bound < center_price * math.sqrt(1 + spread): + return sell_prices + + sell_price = center_price * math.sqrt(1 + spread) + while sell_price < upper_bound: + sell_prices.append(sell_price) + sell_price = sell_price * (1 + increment) + return sell_prices + + @staticmethod + def calculate_amounts(buy_prices, sell_prices, amount, spread, increment): + # Calculate buy amounts + buy_orders = [] + if buy_prices: + highest_buy_price = buy_prices.pop(0) + buy_orders.append({'amount': amount, 'price': highest_buy_price}) + for buy_price in buy_prices: + last_amount = buy_orders[-1]['amount'] + current_amount = last_amount / math.sqrt(1 + increment) + buy_orders.append({'amount': current_amount, 'price': buy_price}) + + # Calculate sell amounts + sell_orders = [] + if sell_prices: + lowest_sell_price = sell_prices.pop(0) + # amount = highest_buy_price * math.sqrt(1 + spread + increment) ? + sell_orders.append({'amount': amount, 'price': lowest_sell_price}) + for sell_price in sell_prices: + last_amount = sell_orders[-1]['amount'] + current_amount = last_amount / math.sqrt(1 + increment) + sell_orders.append({'amount': current_amount, 'price': sell_price}) + + return [buy_orders, sell_orders] + + @staticmethod + def get_required_assets(market, amount, spread, increment, lower_bound, upper_bound): + if not lower_bound or not increment: + return None + + ticker = market.ticker() + highest_bid = ticker.get("highestBid") + lowest_ask = ticker.get("lowestAsk") + if not float(highest_bid): + return None + elif not float(lowest_ask): + return None + else: + center_price = (highest_bid['price'] + lowest_ask['price']) / 2 + + # Calculate buy prices + buy_prices = Strategy.calculate_buy_prices(center_price, spread, increment, lower_bound) + + # Calculate sell prices + sell_prices = Strategy.calculate_sell_prices(center_price, spread, increment, upper_bound) + + # Calculate buy and sell amounts + buy_orders, sell_orders = Strategy.calculate_amounts( + buy_prices, sell_prices, amount, spread, increment + ) + + needed_buy_asset = 0 + for buy_order in buy_orders: + needed_buy_asset += buy_order['amount'] + + needed_sell_asset = 0 + for sell_order in sell_orders: + needed_sell_asset += sell_order['amount'] + + return [needed_buy_asset, needed_sell_asset] + # GUI updaters def update_gui_profit(self): pass def update_gui_slider(self): - pass + ticker = self.market.ticker() + latest_price = ticker.get('latest').get('price') + total_balance = self.total_balance() + total = (total_balance['quote'] * latest_price) + total_balance['base'] + + if not total: # Prevent division by zero + percentage = 50 + else: + percentage = (total_balance['base'] / total) * 100 + idle_add(self.view.set_worker_slider, self.worker_name, percentage) + self['slider'] = percentage diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 3a765fba4..e1cd7adbc 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -1,6 +1,6 @@ import importlib -from dexbot.views.errors import gui_error +import dexbot.controllers.strategy_controller from PyQt5 import QtWidgets @@ -20,104 +20,21 @@ def __init__(self, controller, strategy_module, config=None): self.strategy_widget = widget() self.strategy_widget.setupUi(self) - # Call methods based on the selected strategy + # Invoke the correct controller + class_name = '' if self.module_name == 'relative_orders': - self.strategy_widget.relative_order_size_checkbox.toggled.connect( - self.onchange_relative_order_size_checkbox) - if config: - self.set_relative_orders_values(config) + class_name = 'RelativeOrdersController' elif self.module_name == 'staggered_orders': - if config: - self.set_staggered_orders_values(config) + class_name = 'StaggeredOrdersController' - @gui_error - def onchange_relative_order_size_checkbox(self, checked): - if checked: - self.order_size_input_to_relative() - else: - self.order_size_input_to_static() - - @gui_error - def order_size_input_to_relative(self): - self.strategy_widget.amount_input.setSuffix('%') - self.strategy_widget.amount_input.setDecimals(2) - self.strategy_widget.amount_input.setMaximum(100.00) - self.strategy_widget.amount_input.setMinimumWidth(151) - self.strategy_widget.amount_input.setValue(10.00) - - @gui_error - def order_size_input_to_static(self): - self.strategy_widget.amount_input.setSuffix('') - self.strategy_widget.amount_input.setDecimals(8) - self.strategy_widget.amount_input.setMaximum(1000000000.000000) - self.strategy_widget.amount_input.setValue(0.000000) + strategy_controller = getattr( + dexbot.controllers.strategy_controller, + class_name + ) + self.strategy_controller = strategy_controller(self, controller, config) @property def values(self): """ Returns values all the form values based on selected strategy """ - if self.module_name == 'relative_orders': - return self.relative_orders_values - elif self.module_name == 'staggered_orders': - return self.staggered_orders_values - - @gui_error - def set_relative_orders_values(self, worker_data): - if worker_data.get('amount_relative', False): - self.order_size_input_to_relative() - self.strategy_widget.relative_order_size_checkbox.setChecked(True) - else: - self.order_size_input_to_static() - self.strategy_widget.relative_order_size_checkbox.setChecked(False) - - self.strategy_widget.amount_input.setValue(float(worker_data.get('amount', 0))) - self.strategy_widget.center_price_input.setValue(worker_data.get('center_price', 0)) - self.strategy_widget.spread_input.setValue(worker_data.get('spread', 5)) - - if worker_data.get('center_price_dynamic', True): - self.strategy_widget.center_price_dynamic_checkbox.setChecked(True) - else: - self.strategy_widget.center_price_dynamic_checkbox.setChecked(False) - - @gui_error - def set_staggered_orders_values(self, worker_data): - self.strategy_widget.amount_input.setValue(worker_data.get('amount', 0)) - self.strategy_widget.increment_input.setValue(worker_data.get('increment', 2.5)) - self.strategy_widget.spread_input.setValue(worker_data.get('spread', 5)) - self.strategy_widget.lower_bound_input.setValue(worker_data.get('lower_bound', 0.000001)) - self.strategy_widget.upper_bound_input.setValue(worker_data.get('upper_bound', 1000000)) - - @property - def relative_orders_values(self): - # Remove the percentage character from the end - spread = float(self.strategy_widget.spread_input.text()[:-1]) - - # If order size is relative, remove percentage character from the end - if self.strategy_widget.relative_order_size_checkbox.isChecked(): - amount = float(self.strategy_widget.amount_input.text()[:-1]) - else: - amount = self.strategy_widget.amount_input.text() - - data = { - 'amount': amount, - 'amount_relative': bool(self.strategy_widget.relative_order_size_checkbox.isChecked()), - 'center_price': float(self.strategy_widget.center_price_input.text()), - 'center_price_dynamic': bool(self.strategy_widget.center_price_dynamic_checkbox.isChecked()), - 'spread': spread - } - return data - - @property - def staggered_orders_values(self): - # Remove the percentage character from the end - spread = float(self.strategy_widget.spread_input.text()[:-1]) - increment = float(self.strategy_widget.increment_input.text()[:-1]) - - data = { - 'amount': float(self.strategy_widget.amount_input.text()), - 'spread': spread, - 'increment': increment, - 'lower_bound': float(self.strategy_widget.lower_bound_input.text()), - 'upper_bound': float(self.strategy_widget.upper_bound_input.text()) - } - return data + return self.strategy_controller.values diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 1d11e24fc..33ad529da 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -7,7 +7,7 @@ 0 0 382 - 256 + 283 @@ -327,8 +327,8 @@ 3 - - + + 110 @@ -349,22 +349,22 @@ - - + + N/A - - + + Required base - - + + N/A From 81cc44e0cb4072645a3ccc38fc9c71ebdac9393b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 17 May 2018 12:36:12 +0300 Subject: [PATCH 140/187] Add comments to storage.py --- dexbot/storage.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dexbot/storage.py b/dexbot/storage.py index 66f901826..8d1c45765 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -85,17 +85,25 @@ def clear(self): db_worker.clear(self.category) def save_order(self, order): + """ Save the order to the database + """ order_id = order['id'] db_worker.save_order(self.category, order_id, order) def remove_order(self, order): + """ Removes an order from the database + """ order_id = order['id'] db_worker.remove_order(self.category, order_id) def clear_orders(self): + """ Removes all worker's orders from the database + """ db_worker.clear_orders(self.category) def fetch_orders(self, worker=None): + """ Get all the orders (or just specific worker's orders) from the database + """ if not worker: worker = self.category return db_worker.fetch_orders(worker) From 5d5ca5c64ef1c76cb3d8574c826e661f6eccd251 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 17 May 2018 14:41:50 +0300 Subject: [PATCH 141/187] Add error validation to strategy widgets --- .../controllers/create_worker_controller.py | 22 ++++++++++--------- dexbot/controllers/strategy_controller.py | 18 +++++++++++++++ dexbot/strategies/staggered_orders.py | 2 +- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 4c4ede087..b6da2b708 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -183,32 +183,34 @@ def validate_account_not_in_use(self, account): @gui_error def validate_form(self): - error_text = '' + error_texts = [] base_asset = self.view.base_asset_input.currentText() quote_asset = self.view.quote_asset_input.text() worker_name = self.view.worker_name_input.text() if not self.validate_asset(base_asset): - error_text += 'Field "Base Asset" does not have a valid asset.\n' + error_texts.append('Field "Base Asset" does not have a valid asset.') if not self.validate_asset(quote_asset): - error_text += 'Field "Quote Asset" does not have a valid asset.\n' + error_texts.append('Field "Quote Asset" does not have a valid asset.') if not self.validate_market(base_asset, quote_asset): - error_text += "Market {}/{} doesn't exist.\n".format(base_asset, quote_asset) + error_texts.append("Market {}/{} doesn't exist.".format(base_asset, quote_asset)) if self.mode == 'add': account = self.view.account_input.text() private_key = self.view.private_key_input.text() if not self.validate_worker_name(worker_name): - error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) + error_texts.append('Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) if not self.validate_account_name(account): - error_text += "Account doesn't exist.\n" + error_texts.append("Account doesn't exist.") if not self.validate_account(account, private_key): - error_text += 'Private key is invalid.\n' + error_texts.append('Private key is invalid.') if not self.validate_account_not_in_use(account): - error_text += 'Use a different account. "{}" is already in use.\n'.format(account) + error_texts.append('Use a different account. "{}" is already in use.'.format(account)) elif self.mode == 'edit': if not self.validate_worker_name(worker_name, self.view.worker_name): - error_text += 'Worker name needs to be unique. "{}" is already in use.\n'.format(worker_name) - error_text = error_text.rstrip() # Remove the extra line-ending + error_texts.append('Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) + + error_texts.extend(self.view.strategy_widget.strategy_controller.validation_errors()) + error_text = '\n'.join(error_texts) if error_text: dialog = NoticeDialog(error_text) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index e451ef9bf..4e62c77cd 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -58,6 +58,12 @@ def set_config_values(self, worker_data): else: self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(False) + def validation_errors(self): + error_texts = [] + if not self.view.strategy_widget.amount_input.value(): + error_texts.append("Amount can't be 0") + return error_texts + @property def values(self): data = { @@ -139,6 +145,18 @@ def set_required_base(self, text): def set_required_quote(self, text): self.view.strategy_widget.required_quote_text.setText(text) + def validation_errors(self): + error_texts = [] + if not self.view.strategy_widget.amount_input.value(): + error_texts.append("Amount can't be 0") + if not self.view.strategy_widget.spread_input.value(): + error_texts.append("Spread can't be 0") + if not self.view.strategy_widget.increment_input.value(): + error_texts.append("Increment can't be 0") + if not self.view.strategy_widget.lower_bound_input.value(): + error_texts.append("Lower bound can't be 0") + return error_texts + @property def values(self): data = { diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index a5132eec1..6ba90b247 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -183,7 +183,7 @@ def calculate_amounts(buy_prices, sell_prices, amount, spread, increment): @staticmethod def get_required_assets(market, amount, spread, increment, lower_bound, upper_bound): - if not lower_bound or not increment: + if not amount or not lower_bound or not increment: return None ticker = market.ticker() From 02dcd71d1b8c2d79c5f4eeebc5f7ec38396ac25e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 17 May 2018 14:42:24 +0300 Subject: [PATCH 142/187] Change staggered orders amount calculation logic --- dexbot/strategies/staggered_orders.py | 28 ++++++++------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6ba90b247..41fdf50d7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -40,6 +40,7 @@ def init_strategy(self): self.clear_orders() center_price = self.calculate_center_price() + amount = self.amount spread = self.spread increment = self.increment lower_bound = self.lower_bound @@ -51,21 +52,8 @@ def init_strategy(self): # Calculate sell prices sell_prices = self.calculate_sell_prices(center_price, spread, increment, upper_bound) - # Calculate buy amounts - highest_buy_price = buy_prices.pop(0) - buy_orders = [{'amount': self.amount, 'price': highest_buy_price}] - for buy_price in buy_prices: - last_amount = buy_orders[-1]['amount'] - amount = last_amount / math.sqrt(1 + self.increment) - buy_orders.append({'amount': amount, 'price': buy_price}) - - # Calculate sell amounts - lowest_sell_price = highest_buy_price * math.sqrt(1 + self.spread + self.increment) - sell_orders = [{'amount': self.amount, 'price': lowest_sell_price}] - for sell_price in sell_prices: - last_amount = sell_orders[-1]['amount'] - amount = last_amount / math.sqrt(1 + self.increment) - sell_orders.append({'amount': amount, 'price': sell_price}) + # Calculate buy and sell amounts + buy_orders, sell_orders = self.calculate_amounts(buy_prices, sell_prices, amount, spread, increment) # Make sure there is enough balance for the buy orders needed_buy_asset = 0 @@ -106,12 +94,12 @@ def replace_order(self, order): self.remove_order(order) if order['base']['symbol'] == self.market['base']['symbol']: # Buy order - amount = order['quote']['amount'] price = order['price'] * self.spread + amount = order['quote']['amount'] new_order = self.market_sell(amount, price) else: # Sell order - amount = order['base']['amount'] price = order['price'] / self.spread + amount = order['base']['amount'] new_order = self.market_buy(amount, price) self.save_order(new_order) @@ -141,7 +129,7 @@ def calculate_buy_prices(center_price, spread, increment, lower_bound): buy_price = center_price / math.sqrt(1 + spread) while buy_price > lower_bound: buy_prices.append(buy_price) - buy_price = buy_price * (1 - increment) + buy_price = buy_price / (1 + increment) return buy_prices @staticmethod @@ -172,8 +160,8 @@ def calculate_amounts(buy_prices, sell_prices, amount, spread, increment): sell_orders = [] if sell_prices: lowest_sell_price = sell_prices.pop(0) - # amount = highest_buy_price * math.sqrt(1 + spread + increment) ? - sell_orders.append({'amount': amount, 'price': lowest_sell_price}) + current_amount = amount * math.sqrt(1 + spread + increment) + sell_orders.append({'amount': current_amount, 'price': lowest_sell_price}) for sell_price in sell_prices: last_amount = sell_orders[-1]['amount'] current_amount = last_amount / math.sqrt(1 + increment) From f7d9e4f89d0a6b10053d47b8663e5ccb00fec63c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 17 May 2018 14:56:05 +0300 Subject: [PATCH 143/187] Add spread validation to relative orders --- dexbot/controllers/strategy_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 4e62c77cd..27d919dfc 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -62,6 +62,8 @@ def validation_errors(self): error_texts = [] if not self.view.strategy_widget.amount_input.value(): error_texts.append("Amount can't be 0") + if not self.view.strategy_widget.spread_input.value(): + error_texts.append("Spread can't be 0") return error_texts @property From f3ef0408819904ec309f1f24cc15cbbe70fb33b7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 18 May 2018 14:20:56 +0300 Subject: [PATCH 144/187] Change center price calculation logic --- dexbot/basestrategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index dbd97698d..1f67af2f4 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -1,5 +1,6 @@ import logging import time +import math from .storage import Storage from .statemachine import StateMachine @@ -151,7 +152,7 @@ def calculate_center_price(self): ) self.disabled = True else: - center_price = (highest_bid['price'] + lowest_ask['price']) / 2 + center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) return center_price def calculate_relative_center_price(self, spread, order_ids=None): From 2ce5b1945f3cf8e5e69c27deb648ca77d66e28cc Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 18 May 2018 14:37:59 +0300 Subject: [PATCH 145/187] Change cancel order logic Log the exception instead of raising it Patch made by ihaywood3 --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 1f67af2f4..230fe7433 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -301,7 +301,7 @@ def _cancel(self, orders): self.bitshares.txbuffer.clear() return False else: - raise + self.log.exception("Unable to cancel order") return True def cancel(self, orders): From d3564636fde2edfdc990a602f0659f44d2c16b26 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 18 May 2018 14:38:42 +0300 Subject: [PATCH 146/187] Change dexbot version number to 0.1.27 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 864d36c2f..98c73107a 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.26' +VERSION = '0.1.27' AUTHOR = "codaone" __version__ = VERSION From 12a2ec404d2b0d2a8973c9b2ba3cfb04cd4ab792 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 18 May 2018 15:41:26 +0300 Subject: [PATCH 147/187] Change staggered orders logic There are still some bugs in it, so it's not yet ready for release --- dexbot/strategies/staggered_orders.py | 43 +++++++++++++++++++++------ 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 41fdf50d7..830fd6b2e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -27,7 +27,14 @@ def __init__(self, *args, **kwargs): self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] - self.check_orders() + if self['setup_done']: + self.place_orders() + else: + self.init_strategy() + + if self.view: + self.update_gui_profit() + self.update_gui_slider() def error(self, *args, **kwargs): self.cancel_all() @@ -90,6 +97,9 @@ def init_strategy(self): self['setup_done'] = True def replace_order(self, order): + """ Replaces an order with a reverse order + buy orders become sell orders and sell orders become buy orders + """ self.log.info('Change detected, updating orders') self.remove_order(order) @@ -98,20 +108,35 @@ def replace_order(self, order): amount = order['quote']['amount'] new_order = self.market_sell(amount, price) else: # Sell order - price = order['price'] / self.spread - amount = order['base']['amount'] + price = (order['price'] ** -1) / self.spread + amount = order['quote']['amount'] new_order = self.market_buy(amount, price) self.save_order(new_order) + def place_order(self, order): + if order['base']['symbol'] == self.market['base']['symbol']: # Buy order + price = order['price'] + amount = order['quote']['amount'] + self.market_buy(amount, price) + else: # Sell order + price = order['price'] ** -1 + amount = order['base']['amount'] + self.market_sell(amount, price) + + def place_orders(self): + """ Place all the orders found in the database + """ + self.cancel_all() + orders = self.fetch_orders() + for order_id, order in orders.items(): + self.place_order(order) + def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ - if not self['setup_done']: - self.init_strategy() - orders = self.fetch_orders() - for order in orders: + for order_id, order in orders.items(): current_order = self.get_order(order) if not current_order: self.replace_order(order) @@ -153,7 +178,7 @@ def calculate_amounts(buy_prices, sell_prices, amount, spread, increment): buy_orders.append({'amount': amount, 'price': highest_buy_price}) for buy_price in buy_prices: last_amount = buy_orders[-1]['amount'] - current_amount = last_amount / math.sqrt(1 + increment) + current_amount = last_amount * math.sqrt(1 + increment) buy_orders.append({'amount': current_amount, 'price': buy_price}) # Calculate sell amounts @@ -197,7 +222,7 @@ def get_required_assets(market, amount, spread, increment, lower_bound, upper_bo needed_buy_asset = 0 for buy_order in buy_orders: - needed_buy_asset += buy_order['amount'] + needed_buy_asset += buy_order['amount'] * buy_order['price'] needed_sell_asset = 0 for sell_order in sell_orders: From 50cbb92a3091cd002b697a9e4e3561e87cd0a617 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 18 May 2018 16:10:14 +0300 Subject: [PATCH 148/187] Update relative_orders.py --- dexbot/strategies/relative_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 59c96059a..a514a5a5a 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -62,7 +62,7 @@ def calculate_order_prices(self): if self.is_center_price_dynamic: self.center_price = self.calculate_relative_center_price(self.worker['spread'], self['order_ids']) - self.buy_price = self.center_price * (1 - (self.worker["spread"] / 2) / 100) + self.buy_price = self.center_price / (1 + (self.worker["spread"] / 2) / 100) self.sell_price = self.center_price * (1 + (self.worker["spread"] / 2) / 100) def error(self, *args, **kwargs): From 9847f041245c6590f662f93374a2765b02dec190 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 18 May 2018 16:14:35 +0300 Subject: [PATCH 149/187] Update relative_orders.py --- dexbot/strategies/relative_orders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index a514a5a5a..7118c4979 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -2,6 +2,7 @@ from dexbot.queue.idle_queue import idle_add from bitshares.amount import Amount +import math class Strategy(BaseStrategy): @@ -62,8 +63,8 @@ def calculate_order_prices(self): if self.is_center_price_dynamic: self.center_price = self.calculate_relative_center_price(self.worker['spread'], self['order_ids']) - self.buy_price = self.center_price / (1 + (self.worker["spread"] / 2) / 100) - self.sell_price = self.center_price * (1 + (self.worker["spread"] / 2) / 100) + self.buy_price = self.center_price / math.sqrt(1 + (self.worker["spread"] / 100)) + self.sell_price = self.center_price * math.sqrt(1 + (self.worker["spread"] / 100)) def error(self, *args, **kwargs): self.cancel_all() From 9f63b8e4e4d3956236ed7b3faca9ea9c60c1dff4 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sat, 19 May 2018 21:15:08 +1000 Subject: [PATCH 150/187] detect the graphene error "Assert Exception: now <= trx.expiration" which usually represents the node being unable to sync the blockchain do a number of retries and display an explanation on the GUI --- dexbot/basestrategy.py | 8 ++++++++ dexbot/ui.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index dbd97698d..f79edd724 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -445,5 +445,13 @@ def retry_action(self, action, *args, **kwargs): self.bitshares.txbuffer.clear() self.account.refresh() time.sleep(2) + elif "now <= trx.expiration" in str(e): # usually loss of sync to blockchain + if tries > MAX_TRIES: + raise + else: + tries += 1 + self.log.warning("retrying on '{}'".format(str(e))) + self.bitshares.txbuffer.clear() + time.sleep(6) # wait at least a BitShares block else: raise diff --git a/dexbot/ui.py b/dexbot/ui.py index f563e91a4..0b8e32557 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -167,7 +167,8 @@ def confirmalert(msg): # it's here because both GUI and CLI might use it -TRANSLATIONS = {'amount_to_sell.amount > 0': "You need to have sufficient buy and sell amounts in your account"} +TRANSLATIONS = {'amount_to_sell.amount > 0': "You need to have sufficient buy and sell amounts in your account", + 'now <= trx.expiration': "Your node has difficulty syncing to the blockchain, consider changing nodes"} def translate_error(err): From 0ebfe1d15957ae40ea173dbab3fc1ff4b6800468 Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Sat, 19 May 2018 15:34:10 +0300 Subject: [PATCH 151/187] Use always the latest version of py-bitshares --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3035637f4..de347d5b6 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def run(self): ], }, install_requires=[ - "bitshares", + "bitshares>=0.1.16", "uptick>=0.1.4", "click", "sqlalchemy", From 75822678693b7dc18a1b18ad31af29002225221f Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Sat, 19 May 2018 17:15:15 +0300 Subject: [PATCH 152/187] Fix wrong worker limit issue --- dexbot/views/worker_list.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 832828bcf..9a2803962 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -36,8 +36,6 @@ def __init__(self, main_ctrl): for worker_name in workers: self.add_worker_widget(worker_name) - # Limit the max amount of workers so that the performance isn't greatly affected - self.num_of_workers += 1 if self.num_of_workers >= self.max_workers: self.ui.add_worker_button.setEnabled(False) break @@ -59,6 +57,7 @@ def add_worker_widget(self, worker_name): self.worker_container.addWidget(widget) self.worker_widgets[worker_name] = widget + # Limit the max amount of workers so that the performance isn't greatly affected self.num_of_workers += 1 if self.num_of_workers >= self.max_workers: self.ui.add_worker_button.setEnabled(False) From 53eb5fb361d7e9eb45cedaa124ee1b3df66c49f3 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 21 May 2018 12:14:59 +1000 Subject: [PATCH 153/187] a fairly basic status system. Each wroker item gets a label a the bottom which is updated with new log entries of INFO?WARNING level for that worker. Some new logging commands added so users get a steady stream of updates as the worker is running, --- dexbot/basestrategy.py | 7 +++--- dexbot/controllers/main_controller.py | 9 +++++--- dexbot/strategies/relative_orders.py | 12 +++++++--- dexbot/views/errors.py | 33 ++++++++++++++++++--------- dexbot/views/ui/worker_item_widget.ui | 9 +++++++- dexbot/views/worker_item.py | 21 +++++++++++++++++ dexbot/views/worker_list.py | 5 ++++ dexbot/worker.py | 7 +++--- 8 files changed, 79 insertions(+), 24 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index dbd97698d..d41621365 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -319,9 +319,10 @@ def cancel(self, orders): def cancel_all(self): """ Cancel all orders of the worker's account """ + self.log.info('Cancelling all orders') if self.orders: - self.log.info('Canceling all orders') self.cancel(self.orders) + self.log.info("Orders cancelled") def market_buy(self, amount, price): self.log.info( @@ -335,7 +336,7 @@ def market_buy(self, amount, price): account=self.account.name, returnOrderId="head" ) - self.log.info('Placed buy order {}'.format(buy_transaction)) + self.log.debug('Placed buy order {}'.format(buy_transaction)) buy_order = self.get_order(buy_transaction['orderid']) return buy_order @@ -351,7 +352,7 @@ def market_sell(self, amount, price): account=self.account.name, returnOrderId="head" ) - self.log.info('Placed sell order {}'.format(sell_transaction)) + self.log.debug('Placed sell order {}'.format(sell_transaction)) sell_order = self.get_order(sell_transaction['orderid']) return sell_order diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 64f27b73c..17f67de7f 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -25,12 +25,15 @@ def __init__(self, bitshares_instance): fh.setFormatter(formatter) logger.addHandler(fh) logger.setLevel(logging.INFO) - pyqth = PyQtHandler() - pyqth.setLevel(logging.ERROR) - logger.addHandler(pyqth) + self.pyqt_handler = PyQtHandler() + self.pyqt_handler.setLevel(logging.INFO) + logger.addHandler(self.pyqt_handler) logger.info("DEXBot {} on python {} {}".format(VERSION, sys.version[:6], sys.platform), extra={ 'worker_name': 'NONE', 'account': 'NONE', 'market': 'NONE'}) + def set_info_handler(self, handler): + self.pyqt_handler.set_info_handler(handler) + def create_worker(self, worker_name, config, view): # Todo: Add some threading here so that the GUI doesn't freeze if self.worker_manager and self.worker_manager.is_alive(): diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 59c96059a..4c0177834 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -10,7 +10,7 @@ class Strategy(BaseStrategy): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self.log.info("Initalising Relative Orders") # Define Callbacks self.onMarketUpdate += self.check_orders self.onAccount += self.check_orders @@ -34,7 +34,6 @@ def __init__(self, *args, **kwargs): self.initial_balance = self['initial_balance'] or 0 self.worker_name = kwargs.get('name') self.view = kwargs.get('view') - self.check_orders() @property @@ -115,6 +114,8 @@ def update_orders(self): self['order_ids'] = order_ids + self.log.info("New orders complete") + # Some orders weren't successfully created, redo them if len(order_ids) < 2 and not self.disabled: self.update_orders() @@ -122,14 +123,19 @@ def update_orders(self): def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ + + self.log.info("Market event: checking orders...") + stored_sell_order = self['sell_order'] stored_buy_order = self['buy_order'] + current_sell_order = self.get_updated_order(stored_sell_order) current_buy_order = self.get_updated_order(stored_buy_order) - if not current_sell_order or not current_buy_order: # Either buy or sell order is missing, update both orders self.update_orders() + else: + self.log.info("Orders correct on market") if self.view: self.update_gui_profit() diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index 513098dfc..8f0038c11 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -13,22 +13,33 @@ class PyQtHandler(logging.Handler): Based on Vinay Sajip's DBHandler class (http://www.red-dove.com/python_logging.html) """ + def __init__(self): + logging.Handler.__init__(self) + self.info_handler = None + def emit(self, record): # Use default formatting: self.format(record) message = record.msg - extra = translate_error(message) - if record.exc_info: - if not extra: - extra = translate_error(repr(record.exc_info[1])) - detail = logging._defaultFormatter.formatException(record.exc_info) - else: - detail = None - if hasattr(record, "worker_name"): - title = "Error on {}".format(record.worker_name) + if record.levelno > logging.WARNING: + extra = translate_error(message) + if record.exc_info: + if not extra: + extra = translate_error(repr(record.exc_info[1])) + detail = logging._defaultFormatter.formatException(record.exc_info) + else: + detail = None + if hasattr(record, "worker_name"): + title = "Error on {}".format(record.worker_name) + else: + title = "DEXBot Error" + idle_add(show_dialog, title, message, extra, detail) else: - title = "DEXBot Error" - idle_add(show_dialog, title, message, extra, detail) + if self.info_handler and hasattr(record, "worker_name"): + idle_add(self.info_handler, record.worker_name, record.levelno, message) + + def set_info_handler(self, info_handler): + self.info_handler = info_handler def gui_error(func): diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index 971260d3e..d59b61fd5 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -10,7 +10,7 @@ 0 0 480 - 138 + 179 @@ -548,6 +548,13 @@ border-right: 2px solid #005B78; + + + + + + + diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 387ed57b8..45070d751 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -9,6 +9,21 @@ from PyQt5 import QtWidgets +def pyqt_set_trace(): + '''Set a tracepoint in the Python debugger that works with Qt''' + from PyQt5.QtCore import pyqtRemoveInputHook + import pdb + import sys + pyqtRemoveInputHook() + # set up the debugger + debugger = pdb.Pdb() + debugger.reset() + # custom next to get outside of function scope + debugger.do_next(None) # run the next command + users_frame = sys._getframe().f_back # frame where the user invoked `pyqt_set_trace()` + debugger.interaction(users_frame, None) + + class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): def __init__(self, worker_name, config, main_ctrl, view): @@ -51,6 +66,7 @@ def setup_ui_data(self, config): @gui_error def start_worker(self): + self.set_status("Starting worker") self._start_worker() self.main_ctrl.create_worker(self.worker_name, self.worker_config, self.view) @@ -61,6 +77,8 @@ def _start_worker(self): @gui_error def pause_worker(self): + pyqt_set_trace() + self.set_status("Pausing worker") self._pause_worker() self.main_ctrl.stop_worker(self.worker_name) @@ -123,3 +141,6 @@ def handle_edit_worker(self): self.main_ctrl.replace_worker_config(self.worker_name, new_worker_name, edit_worker_dialog.worker_data) self.reload_widget(new_worker_name) self.worker_name = new_worker_name + + def set_status(self, status): + self.worker_status.setText(status) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 832828bcf..db0ed448d 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -28,6 +28,7 @@ def __init__(self, main_ctrl): self.closing = False self.statusbar_updater = None self.statusbar_updater_first_run = True + self.main_ctrl.set_info_handler(self.set_worker_status) self.ui.add_worker_button.clicked.connect(self.handle_add_worker) @@ -137,3 +138,7 @@ def set_statusbar_message(self): self.ui.status_bar.showMessage("ver {} - Node delay: {:.2f}ms".format(__version__, latency)) else: self.ui.status_bar.showMessage("ver {} - Node disconnected".format(__version__)) + + def set_worker_status(self, worker_name, level, status): + if worker_name != 'NONE': + self.worker_widgets[worker_name].set_status(status) diff --git a/dexbot/worker.py b/dexbot/worker.py index 5fa128f54..66de545df 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -45,7 +45,7 @@ def __init__( user_worker_path = os.path.expanduser("~/bots") if os.path.exists(user_worker_path): sys.path.append(user_worker_path) - + def init_workers(self, config): """ Initialize the workers """ @@ -105,7 +105,7 @@ def update_notify(self): # Events def on_block(self, data): if self.jobs: - try: + try: for job in self.jobs: job() finally: @@ -190,7 +190,8 @@ def stop(self, worker_name=None): # Kill all of the workers for worker in self.workers: self.workers[worker].cancel_all() - self.notify.websocket.close() + if self.notify: + self.notify.websocket.close() def remove_worker(self, worker_name=None): if worker_name: From 4348fc9d931088313f489bd33390e08b141acb10 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 21 May 2018 16:30:10 +1000 Subject: [PATCH 154/187] pyqt_set_trace gone --- dexbot/views/worker_item.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 45070d751..c1e136dde 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -9,21 +9,6 @@ from PyQt5 import QtWidgets -def pyqt_set_trace(): - '''Set a tracepoint in the Python debugger that works with Qt''' - from PyQt5.QtCore import pyqtRemoveInputHook - import pdb - import sys - pyqtRemoveInputHook() - # set up the debugger - debugger = pdb.Pdb() - debugger.reset() - # custom next to get outside of function scope - debugger.do_next(None) # run the next command - users_frame = sys._getframe().f_back # frame where the user invoked `pyqt_set_trace()` - debugger.interaction(users_frame, None) - - class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): def __init__(self, worker_name, config, main_ctrl, view): @@ -77,7 +62,6 @@ def _start_worker(self): @gui_error def pause_worker(self): - pyqt_set_trace() self.set_status("Pausing worker") self._pause_worker() self.main_ctrl.stop_worker(self.worker_name) From 3f629efeceeadc3beee79f64985e6b51ccb54da6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 08:01:03 +0300 Subject: [PATCH 155/187] Change get_order logic in basestrategy.py get_order can now be used to get an order that wasn't made by the worker account --- dexbot/basestrategy.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 5c28485f6..ab9c04349 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -191,11 +191,18 @@ def orders(self): self.account.refresh() return [o for o in self.account.openorders if self.worker["market"] == o.market and self.account.openorders] - def get_order(self, order_id): - for order in self.orders: - if order['id'] == order_id: - return order - return False + @staticmethod + def get_order(order_id, return_none=True): + """ Returns the Order object for the order_id + + :param str order_id: blockchain object id of the order + :param bool return_none: return None instead of an empty + Order object when the order doesn't exist + """ + order = Order(order_id) + if return_none and order['deleted']: + return None + return order def get_updated_order(self, order): """ Tries to get the updated order from the API From 013b79a7b75d250243f528ce2f9fe01c84c247ff Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 08:22:54 +0300 Subject: [PATCH 156/187] Change market_buy and market_sell logic in basestrategy.py Add balance check and deal with orders that instantly fill --- dexbot/basestrategy.py | 44 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index ab9c04349..475b1ff3b 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -330,10 +330,21 @@ def cancel_all(self): self.cancel(self.orders) def market_buy(self, amount, price): + # Make sure we have enough balance for the order + if self.balance(self.market['base']) < price * amount: + self.log.critical( + "Insufficient buy balance, needed {} {}".format( + price * amount, self.market['base']['symbol']) + ) + self.disabled = True + return None + self.log.info( - 'Placing a buy order for {} {} @ {}'.format(price * amount, - self.market["base"]['symbol'], - price)) + 'Placing a buy order for {} {} @ {}'.format( + price * amount, self.market["base"]['symbol'], price) + ) + + # Place the order buy_transaction = self.retry_action( self.market.buy, price, @@ -342,14 +353,28 @@ def market_buy(self, amount, price): returnOrderId="head" ) self.log.info('Placed buy order {}'.format(buy_transaction)) - buy_order = self.get_order(buy_transaction['orderid']) + buy_order = self.get_order(buy_transaction['orderid'], return_none=False) + if buy_order['deleted']: + self.recheck_orders = True + return buy_order def market_sell(self, amount, price): + # Make sure we have enough balance for the order + if self.balance(self.market['quote']) < amount: + self.log.critical( + "Insufficient sell balance, needed {} {}".format( + amount, self.market['quote']['symbol']) + ) + self.disabled = True + return None + self.log.info( - 'Placing a sell order for {} {} @ {}'.format(amount, - self.market["quote"]['symbol'], - price)) + 'Placing a sell order for {} {} @ {}'.format( + amount, self.market["quote"]['symbol'], price) + ) + + # Place the order sell_transaction = self.retry_action( self.market.sell, price, @@ -358,7 +383,10 @@ def market_sell(self, amount, price): returnOrderId="head" ) self.log.info('Placed sell order {}'.format(sell_transaction)) - sell_order = self.get_order(sell_transaction['orderid']) + sell_order = self.get_order(sell_transaction['orderid'], return_none=False) + if sell_order['deleted']: + self.recheck_orders = True + return sell_order def purge(self): From 6420f35b3e7a02d542d326e48173521f6d404dde Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 08:33:09 +0300 Subject: [PATCH 157/187] Fix crashes in staggered orders Fixed few crashes when making orders didn't succeed, and fixed some calculation logic --- dexbot/basestrategy.py | 4 +++ dexbot/strategies/staggered_orders.py | 38 ++++++++++++++++++--------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 475b1ff3b..86ffec0ed 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -119,6 +119,9 @@ def __init__( bitshares_instance=self.bitshares ) + # Recheck flag - Tell the strategy to check for updated orders + self.recheck_orders = False + # Settings for bitshares instance self.bitshares.bundle = bool(self.worker.get("bundle", False)) @@ -319,6 +322,7 @@ def cancel(self, orders): success = self._cancel(orders) if not success and len(orders) > 1: + # One of the order cancels failed, cancel the orders one by one for order in orders: self._cancel(order) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 830fd6b2e..254f6f802 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -14,6 +14,7 @@ def __init__(self, *args, **kwargs): # Define Callbacks self.onMarketUpdate += self.check_orders self.onAccount += self.check_orders + self.ontick += self.tick self.error_ontick = self.error self.error_onMarketUpdate = self.error @@ -39,7 +40,6 @@ def __init__(self, *args, **kwargs): def error(self, *args, **kwargs): self.cancel_all() self.disabled = True - self.log.info(self.execute()) def init_strategy(self): # Make sure no orders remain @@ -87,42 +87,47 @@ def init_strategy(self): # Place the buy orders for buy_order in buy_orders: order = self.market_buy(buy_order['amount'], buy_order['price']) - self.save_order(order) + if order: + self.save_order(order) # Place the sell orders for sell_order in sell_orders: order = self.market_sell(sell_order['amount'], sell_order['price']) - self.save_order(order) + if order: + self.save_order(order) self['setup_done'] = True - def replace_order(self, order): + def place_reverse_order(self, order): """ Replaces an order with a reverse order buy orders become sell orders and sell orders become buy orders """ - self.log.info('Change detected, updating orders') - self.remove_order(order) - if order['base']['symbol'] == self.market['base']['symbol']: # Buy order - price = order['price'] * self.spread + price = order['price'] * (1 + self.spread) amount = order['quote']['amount'] new_order = self.market_sell(amount, price) else: # Sell order - price = (order['price'] ** -1) / self.spread + price = (order['price'] ** -1) / (1 + self.spread) amount = order['quote']['amount'] new_order = self.market_buy(amount, price) - self.save_order(new_order) + if new_order: + self.remove_order(order) + self.save_order(new_order) def place_order(self, order): + self.remove_order(order) + if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] amount = order['quote']['amount'] - self.market_buy(amount, price) + new_order = self.market_buy(amount, price) else: # Sell order price = order['price'] ** -1 amount = order['base']['amount'] - self.market_sell(amount, price) + new_order = self.market_sell(amount, price) + + self.save_order(new_order) def place_orders(self): """ Place all the orders found in the database @@ -139,7 +144,7 @@ def check_orders(self, *args, **kwargs): for order_id, order in orders.items(): current_order = self.get_order(order) if not current_order: - self.replace_order(order) + self.place_reverse_order(order) if self.view: self.update_gui_profit() @@ -230,6 +235,13 @@ def get_required_assets(market, amount, spread, increment, lower_bound, upper_bo return [needed_buy_asset, needed_sell_asset] + def tick(self, d): + """ ticks come in on every block + """ + if self.recheck_orders: + self.check_orders() + self.recheck_orders = False + # GUI updaters def update_gui_profit(self): pass From 0de9adb011e687927ddf940a58f68224678b022c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 08:47:08 +0300 Subject: [PATCH 158/187] Fix staggered orders asset calculation center price --- dexbot/strategies/staggered_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 254f6f802..0050352b0 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -212,7 +212,7 @@ def get_required_assets(market, amount, spread, increment, lower_bound, upper_bo elif not float(lowest_ask): return None else: - center_price = (highest_bid['price'] + lowest_ask['price']) / 2 + center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) # Calculate buy prices buy_prices = Strategy.calculate_buy_prices(center_price, spread, increment, lower_bound) From fe8454eab76cdd37ecc6ca1396017a42ac69daf7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 09:26:37 +0300 Subject: [PATCH 159/187] Fix get_order method in basestrategy --- dexbot/basestrategy.py | 5 ++++- dexbot/strategies/staggered_orders.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 31145fb1e..a0c782bf3 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -199,10 +199,13 @@ def orders(self): def get_order(order_id, return_none=True): """ Returns the Order object for the order_id - :param str order_id: blockchain object id of the order + :param str|dict order_id: blockchain object id of the order + can be a dict with the id key in it :param bool return_none: return None instead of an empty Order object when the order doesn't exist """ + if 'id' in order_id: + order_id = order_id['id'] order = Order(order_id) if return_none and order['deleted']: return None diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 0050352b0..c4a738fb0 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -142,7 +142,7 @@ def check_orders(self, *args, **kwargs): """ orders = self.fetch_orders() for order_id, order in orders.items(): - current_order = self.get_order(order) + current_order = self.get_order(order_id) if not current_order: self.place_reverse_order(order) From 01a0b48f314da918235b7392e6560798962ee6c0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 10:14:49 +0300 Subject: [PATCH 160/187] Fix staggered orders amount calculation --- dexbot/strategies/staggered_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c4a738fb0..338bcccad 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -108,7 +108,7 @@ def place_reverse_order(self, order): new_order = self.market_sell(amount, price) else: # Sell order price = (order['price'] ** -1) / (1 + self.spread) - amount = order['quote']['amount'] + amount = order['base']['amount'] new_order = self.market_buy(amount, price) if new_order: From 717fe1bae4b67d9347d297eae79a3d0400bc8ba1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 14:23:45 +0300 Subject: [PATCH 161/187] Change dexbot version number to 0.1.28 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 98c73107a..2cd73ed96 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.27' +VERSION = '0.1.28' AUTHOR = "codaone" __version__ = VERSION From 176599e8687ec53367010fd857d00f74af5848ca Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 14:26:10 +0300 Subject: [PATCH 162/187] Change dexbot version number to 0.1.29 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 98c73107a..957853d4a 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.27' +VERSION = '0.1.29' AUTHOR = "codaone" __version__ = VERSION From 622a01e47167534139ccb8494a878d6d8da57dd2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 14:39:28 +0300 Subject: [PATCH 163/187] Change dexbot version number to 0.1.30 Also fix some small typos --- dexbot/__init__.py | 2 +- dexbot/basestrategy.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 864d36c2f..62dce44d9 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.26' +VERSION = '0.1.30' AUTHOR = "codaone" __version__ = VERSION diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index f79edd724..cbeeac23d 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -445,13 +445,13 @@ def retry_action(self, action, *args, **kwargs): self.bitshares.txbuffer.clear() self.account.refresh() time.sleep(2) - elif "now <= trx.expiration" in str(e): # usually loss of sync to blockchain + elif "now <= trx.expiration" in str(e): # Usually loss of sync to blockchain if tries > MAX_TRIES: raise else: tries += 1 self.log.warning("retrying on '{}'".format(str(e))) self.bitshares.txbuffer.clear() - time.sleep(6) # wait at least a BitShares block + time.sleep(6) # Wait at least a BitShares block else: raise From 98683594762e12e4a5b6e167ffc2d4b105e074f8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 14:45:44 +0300 Subject: [PATCH 164/187] Change dexbot version number to 0.2.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 98c73107a..d761a5731 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.27' +VERSION = '0.2.0' AUTHOR = "codaone" __version__ = VERSION From d6b6d9492badb10f17c4dfa38072d5d6207ce998 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 15:28:14 +0300 Subject: [PATCH 165/187] Fix worker crash on a rare occasion --- dexbot/__init__.py | 2 +- dexbot/basestrategy.py | 10 ++-------- dexbot/strategies/relative_orders.py | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 62dce44d9..d5d9d3d42 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.1.30' +VERSION = '0.1.31' AUTHOR = "codaone" __version__ = VERSION diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index e57189b7f..b415fafd7 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -223,14 +223,8 @@ def updated_open_orders(self): limit_orders = self.account['limit_orders'][:] for o in limit_orders: - base_amount = o['for_sale'] - assert type(base_amount) in [int, float], "o['for_sale'] not num {}".format(dict(o)) - assert type(o['sell_price']['base']['amount']) in [ - int, float], "o['sell_base']['base']['amount'] not num {}".format(dict(o)) - assert type(o['sell_price']['quote']['amount']) in [ - int, float], "o['sell_base']['quote']['amount'] not num {}".format(dict(o)) - - price = o['sell_price']['base']['amount'] / o['sell_price']['quote']['amount'] + base_amount = float(o['for_sale']) + price = float(o['sell_price']['base']['amount']) / float(o['sell_price']['quote']['amount']) quote_amount = base_amount / price o['sell_price']['base']['amount'] = base_amount o['sell_price']['quote']['amount'] = quote_amount diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 59c96059a..f2ecb1421 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -124,8 +124,8 @@ def check_orders(self, *args, **kwargs): """ stored_sell_order = self['sell_order'] stored_buy_order = self['buy_order'] - current_sell_order = self.get_updated_order(stored_sell_order) - current_buy_order = self.get_updated_order(stored_buy_order) + current_sell_order = self.get_order(stored_sell_order) + current_buy_order = self.get_order(stored_buy_order) if not current_sell_order or not current_buy_order: # Either buy or sell order is missing, update both orders From cd004d6b933240c7e4b738ed937321343cbc129c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 22 May 2018 15:32:03 +0300 Subject: [PATCH 166/187] Change staggered orders slider logic The slider now calculates the order sizes too --- dexbot/strategies/staggered_orders.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 338bcccad..34b18bf87 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -249,7 +249,8 @@ def update_gui_profit(self): def update_gui_slider(self): ticker = self.market.ticker() latest_price = ticker.get('latest').get('price') - total_balance = self.total_balance() + order_ids = self.fetch_orders().keys() + total_balance = self.total_balance(order_ids) total = (total_balance['quote'] * latest_price) + total_balance['base'] if not total: # Prevent division by zero From 42dc6fece5708173da46ad0bbd43e3d945aba9e2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 23 May 2018 08:00:56 +0300 Subject: [PATCH 167/187] Change relative orders import order --- dexbot/strategies/relative_orders.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 7118c4979..d28d08f60 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,9 +1,8 @@ +import math + from dexbot.basestrategy import BaseStrategy from dexbot.queue.idle_queue import idle_add -from bitshares.amount import Amount -import math - class Strategy(BaseStrategy): """ Relative Orders strategy From 18a05d3dad82d05767101a581258aa0e9018f1b8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 23 May 2018 08:24:56 +0300 Subject: [PATCH 168/187] Change py-bitshares dependency to be forced version 0.1.16 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index de347d5b6..a9928d85d 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def run(self): ], }, install_requires=[ - "bitshares>=0.1.16", + "bitshares==0.1.16", "uptick>=0.1.4", "click", "sqlalchemy", From 3633fc338e1ef921a1a7c3f30dbfde7b4d12ce02 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 23 May 2018 08:29:00 +0300 Subject: [PATCH 169/187] Add missing hidden imports to gui.spec and cli.spec --- cli.spec | 3 +++ gui.spec | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cli.spec b/cli.spec index 00d38dbb0..6294448a0 100644 --- a/cli.spec +++ b/cli.spec @@ -12,6 +12,9 @@ hiddenimports_strategies = [ 'dexbot.strategies.staggered_orders', 'dexbot.strategies.storagedemo', 'dexbot.strategies.walls', + 'dexbot.views.ui.forms', + 'dexbot.views.ui.forms.relative_orders_widget_ui', + 'dexbot.views.ui.forms.staggered_orders_widget_ui', ] hiddenimports_packaging = [ diff --git a/gui.spec b/gui.spec index 57f4bf9bd..753169ffe 100644 --- a/gui.spec +++ b/gui.spec @@ -12,6 +12,9 @@ hiddenimports_strategies = [ 'dexbot.strategies.staggered_orders', 'dexbot.strategies.storagedemo', 'dexbot.strategies.walls', + 'dexbot.views.ui.forms', + 'dexbot.views.ui.forms.relative_orders_widget_ui', + 'dexbot.views.ui.forms.staggered_orders_widget_ui', ] hiddenimports_packaging = [ From b0a13976871d150e99c7d50179b3095c68f8faca Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 23 May 2018 08:39:36 +0300 Subject: [PATCH 170/187] Fix pyuic problem --- cli.spec | 3 --- pyuic.json | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cli.spec b/cli.spec index 6294448a0..00d38dbb0 100644 --- a/cli.spec +++ b/cli.spec @@ -12,9 +12,6 @@ hiddenimports_strategies = [ 'dexbot.strategies.staggered_orders', 'dexbot.strategies.storagedemo', 'dexbot.strategies.walls', - 'dexbot.views.ui.forms', - 'dexbot.views.ui.forms.relative_orders_widget_ui', - 'dexbot.views.ui.forms.staggered_orders_widget_ui', ] hiddenimports_packaging = [ diff --git a/pyuic.json b/pyuic.json index 1bf28f2a6..09b83e80e 100644 --- a/pyuic.json +++ b/pyuic.json @@ -1,6 +1,7 @@ { "files": [ ["dexbot/views/ui/*.ui", "dexbot/views/ui/"], + ["dexbot/views/ui/forms/*.ui", "dexbot/views/ui/forms"], ["dexbot/resources/*.qrc", "dexbot/resources/"] ], "hooks": [], From bdf2f70c6315da28f5e02fa6c9bee723b51197f4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 23 May 2018 09:36:51 +0300 Subject: [PATCH 171/187] Add option for center price offset --- dexbot/basestrategy.py | 2 +- dexbot/controllers/strategy_controller.py | 6 ++++++ dexbot/strategies/relative_orders.py | 6 +++++- dexbot/views/ui/create_worker_window.ui | 2 +- dexbot/views/ui/edit_worker_window.ui | 2 +- .../views/ui/forms/relative_orders_widget.ui | 19 +++++++++++++------ 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 62f52a3a7..f7c456f7f 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -157,7 +157,7 @@ def calculate_center_price(self): center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) return center_price - def calculate_relative_center_price(self, spread, order_ids=None): + def calculate_center_price_with_offset(self, spread, order_ids=None): """ Calculate center price which shifts based on available funds """ ticker = self.market.ticker() diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 27d919dfc..60be611d9 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -58,6 +58,11 @@ def set_config_values(self, worker_data): else: self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(False) + if worker_data.get('center_price_offset', True): + self.view.strategy_widget.center_price_offset_checkbox.setChecked(True) + else: + self.view.strategy_widget.center_price_offset_checkbox.setChecked(False) + def validation_errors(self): error_texts = [] if not self.view.strategy_widget.amount_input.value(): @@ -73,6 +78,7 @@ def values(self): 'amount_relative': self.view.strategy_widget.relative_order_size_checkbox.isChecked(), 'center_price': self.view.strategy_widget.center_price_input.value(), 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), + 'center_price_offset': self.view.strategy_widget.center_price_offset_checkbox.isChecked(), 'spread': self.view.strategy_widget.spread_input.value() } return data diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 4d1bb1c4c..ad29d7a77 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -26,6 +26,7 @@ def __init__(self, *args, **kwargs): self.center_price = self.worker["center_price"] self.is_relative_order_size = self.worker['amount_relative'] + self.is_center_price_offset = self.worker.get('center_price_offset', False) self.order_size = float(self.worker['amount']) self.buy_price = None @@ -60,7 +61,10 @@ def amount_base(self): def calculate_order_prices(self): if self.is_center_price_dynamic: - self.center_price = self.calculate_relative_center_price(self.worker['spread'], self['order_ids']) + if self.is_center_price_offset: + self.center_price = self.calculate_center_price_with_offset(self.worker['spread'], self['order_ids']) + else: + self.center_price = self.calculate_center_price() self.buy_price = self.center_price / math.sqrt(1 + (self.worker["spread"] / 100)) self.sell_price = self.center_price * math.sqrt(1 + (self.worker["spread"] / 100)) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index a5d0c60ba..9e6e9eb1a 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -6,7 +6,7 @@ 0 0 - 418 + 428 345 diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index a8c28df04..bbdf9c44c 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -6,7 +6,7 @@ 0 0 - 400 + 428 302 diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index e239b0451..1c04963d0 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -7,7 +7,7 @@ 0 0 446 - 203 + 225 @@ -100,7 +100,7 @@ - + @@ -128,7 +128,7 @@ - + false @@ -165,7 +165,7 @@ - + @@ -193,7 +193,7 @@ - + @@ -221,7 +221,7 @@ - + Calculate center price dynamically @@ -238,6 +238,13 @@ + + + + Center Price Offset based on asset balances + + + From 474b8ab167f30ce15ae36a3ec80e8c5dd76030fb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 23 May 2018 14:16:16 +0300 Subject: [PATCH 172/187] Fix crash in relative orders --- dexbot/basestrategy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index f7c456f7f..3e61d2ebc 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -204,6 +204,8 @@ def get_order(order_id, return_none=True): :param bool return_none: return None instead of an empty Order object when the order doesn't exist """ + if not order_id: + return None if 'id' in order_id: order_id = order_id['id'] order = Order(order_id) From 138734c7ade23dd497ed6c6bad0d5b2b757935fa Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 23 May 2018 14:17:44 +0300 Subject: [PATCH 173/187] Change offset center price calculation logic Static center price can now be given to the method and the mehod will properly offset it --- dexbot/basestrategy.py | 77 +++++++++---------- dexbot/controllers/strategy_controller.py | 1 + dexbot/strategies/relative_orders.py | 12 ++- .../views/ui/forms/relative_orders_widget.ui | 2 +- 4 files changed, 49 insertions(+), 43 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 3e61d2ebc..e8af83190 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -139,54 +139,53 @@ def __init__( 'is_disabled': lambda: self.disabled} ) - def calculate_center_price(self): + def calculate_center_price(self, suppress_errors=False): ticker = self.market.ticker() highest_bid = ticker.get("highestBid") lowest_ask = ticker.get("lowestAsk") - if highest_bid is None or highest_bid == 0.0: - self.log.critical( - "Cannot estimate center price, there is no highest bid." - ) - self.disabled = True + if not float(highest_bid): + if not suppress_errors: + self.log.critical( + "Cannot estimate center price, there is no highest bid." + ) + self.disabled = True + return None elif lowest_ask is None or lowest_ask == 0.0: - self.log.critical( - "Cannot estimate center price, there is no lowest ask." - ) - self.disabled = True - else: - center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) - return center_price + if not suppress_errors: + self.log.critical( + "Cannot estimate center price, there is no lowest ask." + ) + self.disabled = True + return None - def calculate_center_price_with_offset(self, spread, order_ids=None): + center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) + return center_price + + def calculate_offset_center_price(self, spread, center_price=None, order_ids=None): """ Calculate center price which shifts based on available funds """ - ticker = self.market.ticker() - highest_bid = ticker.get("highestBid").get('price') - lowest_ask = ticker.get("lowestAsk").get('price') - latest_price = ticker.get('latest').get('price') - if highest_bid is None or highest_bid == 0.0: - self.log.critical( - "Cannot estimate center price, there is no highest bid." - ) - self.disabled = True - elif lowest_ask is None or lowest_ask == 0.0: - self.log.critical( - "Cannot estimate center price, there is no lowest ask." - ) - self.disabled = True + if center_price is None: + # No center price was given so we simply calculate the center price + calculated_center_price = self.calculate_center_price() + center_price = calculated_center_price else: - total_balance = self.total_balance(order_ids) - total = (total_balance['quote'] * latest_price) + total_balance['base'] + # Center price was given so we only use the calculated center price + # for quote to base asset conversion + calculated_center_price = self.calculate_center_price(True) + if not calculated_center_price: + calculated_center_price = center_price - if not total: # Prevent division by zero - percentage = 0.5 - else: - percentage = (total_balance['base'] / total) - center_price = (highest_bid + lowest_ask) / 2 - lowest_price = center_price * (1 - spread / 100) - highest_price = center_price * (1 + spread / 100) - relative_center_price = ((highest_price - lowest_price) * percentage) + lowest_price - return relative_center_price + total_balance = self.total_balance(order_ids) + total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] + + if not total: # Prevent division by zero + percentage = 0 + else: + percentage = (total_balance['base'] / total) + lowest_price = center_price / math.sqrt(1 + spread) + highest_price = center_price * math.sqrt(1 + spread) + offset_center_price = ((highest_price - lowest_price) * percentage) + lowest_price + return offset_center_price @property def orders(self): diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 60be611d9..3988168f9 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -57,6 +57,7 @@ def set_config_values(self, worker_data): self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(True) else: self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(False) + self.view.strategy_widget.center_price_input.setDisabled(False) if worker_data.get('center_price_offset', True): self.view.strategy_widget.center_price_offset_checkbox.setChecked(True) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index ad29d7a77..0540960b1 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -28,6 +28,7 @@ def __init__(self, *args, **kwargs): self.is_relative_order_size = self.worker['amount_relative'] self.is_center_price_offset = self.worker.get('center_price_offset', False) self.order_size = float(self.worker['amount']) + self.spread = self.worker.get('spread') / 100 self.buy_price = None self.sell_price = None @@ -62,12 +63,17 @@ def amount_base(self): def calculate_order_prices(self): if self.is_center_price_dynamic: if self.is_center_price_offset: - self.center_price = self.calculate_center_price_with_offset(self.worker['spread'], self['order_ids']) + self.center_price = self.calculate_offset_center_price( + self.spread, order_ids=self['order_ids']) else: self.center_price = self.calculate_center_price() + else: + if self.is_center_price_offset: + self.center_price = self.calculate_offset_center_price( + self.spread, self.center_price, self['order_ids']) - self.buy_price = self.center_price / math.sqrt(1 + (self.worker["spread"] / 100)) - self.sell_price = self.center_price * math.sqrt(1 + (self.worker["spread"] / 100)) + self.buy_price = self.center_price / math.sqrt(1 + self.spread) + self.sell_price = self.center_price * math.sqrt(1 + self.spread) def error(self, *args, **kwargs): self.cancel_all() diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index 1c04963d0..e43a8db81 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -241,7 +241,7 @@ - Center Price Offset based on asset balances + Center price offset based on asset balances From 3265ceb455a9ad6e45ff85d925f21d58f6bfddc7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 23 May 2018 14:30:18 +0300 Subject: [PATCH 174/187] Change dexbot version number to 0.2.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index d761a5731..a5e04fb6f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.2.0' +VERSION = '0.2.1' AUTHOR = "codaone" __version__ = VERSION From 57afa15459735c835cd5b7a8e9b7af5005367668 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 24 May 2018 14:50:44 +0300 Subject: [PATCH 175/187] Add number rounding to amounts and prices on buy/sell logs --- dexbot/basestrategy.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 5d3d27f7e..9ef252a70 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -334,18 +334,22 @@ def cancel_all(self): self.log.info("Orders cancelled") def market_buy(self, amount, price): + symbol = self.market['base']['symbol'] + precision = self.market['base']['precision'] + base_amount = self.truncate(price * amount, precision) + # Make sure we have enough balance for the order - if self.balance(self.market['base']) < price * amount: + if self.balance(self.market['base']) < base_amount: self.log.critical( "Insufficient buy balance, needed {} {}".format( - price * amount, self.market['base']['symbol']) + base_amount, symbol) ) self.disabled = True return None self.log.info( 'Placing a buy order for {} {} @ {}'.format( - price * amount, self.market["base"]['symbol'], price) + base_amount, symbol, round(price, 8)) ) # Place the order @@ -364,18 +368,22 @@ def market_buy(self, amount, price): return buy_order def market_sell(self, amount, price): + symbol = self.market['quote']['symbol'] + precision = self.market['quote']['precision'] + quote_amount = self.truncate(amount, precision) + # Make sure we have enough balance for the order - if self.balance(self.market['quote']) < amount: + if self.balance(self.market['quote']) < quote_amount: self.log.critical( "Insufficient sell balance, needed {} {}".format( - amount, self.market['quote']['symbol']) + amount, symbol) ) self.disabled = True return None self.log.info( 'Placing a sell order for {} {} @ {}'.format( - amount, self.market["quote"]['symbol'], price) + quote_amount, symbol, round(price, 8)) ) # Place the order @@ -494,3 +502,9 @@ def retry_action(self, action, *args, **kwargs): time.sleep(6) # Wait at least a BitShares block else: raise + + @staticmethod + def truncate(number, decimals): + """ Change the decimal point of a number without rounding + """ + return math.floor(number * 10 ** decimals) / 10 ** decimals From bf66a92449672d0f2794abf61f43966edff1fefb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 24 May 2018 14:53:45 +0300 Subject: [PATCH 176/187] Fix typos --- dexbot/basestrategy.py | 4 ++-- dexbot/strategies/relative_orders.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 9ef252a70..4330e140a 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -328,10 +328,10 @@ def cancel(self, orders): def cancel_all(self): """ Cancel all orders of the worker's account """ - self.log.info('Cancelling all orders') + self.log.info('Canceling all orders') if self.orders: self.cancel(self.orders) - self.log.info("Orders cancelled") + self.log.info("Orders canceled") def market_buy(self, amount, price): symbol = self.market['base']['symbol'] diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 46277ffea..48238dd3e 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -10,7 +10,7 @@ class Strategy(BaseStrategy): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.log.info("Initalising Relative Orders") + self.log.info("Initializing Relative Orders") # Define Callbacks self.onMarketUpdate += self.check_orders self.onAccount += self.check_orders @@ -133,7 +133,6 @@ def update_orders(self): def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ - self.log.info("Market event: checking orders...") stored_sell_order = self['sell_order'] From eb41769ab3aa140383367c4665de832f7fffa714 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 24 May 2018 14:59:48 +0300 Subject: [PATCH 177/187] Change dexbot version number to 0.2.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a5e04fb6f..fb0c416e0 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.2.1' +VERSION = '0.2.2' AUTHOR = "codaone" __version__ = VERSION From 0d8c50c84c082127e7d21fbbe153f44e23831b51 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 08:15:38 +0300 Subject: [PATCH 178/187] Remove duplicate balance check from relative orders --- dexbot/strategies/relative_orders.py | 29 ++++++++-------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 48238dd3e..b9800afcd 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -98,29 +98,16 @@ def update_orders(self): amount_quote = self.amount_quote # Buy Side - if float(self.balance(self.market["base"])) < self.buy_price * amount_base: - self.log.critical( - 'Insufficient buy balance, needed {} {}'.format(self.buy_price * amount_base, - self.market['base']['symbol']) - ) - self.disabled = True - else: - buy_order = self.market_buy(amount_base, self.buy_price) - if buy_order: - self['buy_order'] = buy_order - order_ids.append(buy_order['id']) + buy_order = self.market_buy(amount_base, self.buy_price) + if buy_order: + self['buy_order'] = buy_order + order_ids.append(buy_order['id']) # Sell Side - if float(self.balance(self.market["quote"])) < amount_quote: - self.log.critical( - "Insufficient sell balance, needed {} {}".format(amount_quote, self.market['quote']['symbol']) - ) - self.disabled = True - else: - sell_order = self.market_sell(amount_quote, self.sell_price) - if sell_order: - self['sell_order'] = sell_order - order_ids.append(sell_order['id']) + sell_order = self.market_sell(amount_quote, self.sell_price) + if sell_order: + self['sell_order'] = sell_order + order_ids.append(sell_order['id']) self['order_ids'] = order_ids From 6dcd3d429ec9ec89b0b682d54a7a8a9f02a98b65 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 08:17:16 +0300 Subject: [PATCH 179/187] Add return_none parameter to market_buy and market_sell --- dexbot/basestrategy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 4330e140a..126d9d7bf 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -333,7 +333,7 @@ def cancel_all(self): self.cancel(self.orders) self.log.info("Orders canceled") - def market_buy(self, amount, price): + def market_buy(self, amount, price, return_none=False): symbol = self.market['base']['symbol'] precision = self.market['base']['precision'] base_amount = self.truncate(price * amount, precision) @@ -361,13 +361,13 @@ def market_buy(self, amount, price): returnOrderId="head" ) self.log.debug('Placed buy order {}'.format(buy_transaction)) - buy_order = self.get_order(buy_transaction['orderid'], return_none=False) - if buy_order['deleted']: + buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) + if buy_order and buy_order['deleted']: self.recheck_orders = True return buy_order - def market_sell(self, amount, price): + def market_sell(self, amount, price, return_none=False): symbol = self.market['quote']['symbol'] precision = self.market['quote']['precision'] quote_amount = self.truncate(amount, precision) @@ -395,8 +395,8 @@ def market_sell(self, amount, price): returnOrderId="head" ) self.log.debug('Placed sell order {}'.format(sell_transaction)) - sell_order = self.get_order(sell_transaction['orderid'], return_none=False) - if sell_order['deleted']: + sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) + if sell_order and sell_order['deleted']: self.recheck_orders = True return sell_order From 0cd1ede8c97200bd5c0c26aee818fd70c262af3f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 08:17:56 +0300 Subject: [PATCH 180/187] Change log messages in relative orders --- dexbot/strategies/relative_orders.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index b9800afcd..1de1033a7 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -111,7 +111,7 @@ def update_orders(self): self['order_ids'] = order_ids - self.log.info("New orders complete") + self.log.info("Done placing orders") # Some orders weren't successfully created, redo them if len(order_ids) < 2 and not self.disabled: @@ -120,8 +120,6 @@ def update_orders(self): def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ - self.log.info("Market event: checking orders...") - stored_sell_order = self['sell_order'] stored_buy_order = self['buy_order'] current_sell_order = self.get_order(stored_sell_order) From 76c8ff620e66a7b16e9d90a5c726198386a18de6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 08:19:32 +0300 Subject: [PATCH 181/187] Change code layout in relative orders --- dexbot/strategies/relative_orders.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 1de1033a7..d6e702d75 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -11,6 +11,7 @@ class Strategy(BaseStrategy): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log.info("Initializing Relative Orders") + # Define Callbacks self.onMarketUpdate += self.check_orders self.onAccount += self.check_orders @@ -38,6 +39,10 @@ def __init__(self, *args, **kwargs): self.view = kwargs.get('view') self.check_orders() + def error(self, *args, **kwargs): + self.cancel_all() + self.disabled = True + @property def amount_quote(self): """ Get quote amount, calculate if order size is relative @@ -74,11 +79,6 @@ def calculate_order_prices(self): self.buy_price = self.center_price / math.sqrt(1 + self.spread) self.sell_price = self.center_price * math.sqrt(1 + self.spread) - def error(self, *args, **kwargs): - self.cancel_all() - self.disabled = True - self.log.info(self.execute()) - def update_orders(self): self.log.info('Change detected, updating orders') From 477dc32589e346ca3e05ba8b966e84818af12d7d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 08:20:25 +0300 Subject: [PATCH 182/187] Fix order placing bug in staggered orders --- dexbot/strategies/staggered_orders.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 34b18bf87..96f0ba0ae 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -135,7 +135,8 @@ def place_orders(self): self.cancel_all() orders = self.fetch_orders() for order_id, order in orders.items(): - self.place_order(order) + if not self.get_order(order_id): + self.place_order(order) def check_orders(self, *args, **kwargs): """ Tests if the orders need updating From 722031968355aa00d299da9a78b8dd2d38e562fc Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 08:38:02 +0300 Subject: [PATCH 183/187] Add logging messages to staggered orders --- dexbot/strategies/staggered_orders.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 96f0ba0ae..574acee56 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -10,6 +10,7 @@ class Strategy(BaseStrategy): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.log.info("Initializing Staggered Orders") # Define Callbacks self.onMarketUpdate += self.check_orders @@ -97,6 +98,7 @@ def init_strategy(self): self.save_order(order) self['setup_done'] = True + self.log.info("Done placing orders") def place_reverse_order(self, order): """ Replaces an order with a reverse order @@ -138,14 +140,21 @@ def place_orders(self): if not self.get_order(order_id): self.place_order(order) + self.log.info("Done placing orders") + def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ + order_placed = False orders = self.fetch_orders() for order_id, order in orders.items(): current_order = self.get_order(order_id) if not current_order: self.place_reverse_order(order) + order_placed = True + + if order_placed: + self.log.info("Done placing orders") if self.view: self.update_gui_profit() From d566db12410bbea8c66f24cb7c7979cfc493306f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 10:37:58 +0300 Subject: [PATCH 184/187] Fix crash when an order is filled instantly after its placement --- dexbot/basestrategy.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index e8af83190..c93e66609 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -358,6 +358,9 @@ def market_buy(self, amount, price): self.log.info('Placed buy order {}'.format(buy_transaction)) buy_order = self.get_order(buy_transaction['orderid'], return_none=False) if buy_order['deleted']: + # The API doesn't return data on orders that don't exist + # We need to calculate the data on our own + buy_order = self.calculate_order_data(buy_order, amount, price) self.recheck_orders = True return buy_order @@ -388,10 +391,22 @@ def market_sell(self, amount, price): self.log.info('Placed sell order {}'.format(sell_transaction)) sell_order = self.get_order(sell_transaction['orderid'], return_none=False) if sell_order['deleted']: + # The API doesn't return data on orders that don't exist + # We need to calculate the data on our own + sell_order = self.calculate_order_data(sell_order, amount, price) + sell_order.invert() self.recheck_orders = True return sell_order + def calculate_order_data(self, order, amount, price): + quote_asset = Amount(amount, self.market['quote']['symbol']) + order['quote'] = quote_asset + order['price'] = price + base_asset = Amount(amount * price, self.market['base']['symbol']) + order['base'] = base_asset + return order + def purge(self): """ Clear all the worker data from the database and cancel all orders """ From bf454c8790e86e4821c2f26dc12ae9bb942b1fb3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 10:39:17 +0300 Subject: [PATCH 185/187] Change dexbot version number to 0.2.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a5e04fb6f..fb0c416e0 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.2.1' +VERSION = '0.2.2' AUTHOR = "codaone" __version__ = VERSION From 697e69352936fd42f462b1d78575c24ff07a08d1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 12:59:00 +0300 Subject: [PATCH 186/187] Change dexbot version number to 0.2.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index fb0c416e0..9a1effc88 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.2.2' +VERSION = '0.2.3' AUTHOR = "codaone" __version__ = VERSION From d6e7f72e94aeb32f13440b8dfedc627393c99bab Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 13:15:00 +0300 Subject: [PATCH 187/187] Fix missing arguments in relative orders --- dexbot/strategies/relative_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index d6e702d75..64a38b93a 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -98,13 +98,13 @@ def update_orders(self): amount_quote = self.amount_quote # Buy Side - buy_order = self.market_buy(amount_base, self.buy_price) + buy_order = self.market_buy(amount_base, self.buy_price, True) if buy_order: self['buy_order'] = buy_order order_ids.append(buy_order['id']) # Sell Side - sell_order = self.market_sell(amount_quote, self.sell_price) + sell_order = self.market_sell(amount_quote, self.sell_price, True) if sell_order: self['sell_order'] = sell_order order_ids.append(sell_order['id'])